# xdto-compile v1.1 — 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')] [Alias('Path')] [string]$XsdPath, [Parameter(Mandatory=$true, ParameterSetName='Inline')] [string]$Xsd, [Parameter(Mandatory=$true)] [string]$OutputDir, [string]$Name, [object]$Synonym, [string]$Comment, [switch]$Force ) $ErrorActionPreference = "Stop" # Эти пространства имён предоставляет сама платформа — пакетов в конфигурации # для них нет и быть не должно (выведено по корпусу: импортируются, но # targetNamespace с таким значением ни у одного пакета нет) $PLATFORM_NS = @( "http://v8.1c.ru/8.1/data/core", "http://v8.1c.ru/8.1/data/enterprise", "http://v8.1c.ru/8.1/data/enterprise/current-config", "http://v8.1c.ru/8.1/data-composition-system/settings", "http://v8.1c.ru/8.3/data/ext", "http://www.w3.org/2001/XMLSchema" ) [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $XDTO_NS = "http://v8.1c.ru/8.1/xdto" $XS_NS = "http://www.w3.org/2001/XMLSchema" $XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" $MD_NS = "http://v8.1c.ru/8.3/MDClasses" $V8_NS = "http://v8.1c.ru/8.1/data/core" # --- 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.get_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) { $mode = "deny" try { $pj = Find-V8Project $cfgDir if ($pj) { $cfg = Get-Content -Path $pj -Raw -Encoding UTF8 | ConvertFrom-Json if ($cfg.PSObject.Properties.Name -contains 'editingAllowedCheck' -and $cfg.editingAllowedCheck) { $mode = [string]$cfg.editingAllowedCheck } } } catch {} return $mode } function Assert-EditAllowed([string]$targetPath) { try { $mode = $null $d = $targetPath for ($i = 0; $i -lt 20 -and $d; $i++) { $cfgXml = Join-Path $d "Configuration.xml" $supportBin = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" # Автономный объект (внешняя обработка/отчёт) — граница климба foreach ($x in @(Get-ChildItem -Path $d -Filter "*.xml" -File -ErrorAction SilentlyContinue)) { if (Test-ExternalObjectRoot $x.FullName) { return } } if (Test-Path $cfgXml) { if (Test-Path $supportBin) { $mode = Get-EditMode $d if ($mode -eq "off") { return } $msg = "Конфигурация находится на поддержке (Ext/ParentConfigurations.bin). Правка может быть запрещена." if ($mode -eq "warn") { Write-Warning $msg; return } throw "$msg Снимите с поддержки (/support-edit) или задайте editingAllowedCheck в .v8-project.json." } return } $parent = [System.IO.Path]::GetDirectoryName($d) if ($parent -eq $d) { break } $d = $parent } } catch [System.Management.Automation.RuntimeException] { throw } catch {} } # --- Load the schema --- if ($PSCmdlet.ParameterSetName -eq 'Inline') { $xsdText = $Xsd $defaultName = "Package" } else { if (-not (Test-Path $XsdPath -PathType Leaf)) { throw "Файл XSD не найден: $XsdPath" } $xsdText = [System.IO.File]::ReadAllText($XsdPath) $defaultName = [System.IO.Path]::GetFileNameWithoutExtension($XsdPath) } $xdoc = New-Object System.Xml.XmlDocument $xdoc.PreserveWhitespace = $false try { $xdoc.LoadXml($xsdText) } catch { throw "Не удалось разобрать XSD: $($_.Exception.Message)" } $schema = $xdoc.DocumentElement if ($schema.get_LocalName() -ne "schema" -or $schema.NamespaceURI -ne $XS_NS) { throw "Ожидался корневой в пространстве имён $XS_NS" } $targetNs = $schema.GetAttribute("targetNamespace") # --- Emit-tree primitives ----------------------------------------------------- # A node carries attributes in canonical order; QName values keep their namespace # so prefixes can be assigned per depth at serialization time (the dNpN scheme). function New-Node([string]$tag) { return [pscustomobject]@{ Tag = $tag; Attrs = (New-Object System.Collections.ArrayList); Children = (New-Object System.Collections.ArrayList); Text = $null; Prefix = $null; DeclareNs = $null } } function Add-Attr($node, [string]$name, $value) { # $value НЕ типизируем: [string]$null коэрсится в "" и атрибут ложно появляется if ($null -eq $value) { return } [void]$node.Attrs.Add([pscustomobject]@{ Name = $name; Value = [string]$value; Ns = $null; Local = $null }) } function Add-QAttr($node, [string]$name, $ns, $local) { if ($null -eq $local) { return } [void]$node.Attrs.Add([pscustomobject]@{ Name = $name; Value = $null; Ns = [string]$ns; Local = [string]$local }) } function Add-QListAttr($node, [string]$name, $pairs, [bool]$clark) { if (-not $pairs -or $pairs.Count -eq 0) { return } [void]$node.Attrs.Add([pscustomobject]@{ Name = $name; Value = $null; Ns = $null; Local = $null; List = $pairs; Clark = $clark }) } function Add-Child($node, $child) { if ($child) { [void]$node.Children.Add($child) } } # Canonical attribute order per element — derived by topological sort over the # whole 8.3.24 corpus (acc + erp, 760 packages), see docs/1c-xdto-spec.md. $ATTR_ORDER = @{ "package" = @("targetNamespace", "elementFormQualified", "attributeFormQualified") "import" = @("namespace") "objectType" = @("name", "base", "open", "abstract", "mixed", "ordered", "sequenced") "property" = @("name", "ref", "type", "lowerBound", "upperBound", "nillable", "fixed", "default", "form", "localName", "qualified") "valueType" = @("name", "base", "variety", "itemType", "length", "memberTypes", "minExclusive", "maxExclusive", "minInclusive", "maxInclusive", "minLength", "maxLength", "totalDigits", "fractionDigits", "whiteSpace") "typeDef" = @("xsi:type", "base", "mixed", "open", "ordered", "sequenced", "variety", "itemType", "length", "memberTypes", "minExclusive", "maxExclusive", "minInclusive", "maxInclusive", "minLength", "maxLength", "totalDigits", "fractionDigits", "whiteSpace") "enumeration" = @("xsi:type") } function Sort-Attrs($node) { $order = $ATTR_ORDER[$node.Tag] if (-not $order) { return $node.Attrs } $sorted = New-Object System.Collections.ArrayList foreach ($n in $order) { foreach ($a in $node.Attrs) { if ($a.Name -eq $n) { [void]$sorted.Add($a) } } } foreach ($a in $node.Attrs) { if ($order -notcontains $a.Name) { [void]$sorted.Add($a) } } return $sorted } function Esc([string]$s) { if ($null -eq $s) { return "" } return $s.Replace("&", "&").Replace("<", "<").Replace(">", ">").Replace('"', """) } function EscText([string]$s) { if ($null -eq $s) { return "" } return $s.Replace("&", "&").Replace("<", "<").Replace(">", ">") } # --- Serializer with the dNpN prefix scheme --- $out = New-Object System.Text.StringBuilder function Serialize-Node($node, [int]$depth, $inherited) { $indent = "`t" * ($depth - 1) $attrs = Sort-Attrs $node # Namespaces needing a NEW declaration here: те, что ещё не в области видимости. # Сериализатор платформы объявляет префикс на первом узле, где он нужен, а # потомки его переиспользуют — отсюда d2p1 у property внутри objectType. $localNs = New-Object System.Collections.ArrayList function Need-Prefix([string]$ns) { if (-not $ns -or $ns -eq $XS_NS -or $ns -eq $XSI_NS) { return } if ($inherited.ContainsKey($ns)) { return } if (-not $localNs.Contains($ns)) { [void]$localNs.Add($ns) } } foreach ($a in $attrs) { if ($a.PSObject.Properties.Name -contains 'List' -and $a.List) { # Нотация Кларка несёт ns в значении и префикса не требует; редкие # случаи, где платформа его всё же объявила, приходят зеркалом declareNs if (-not $a.Clark) { foreach ($p in $a.List) { Need-Prefix $p.Ns } } } elseif ($a.Ns) { Need-Prefix $a.Ns } } # Свойство с qualified платформа сериализует с явным префиксом пространства # имён XDTO — и в имени тега, и в имени самого атрибута $hasQualified = $false foreach ($a in $attrs) { if ($a.Name -eq "qualified") { $hasQualified = $true } } if ($hasQualified) { Need-Prefix $XDTO_NS } if ($node.DeclareNs) { Need-Prefix $node.DeclareNs } $prefixOf = @{} foreach ($k in $inherited.Keys) { $prefixOf[$k] = $inherited[$k] } $nsDecls = "" for ($i = 0; $i -lt $localNs.Count; $i++) { # Осмысленный префикс из исходника (зеркало xdto:prefix) имеет приоритет $px = if ($i -eq 0 -and $node.Prefix) { $node.Prefix } else { "d${depth}p$($i + 1)" } $prefixOf[$localNs[$i]] = $px $nsDecls += " xmlns:$px=`"$(Esc $localNs[$i])`"" } function QVal([string]$ns, [string]$local) { if (-not $ns) { return $local } if ($ns -eq $XS_NS) { return "xs:$local" } if ($ns -eq $XSI_NS) { return "xsi:$local" } return "$($prefixOf[$ns]):$local" } $attrText = "" foreach ($a in $attrs) { if ($a.PSObject.Properties.Name -contains 'List' -and $a.List) { $vals = @() foreach ($p in $a.List) { if ($a.Clark) { $vals += $(if ($p.Ns) { "{$($p.Ns)}$($p.Local)" } else { $p.Local }) } else { $vals += (QVal $p.Ns $p.Local) } } $attrText += " $($a.Name)=`"$(Esc ($vals -join ' '))`"" } elseif ($a.Ns -or $a.Local) { $attrText += " $($a.Name)=`"$(Esc (QVal $a.Ns $a.Local))`"" } elseif ($a.Name -eq "qualified") { $attrText += " $($prefixOf[$XDTO_NS]):qualified=`"$(Esc $a.Value)`"" } else { $attrText += " $($a.Name)=`"$(Esc $a.Value)`"" } } $tagName = $node.Tag if ($hasQualified) { $tagName = "$($prefixOf[$XDTO_NS]):$($node.Tag)" } $hasChildren = $node.Children.Count -gt 0 # Пустое значение пишется самозакрывающимся тегом: , а не $hasText = ($null -ne $node.Text -and $node.Text -ne "") if (-not $hasChildren -and -not $hasText) { [void]$out.Append("$indent<$tagName$nsDecls$attrText/>`r`n") return } if ($hasText -and -not $hasChildren) { [void]$out.Append("$indent<$tagName$nsDecls$attrText>$(EscText $node.Text)`r`n") return } [void]$out.Append("$indent<$tagName$nsDecls$attrText>`r`n") foreach ($c in $node.Children) { Serialize-Node $c ($depth + 1) $prefixOf } [void]$out.Append("$indent`r`n") } # --- XSD reading helpers --- # Предупреждения о том, что XSD выражает, а модель XDTO — нет. Молча ронять # такие конструкции нельзя: пакет соберётся, а половина свойств исчезнет. $script:warnings = New-Object System.Collections.ArrayList function Warn([string]$msg) { if (-not $script:warnings.Contains($msg)) { [void]$script:warnings.Add($msg) } } function XA([System.Xml.XmlElement]$el, [string]$name) { if ($el.HasAttribute($name)) { return $el.GetAttribute($name) } return $null } function MA([System.Xml.XmlElement]$el, [string]$name) { # xdto: mirror attribute — the literal value to write into Package.bin. # Префикс не фиксируем: ищем по namespace, а не по строке "xdto:". $a = $el.Attributes.GetNamedItem($name, $XDTO_NS) if ($null -eq $a) { return $null } return $a.Value } function XChildren([System.Xml.XmlElement]$el, [string]$local) { $res = New-Object System.Collections.ArrayList foreach ($c in $el.ChildNodes) { if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.NamespaceURI -eq $XS_NS -and $c.get_LocalName() -eq $local) { [void]$res.Add($c) } } # ArrayList, а не @(): PowerShell разворачивает массив из одного элемента при return return ,$res } function XFirst([System.Xml.XmlElement]$el, [string]$local) { $r = XChildren $el $local if ($r.Count -gt 0) { return $r[0] } return $null } # Split a QName from the XSD into (ns, local) using that element's prefix scope function Split-QName([System.Xml.XmlElement]$el, [string]$qname) { if ($null -eq $qname -or $qname -eq "") { return $null } $parts = $qname.Split(":") if ($parts.Count -eq 2) { $ns = $el.GetNamespaceOfPrefix($parts[0]) $local = $parts[1] } else { # Прощающий ввод: голое имя типа трактуем как тип целевого пространства $ns = $el.GetNamespaceOfPrefix("") if (-not $ns) { $ns = $targetNs } $local = $parts[0] } return [pscustomobject]@{ Ns = $ns; Local = $local } } function Split-QNameList([System.Xml.XmlElement]$el, [string]$list) { if (-not $list) { return @() } $res = @() foreach ($q in ($list -split "\s+")) { if ($q) { $res += (Split-QName $el $q) } } return $res } $FACETS = @("length", "minLength", "maxLength", "totalDigits", "fractionDigits", "minInclusive", "maxInclusive", "minExclusive", "maxExclusive", "whiteSpace") # --- simpleType -> valueType / typeDef(ValueType) --- function Fill-SimpleType($node, [System.Xml.XmlElement]$st) { $restriction = XFirst $st "restriction" $list = XFirst $st "list" $union = XFirst $st "union" if ($list) { $it = Split-QName $list (XA $list "itemType") $mv = MA $list "variety" Add-Attr $node "variety" $(if ($null -ne $mv) { $mv } else { "List" }) if ($it) { Add-QAttr $node "itemType" $it.Ns $it.Local } return } if ($union) { $mv = MA $union "variety" Set-AttrValue $node "variety" $(if ($null -ne $mv) { $mv } else { "Union" }) $members = @(Split-QNameList $union (XA $union "memberTypes")) # По умолчанию нотация Кларка — так записано 125 из 135 memberTypes корпуса $useClark = ((MA $union "memberTypesForm") -ne "prefixed") if ($members.Count -gt 0) { Add-QListAttr $node "memberTypes" $members $useClark } $node.DeclareNs = MA $union "declareNs" foreach ($anon in (XChildren $union "simpleType")) { # typeDef в контексте простого типа xsi:type не несёт (40 узлов корпуса) $td = New-Node "typeDef" Fill-SimpleType $td $anon Add-Child $node $td } return } if ($restriction) { $b = Split-QName $restriction (XA $restriction "base") if ($b) { Add-QAttr $node "base" $b.Ns $b.Local } $mv = MA $restriction "variety" if ($null -ne $mv) { Add-Attr $node "variety" $mv } # Анонимный базовый тип внутри xs:restriction — typeDef без xsi:type $anonBase = XFirst $restriction "simpleType" if ($anonBase) { $td = New-Node "typeDef" Fill-SimpleType $td $anonBase Add-Child $node $td } foreach ($f in $FACETS) { foreach ($fe in (XChildren $restriction $f)) { Add-Attr $node $f (XA $fe "value") } } foreach ($pe in (XChildren $restriction "pattern")) { $pn = New-Node "pattern"; $pn.Text = (XA $pe "value"); Add-Child $node $pn } foreach ($en in (XChildren $restriction "enumeration")) { $enode = New-Node "enumeration" $mt = MA $en "type" if ($null -ne $mt) { $q = Split-QName $en $mt Add-QAttr $enode "xsi:type" $q.Ns $q.Local } $enode.Text = (XA $en "value") Add-Child $node $enode } } } function Get-PropKey($p) { foreach ($a in $p.Attrs) { if ($a.Name -eq "name") { return $a.Value } } foreach ($a in $p.Attrs) { if ($a.Name -eq "ref") { return "@" + $a.Local } } return $null } # Восстановить исходный порядок свойств по зеркалу xdto:order function Reorder-Properties($node, $names) { $props = @($node.Children | Where-Object { $_.Tag -eq "property" }) if ($props.Count -lt 2) { return } $byKey = @{} foreach ($p in $props) { $k = Get-PropKey $p if ($null -ne $k -and -not $byKey.ContainsKey($k)) { $byKey[$k] = $p } } $ordered = New-Object System.Collections.ArrayList foreach ($n in $names) { if ($byKey.ContainsKey($n)) { [void]$ordered.Add($byKey[$n]); $byKey.Remove($n) } } foreach ($p in $props) { if ($ordered -notcontains $p) { [void]$ordered.Add($p) } } $others = @($node.Children | Where-Object { $_.Tag -ne "property" }) $node.Children.Clear() foreach ($p in $ordered) { [void]$node.Children.Add($p) } foreach ($o in $others) { [void]$node.Children.Add($o) } } function Set-AttrValue($node, [string]$name, [string]$value) { foreach ($a in $node.Attrs) { if ($a.Name -eq $name) { $a.Value = $value; return } } Add-Attr $node $name $value } # --- element / attribute -> property --- function Build-Property([System.Xml.XmlElement]$el, [bool]$isAttribute) { $p = New-Node "property" $xsdName = XA $el "name" $mirrorName = MA $el "name" if ($null -ne $mirrorName) { Add-Attr $p "name" $mirrorName $localName = $xsdName } else { Add-Attr $p "name" $xsdName $localName = $null } $refQ = Split-QName $el (XA $el "ref") if ($refQ) { Add-QAttr $p "ref" $refQ.Ns $refQ.Local } $typeQ = Split-QName $el (XA $el "type") if ($typeQ) { Add-QAttr $p "type" $typeQ.Ns $typeQ.Local } if ($isAttribute) { Add-Attr $p "lowerBound" (MA $el "lowerBound") Add-Attr $p "upperBound" (MA $el "upperBound") Add-Attr $p "nillable" (MA $el "nillable") } else { Add-Attr $p "lowerBound" (XA $el "minOccurs") $maxOcc = XA $el "maxOccurs" if ($null -ne $maxOcc) { Add-Attr $p "upperBound" $(if ($maxOcc -eq "unbounded") { "-1" } else { $maxOcc }) } Add-Attr $p "nillable" (XA $el "nillable") } # XSD-шный fixed="V" несёт значение, в модели это fixed="true" + default="V". # Прощающий ввод: модельная форма через зеркало xdto:fixed тоже принимается. $mFixed = MA $el "fixed" if ($null -ne $mFixed) { Add-Attr $p "fixed" $mFixed Add-Attr $p "default" (XA $el "default") if ($mFixed -ceq "true" -and $null -eq (XA $el "default")) { Warn "Свойство `"$(XA $el 'name')`": xdto:fixed=`"true`" без default — платформа отвергнет пакет («Отсутствует фиксированное значение»). Значение задаётся атрибутом default, либо пишите XSD-форму fixed=`"значение`"" } } elseif ($null -ne (XA $el "fixed")) { Add-Attr $p "fixed" "true" Add-Attr $p "default" (XA $el "fixed") } else { Add-Attr $p "default" (XA $el "default") } if ($isAttribute) { Add-Attr $p "form" "Attribute" } else { $mf = MA $el "form" if ($null -ne $mf) { Add-Attr $p "form" $mf } } Add-Attr $p "localName" $localName Add-Attr $p "qualified" (MA $el "qualified") $p.Prefix = MA $el "prefix" # Anonymous inline type $anonSimple = XFirst $el "simpleType" $anonComplex = XFirst $el "complexType" if ($anonSimple) { $td = New-Node "typeDef" Add-Attr $td "xsi:type" "ValueType" Fill-SimpleType $td $anonSimple Add-Child $p $td } elseif ($anonComplex) { $td = New-Node "typeDef" Add-Attr $td "xsi:type" "ObjectType" Fill-ComplexType $td $anonComplex Add-Child $p $td } return $p } # --- complexType -> objectType / typeDef(ObjectType) --- # Разрешение xs:group / xs:attributeGroup по ссылке $script:GROUPS = @{} $script:ATTR_GROUPS = @{} function Resolve-Group([System.Xml.XmlElement]$el, [string]$kind) { $ref = XA $el "ref" if (-not $ref) { return $null } $q = Split-QName $el $ref if (-not $q) { return $null } $map = if ($kind -eq "group") { $script:GROUPS } else { $script:ATTR_GROUPS } if ($map.ContainsKey($q.Local)) { return $map[$q.Local] } return $null } # Модель XDTO знает только плоский список свойств: вложенные частицы уплощаются. # Каждое уплощение — предупреждение, потому что меняется смысл схемы. function Collect-Particle([System.Xml.XmlElement]$particle, $elemList, [ref]$isOpen, [string]$typeName, [int]$depth, [bool]$optionalize = $false) { if ($depth -gt 20) { return } foreach ($c in $particle.ChildNodes) { if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element -or $c.NamespaceURI -ne $XS_NS) { continue } switch ($c.get_LocalName()) { "element" { $prop = Build-Property $c $false # Ветка уплощённого xs:choice обязана стать необязательной: иначе # «одно из двух» превращается в «оба сразу», и тип нельзя заполнить if ($optionalize) { Set-AttrValue $prop "lowerBound" "0" } [void]$elemList.Add($prop) } "any" { $isOpen.Value = $true } "sequence" { Warn "$typeName : вложенная xs:sequence уплощена — модель XDTO хранит плоский список свойств" Collect-Particle $c $elemList $isOpen $typeName ($depth + 1) $optionalize } "choice" { $branches = @() foreach ($b in $c.ChildNodes) { if ($b.NodeType -eq [System.Xml.XmlNodeType]::Element -and $b.NamespaceURI -eq $XS_NS -and $b.HasAttribute("name")) { $branches += $b.GetAttribute("name") } } $list = if ($branches.Count -gt 0) { " (" + ($branches -join ", ") + ")" } else { "" } Warn ("$typeName : вложенная xs:choice уплощена — ветки$list сделаны необязательными. " + "Выбор одного из вариантов не сохранён: модель не запретит заполнить сразу несколько или ни одного") Collect-Particle $c $elemList $isOpen $typeName ($depth + 1) $true } "all" { Warn "$typeName : xs:all трактуется как последовательность" Collect-Particle $c $elemList $isOpen $typeName ($depth + 1) $optionalize } "group" { $g = Resolve-Group $c "group" if ($g) { foreach ($gc in $g.ChildNodes) { if ($gc.NodeType -eq [System.Xml.XmlNodeType]::Element -and $gc.NamespaceURI -eq $XS_NS -and @("sequence", "choice", "all") -contains $gc.get_LocalName()) { Collect-Particle $gc $elemList $isOpen $typeName ($depth + 1) $optionalize } } } else { Warn "$typeName : не найдена группа $(XA $c 'ref') — её свойства в пакет не попали" } } } # Кратность на самой частице модель выразить не может if (@("sequence", "choice", "all", "group") -contains $c.get_LocalName()) { if ((XA $c "maxOccurs") -or (XA $c "minOccurs")) { Warn "$typeName : кратность на вложенной частице () не выражается в модели XDTO" } } } } # open / ordered / sequenced / abstract / mixed: выводим где выводимо, # остальное приходит зеркалом xdto: function Set-TypeFlags($node, [System.Xml.XmlElement]$ct, $isOpen, $choice) { $mOpen = MA $ct "open" if ($null -ne $mOpen) { Add-Attr $node "open" $mOpen } elseif ($isOpen) { Add-Attr $node "open" "true" } $mOrdered = MA $ct "ordered" if ($null -ne $mOrdered) { Add-Attr $node "ordered" $mOrdered } elseif ($choice) { Add-Attr $node "ordered" "false" } $mSeq = MA $ct "sequenced" if ($null -ne $mSeq) { Add-Attr $node "sequenced" $mSeq } $mAbstract = MA $ct "abstract" if ($null -ne $mAbstract) { Add-Attr $node "abstract" $mAbstract } elseif ((XA $ct "abstract") -eq "true") { Add-Attr $node "abstract" "true" } $mMixed = MA $ct "mixed" if ($null -ne $mMixed) { Add-Attr $node "mixed" $mMixed } elseif ((XA $ct "mixed") -eq "true") { Add-Attr $node "mixed" "true" } } function Fill-ComplexType($node, [System.Xml.XmlElement]$ct) { # xs:complexContent/xs:extension carries the base type $content = XFirst $ct "complexContent" $body = $ct if ($content) { $ext = XFirst $content "extension" if ($ext) { $b = Split-QName $ext (XA $ext "base") if ($b) { Add-QAttr $node "base" $b.Ns $b.Local } $body = $ext } } # xs:simpleContent -> a "Text" property holding the element's own value $simple = XFirst $ct "simpleContent" if ($simple) { $ext = XFirst $simple "extension" if ($ext) { foreach ($a in (XChildren $ext "attribute")) { Add-Child $node (Build-Property $a $true) } $tp = New-Node "property" $tName = MA $ext "textName" Add-Attr $tp "name" $(if ($null -ne $tName) { $tName } else { "__content" }) $b = Split-QName $ext (XA $ext "base") if ($b) { Add-QAttr $tp "type" $b.Ns $b.Local } Add-Attr $tp "lowerBound" (MA $ext "textlowerBound") Add-Attr $tp "upperBound" (MA $ext "textupperBound") Add-Attr $tp "nillable" (MA $ext "textnillable") Add-Attr $tp "form" "Text" Add-Child $node $tp # xs:simpleContent не отменяет флаги самого xs:complexType Set-TypeFlags $node $ct $false $null return } } # Particle: xs:sequence (ordered) or xs:choice (ordered="false") $seq = XFirst $body "sequence" $cho = XFirst $body "choice" $all = XFirst $body "all" $grp = XFirst $body "group" $particle = if ($seq) { $seq } elseif ($cho) { $cho } elseif ($all) { $all } else { $grp } $isOpen = $false # Порядок в XDTO: сначала form="Attribute", потом остальные (верно для 96.5% # типов корпуса). Отклонения приходят зеркалом xdto:order. $elemProps = New-Object System.Collections.ArrayList $typeName = if ($ct.HasAttribute("name")) { $ct.GetAttribute("name") } else { "(анонимный тип)" } if ($particle) { $openRef = [ref]$isOpen if ($all) { Warn "$typeName : xs:all трактуется как последовательность" } if ($grp -and -not $seq -and -not $cho -and -not $all) { # Корневая частица задана ссылкой на группу — раскрываем её содержимое $g = Resolve-Group $grp "group" if ($g) { foreach ($gc in $g.ChildNodes) { if ($gc.NodeType -eq [System.Xml.XmlNodeType]::Element -and $gc.NamespaceURI -eq $XS_NS -and @("sequence", "choice", "all") -contains $gc.get_LocalName()) { Collect-Particle $gc $elemProps $openRef $typeName 1 } } } else { Warn "$typeName : не найдена группа $(XA $grp 'ref') — её свойства в пакет не попали" } } else { Collect-Particle $particle $elemProps $openRef $typeName 0 } $isOpen = $openRef.Value } foreach ($a in (XChildren $body "attribute")) { Add-Child $node (Build-Property $a $true) } # xs:attributeGroup раскрываем по ссылке foreach ($ag in (XChildren $body "attributeGroup")) { $g = Resolve-Group $ag "attributeGroup" if ($g) { foreach ($a in (XChildren $g "attribute")) { Add-Child $node (Build-Property $a $true) } } else { Warn "Не найдена группа атрибутов $(XA $ag 'ref') — её атрибуты в пакет не попали" } } foreach ($e in $elemProps) { Add-Child $node $e } if ((XChildren $body "anyAttribute").Count -gt 0) { $isOpen = $true } $mOrder = MA $ct "order" if ($null -ne $mOrder) { Reorder-Properties $node ($mOrder -split "\|") } Set-TypeFlags $node $ct $isOpen $cho } # --- Build the package tree --- $pkgNode = New-Node "package" Add-Attr $pkgNode "targetNamespace" $targetNs $efqMirror = MA $schema "elementFormQualified" $afqMirror = MA $schema "attributeFormQualified" $efd = XA $schema "elementFormDefault" $afd = XA $schema "attributeFormDefault" if ($null -ne $efqMirror) { Add-Attr $pkgNode "elementFormQualified" $efqMirror } elseif ($null -ne $efd) { Add-Attr $pkgNode "elementFormQualified" $(if ($efd -eq "qualified") { "true" } else { "false" }) } if ($null -ne $afqMirror) { Add-Attr $pkgNode "attributeFormQualified" $afqMirror } elseif ($null -ne $afd) { Add-Attr $pkgNode "attributeFormQualified" $(if ($afd -eq "qualified") { "true" } else { "false" }) } # Metadata properties from xs:annotation/xs:appinfo $metaName = $null; $metaComment = $null; $metaSynonym = @() $ann = XFirst $schema "annotation" if ($ann) { $appinfo = XFirst $ann "appinfo" if ($appinfo) { foreach ($pk in $appinfo.ChildNodes) { if ($pk.NodeType -ne [System.Xml.XmlNodeType]::Element -or $pk.NamespaceURI -ne $XDTO_NS) { continue } foreach ($f in $pk.ChildNodes) { if ($f.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue } switch ($f.get_LocalName()) { "name" { $metaName = $f.InnerText } "comment" { $metaComment = $f.InnerText } "synonym" { $metaSynonym += @{ Lang = $f.GetAttribute("lang"); Content = $f.InnerText } } } } } } } # Реестр глобальных групп — нужен до обхода, чтобы раскрывать ссылки foreach ($node in $schema.ChildNodes) { if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element -or $node.NamespaceURI -ne $XS_NS) { continue } $nm = XA $node "name" if ($node.get_LocalName() -eq "group" -and $nm) { $script:GROUPS[$nm] = $node } if ($node.get_LocalName() -eq "attributeGroup" -and $nm) { $script:ATTR_GROUPS[$nm] = $node } } # Конструкции XSD, которым в модели XDTO нет соответствия foreach ($sg in $schema.SelectNodes("//*[local-name()='element'][@substitutionGroup]")) { Warn "Подстановочные группы (substitutionGroup) не поддерживаются моделью XDTO — объявление $($sg.GetAttribute('name')) сохранено как обычное" } foreach ($idc in @("key", "keyref", "unique")) { if ($schema.SelectNodes("//*[local-name()='$idc']").Count -gt 0) { Warn "Ограничения целостности (xs:$idc) в модели XDTO не хранятся — отброшены" } } if ($schema.SelectNodes("//*[local-name()='redefine']").Count -gt 0) { Warn "xs:redefine не поддерживается — переопределения проигнорированы" } if ($schema.SelectNodes("//*[local-name()='include']").Count -gt 0) { Warn "xs:include проигнорирован: модель XDTO разрешает зависимости только по namespace. Соберите включаемую схему отдельным пакетом и добавьте " } foreach ($node in $schema.ChildNodes) { if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element -or $node.NamespaceURI -ne $XS_NS) { continue } switch ($node.get_LocalName()) { "annotation" { } "group" { } "attributeGroup" { } "notation" { } "import" { $n = New-Node "import" Add-Attr $n "namespace" (XA $node "namespace") Add-Child $pkgNode $n } "include" { } "element" { Add-Child $pkgNode (Build-Property $node $false) } "attribute" { Add-Child $pkgNode (Build-Property $node $true) } "simpleType" { $n = New-Node "valueType" Add-Attr $n "name" (XA $node "name") Fill-SimpleType $n $node Add-Child $pkgNode $n } "complexType" { $n = New-Node "objectType" Add-Attr $n "name" (XA $node "name") Fill-ComplexType $n $node Add-Child $pkgNode $n } } } # --- Serialize Package.bin --- # Модель XDTO требует строгой последовательности элементов верхнего уровня: # import → property → valueType → objectType. Порядок объявлений в XSD произвольный, # поэтому пересортировываем — иначе платформа отвергает пакет с «Ошибка преобразования # данных XDTO». Все 760 пакетов корпуса этому порядку удовлетворяют, так что # round-trip не затрагивается. $TOP_ORDER = @("import", "property", "valueType", "objectType") $sortedChildren = New-Object System.Collections.ArrayList foreach ($t in $TOP_ORDER) { foreach ($c in $pkgNode.Children) { if ($c.Tag -eq $t) { [void]$sortedChildren.Add($c) } } } foreach ($c in $pkgNode.Children) { if ($TOP_ORDER -notcontains $c.Tag) { [void]$sortedChildren.Add($c) } } $pkgNode.Children.Clear() foreach ($c in $sortedChildren) { [void]$pkgNode.Children.Add($c) } $attrsRoot = Sort-Attrs $pkgNode $rootAttrText = "" foreach ($a in $attrsRoot) { $rootAttrText += " $($a.Name)=`"$(Esc $a.Value)`"" } [void]$out.Append("`r`n") foreach ($c in $pkgNode.Children) { Serialize-Node $c 2 @{} } [void]$out.Append("") $binText = $out.ToString() # --- Resolve the package name --- if (-not $Name) { if ($metaName) { $Name = $metaName } else { $Name = $defaultName } } # Санация под идентификатор 1С $Name = ($Name -replace '[^\wЀ-ӿ]', '_') if ($Name -match '^\d') { $Name = "_$Name" } Assert-EditAllowed $OutputDir $pkgRoot = Join-Path $OutputDir "XDTOPackages" $pkgDir = Join-Path $pkgRoot $Name $extDir = Join-Path $pkgDir "Ext" $mdFile = Join-Path $pkgRoot "$Name.xml" $binFile = Join-Path $extDir "Package.bin" if ((Test-Path $binFile) -and -not $Force) { throw "Пакет уже существует: $binFile. Используйте -Force для перезаписи." } New-Item -ItemType Directory -Path $extDir -Force | Out-Null $encBom = New-Object System.Text.UTF8Encoding($true) [System.IO.File]::WriteAllText($binFile, $binText, $encBom) # --- Metadata object file --- if (-not $Synonym -and $metaSynonym.Count -gt 0) { $synItems = $metaSynonym } elseif ($Synonym -is [System.Collections.IDictionary]) { $synItems = @() foreach ($k in $Synonym.Keys) { $synItems += @{ Lang = [string]$k; Content = [string]$Synonym[$k] } } } elseif ($Synonym) { $synItems = @(@{ Lang = "ru"; Content = [string]$Synonym }) } else { $synItems = @(@{ Lang = "ru"; Content = $Name }) } if (-not $Comment -and $metaComment) { $Comment = $metaComment } $uuid = [guid]::NewGuid().ToString() $md = New-Object System.Text.StringBuilder function M([string]$s) { [void]$md.Append($s); [void]$md.Append("`r`n") } M '' M '' M "`t" M "`t`t" M "`t`t`t$(EscText $Name)" M "`t`t`t" foreach ($s in $synItems) { M "`t`t`t`t" M "`t`t`t`t`t$(EscText $s.Lang)" M "`t`t`t`t`t$(EscText $s.Content)" M "`t`t`t`t" } M "`t`t`t" if ($Comment) { M "`t`t`t$(EscText $Comment)" } else { M "`t`t`t" } M "`t`t`t$(EscText $targetNs)" M "`t`t" M "`t" [void]$md.Append("") [System.IO.File]::WriteAllText($mdFile, $md.ToString(), $encBom) # --- Register in Configuration.xml --- # Ранняя диагностика: отказ платформы при db-update дешевле поймать на сборке $xdtoRootDir = Join-Path $OutputDir "XDTOPackages" $declaredImports = @() foreach ($c in $pkgNode.Children) { if ($c.Tag -eq "import") { foreach ($a in $c.Attrs) { if ($a.Name -eq "namespace") { $declaredImports += $a.Value } } } } if ($declaredImports.Count -gt 0 -and (Test-Path $xdtoRootDir)) { $knownNs = @{} foreach ($other in (Get-ChildItem $xdtoRootDir -Directory -ErrorAction SilentlyContinue)) { $ob = Join-Path (Join-Path $other.FullName "Ext") "Package.bin" if (-not (Test-Path $ob)) { continue } try { $od = New-Object System.Xml.XmlDocument $od.Load($ob) $knownNs[$od.DocumentElement.GetAttribute("targetNamespace")] = $true } catch {} } foreach ($imp in $declaredImports) { if (-not $knownNs.ContainsKey($imp) -and $PLATFORM_NS -notcontains $imp) { Warn "Импорт `"$imp`" не разрешается: пакета с таким namespace в конфигурации нет. Платформа отвергнет пакет при обновлении — соберите зависимость первой" } } } $configXmlPath = Join-Path $OutputDir "Configuration.xml" $regResult = "no-config" if (Test-Path $configXmlPath) { $configDoc = New-Object System.Xml.XmlDocument $configDoc.PreserveWhitespace = $true $configDoc.Load($configXmlPath) $nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable) $nsMgr.AddNamespace("md", $MD_NS) $childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr) if ($childObjects) { $existing = $childObjects.SelectNodes("md:XDTOPackage", $nsMgr) $already = $false foreach ($e in $existing) { if ($e.InnerText -eq $Name) { $already = $true; break } } if ($already) { $regResult = "already" } else { $newElem = $configDoc.CreateElement("XDTOPackage", $MD_NS) $newElem.InnerText = $Name if ($existing.Count -gt 0) { $lastElem = $existing[$existing.Count - 1] $newWs = $configDoc.CreateWhitespace("`n`t`t`t") $childObjects.InsertAfter($newWs, $lastElem) | Out-Null $childObjects.InsertAfter($newElem, $newWs) | Out-Null } else { $lastChild = $childObjects.LastChild if ($lastChild -and $lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) { $newWs = $configDoc.CreateWhitespace("`n`t`t`t") $childObjects.InsertBefore($newWs, $lastChild) | Out-Null $childObjects.InsertBefore($newElem, $lastChild) | Out-Null } else { $childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null $childObjects.AppendChild($newElem) | Out-Null $childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null } } $cfgSettings = New-Object System.Xml.XmlWriterSettings $cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true) $cfgSettings.Indent = $false $stream = New-Object System.IO.FileStream($configXmlPath, [System.IO.FileMode]::Create) $writer = [System.Xml.XmlWriter]::Create($stream, $cfgSettings) $configDoc.Save($writer) $writer.Close() $stream.Close() $regResult = "added" } } } # --- Report --- $typeCount = 0 foreach ($c in $pkgNode.Children) { if ($c.Tag -eq "objectType" -or $c.Tag -eq "valueType") { $typeCount++ } } Write-Host "✓ Пакет XDTO собран: $Name" Write-Host " Namespace: $targetNs" Write-Host " Типов: $typeCount" Write-Host " Файлы: XDTOPackages/$Name.xml, XDTOPackages/$Name/Ext/Package.bin" if ($script:warnings.Count -gt 0) { Write-Host "" Write-Host "Предупреждения ($($script:warnings.Count)) — конструкции XSD без точного соответствия в модели XDTO:" foreach ($w in $script:warnings) { Write-Host " ! $w" } Write-Host "" } switch ($regResult) { "added" { Write-Host " Configuration.xml: $Name добавлен в ChildObjects" } "already" { Write-Host " Configuration.xml: $Name уже зарегистрирован" } "no-config" { Write-Host " Configuration.xml не найден — регистрация пропущена" } }