mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-27 07:01:02 +03:00
feat(xdto-compile,xdto-decompile,xdto-validate): пакеты XDTO из XML-схемы
Три навыка для работы с пакетами XDTO. Формат описания — обычная XSD, своего DSL нет: рутину снимает конвертер (локальные объявления префиксов dNpM на каждой ссылке, инвертированная кратность lowerBound/upperBound, фасеты атрибутами вместо дочерних элементов, обязательный порядок элементов верхнего уровня). То, чего XSD выразить не может — nillable у атрибута, qualified у свойства, «атрибут записан явно» — едет атрибутами из пространства имён модели XDTO по правилу «то же имя, что в Package.bin». Свойства объекта метаданных живут в xs:appinfo, поэтому пара decompile → compile замыкается без потерь. Инвариант bin → xsd → bin проверен побайтово на 760 пакетах выгрузок Бухгалтерии и ERP 8.3.24 (харнесс debug/xdto/roundtrip-corpus.mjs). Сборка из рукописной XSD проверена загрузкой в базу 8.3.24 — именно она вскрыла обязательный порядок import→property→valueType→objectType, невидимый для корпусной сверки: все выгрузки уже канонические. xdto-validate ловит два класса тихих дефектов, которые платформа не диагностирует: подмену неразрешённого чужого типа на xs:anyType при импорте XML-схемы и nillable у свойства-атрибута, теряемый экспортом схемы в Конфигураторе. Тесты: 18 снэпшот-кейсов на синтетических схемах (типовые конфигурации в репозиторий не тащим), паритет PS↔PY на общих эталонах. Раннер получил caseFiles — копирование файлов кейса в workDir для навыков с файловым, а не JSON входом. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d544071b1e
commit
d05aef54b4
@@ -0,0 +1,93 @@
|
||||
---
|
||||
name: xdto-compile
|
||||
description: Создание пакета XDTO 1С из XML-схемы (XSD). Используй когда нужно добавить пакет XDTO в конфигурацию — для обмена, интеграции, веб-сервиса, разбора внешнего XML-формата
|
||||
argument-hint: -XsdPath <файл.xsd> -OutputDir <каталог-конфигурации> [-Name <имя>] [-Synonym <синоним>] [-Comment <текст>] [-Force]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /xdto-compile — Создание пакета XDTO из XML-схемы
|
||||
|
||||
Собирает пакет XDTO по обычной XML-схеме: `XDTOPackages/<Имя>.xml`,
|
||||
`XDTOPackages/<Имя>/Ext/Package.bin` и регистрацию в `Configuration.xml`.
|
||||
|
||||
Отдельного DSL нет — входной формат это XSD. Вся механика перевода (локальные объявления
|
||||
префиксов `dNpM` на каждой ссылке, инвертированная кратность `lowerBound`/`upperBound`,
|
||||
фасеты атрибутами вместо дочерних элементов) делается навыком.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `XsdPath` | Путь к файлу XML-схемы |
|
||||
| `Xsd` | Схема строкой, вместо `-XsdPath` |
|
||||
| `OutputDir` | Каталог конфигурации (где лежит `Configuration.xml`) |
|
||||
| `Name` | Имя объекта метаданных. По умолчанию — из `xs:appinfo`, иначе имя файла XSD, санированное под идентификатор 1С |
|
||||
| `Synonym` | Синоним: строка (русский) или хеш-таблица `@{ru='…'; en='…'}` |
|
||||
| `Comment` | Комментарий |
|
||||
| `Force` | Перезаписать существующий пакет |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-compile.ps1" -XsdPath "<схема.xsd>" -OutputDir "<каталог>"
|
||||
```
|
||||
|
||||
Примеры:
|
||||
```powershell
|
||||
... -XsdPath bank.xsd -OutputDir C:\cfsrc\erp -Name ОбменСБанком -Synonym "Обмен с банком"
|
||||
... -XsdPath fss.xsd -OutputDir C:\cfsrc\erp -Force
|
||||
```
|
||||
|
||||
## Формат входа
|
||||
|
||||
Обычная XML-схема. Две вещи, которые XSD выразить не может (`nillable` у атрибута,
|
||||
`qualified` у отдельного свойства), пишутся атрибутами из пространства имён модели XDTO:
|
||||
|
||||
```xml
|
||||
<xs:attribute name="Представление" type="xs:string"
|
||||
xmlns:xdto="http://v8.1c.ru/8.1/xdto" xdto:nillable="true"/>
|
||||
```
|
||||
|
||||
Правило: **чего XSD сказать не может — пиши атрибутом `xdto:` с тем же именем, что в модели**.
|
||||
Схема при этом остаётся валидной. Полный список аннотаций — в справке ниже.
|
||||
|
||||
Свойства объекта метаданных можно задать прямо в схеме:
|
||||
|
||||
```xml
|
||||
<xs:annotation>
|
||||
<xs:appinfo>
|
||||
<xdto:package xmlns:xdto="http://v8.1c.ru/8.1/xdto">
|
||||
<xdto:name>ОбменСБанком</xdto:name>
|
||||
<xdto:synonym lang="ru">Обмен с банком</xdto:synonym>
|
||||
</xdto:package>
|
||||
</xs:appinfo>
|
||||
</xs:annotation>
|
||||
```
|
||||
|
||||
Принимается и не вполне каноническая схема: имя типа без префикса трактуется как тип
|
||||
целевого пространства имён, `form="qualified"` на глобальных объявлениях (так экспортирует
|
||||
сам Конфигуратор) — читается.
|
||||
|
||||
## Зависимости между пакетами
|
||||
|
||||
`<xs:import namespace="…"/>` разрешается по namespace среди пакетов конфигурации.
|
||||
Если пакета с таким пространством имён в конфигурации нет, платформа при загрузке молча
|
||||
подменит тип на `xs:anyType` — без ошибки. Сначала соберите пакеты-зависимости,
|
||||
затем зависящий, и проверьте результат через `/xdto-validate`.
|
||||
|
||||
## Типичный workflow
|
||||
|
||||
1. Получить XSD от контрагента (или выгрузить: Конфигуратор → пакет → Экспорт XML-схемы)
|
||||
2. `/xdto-compile -XsdPath <файл> -OutputDir <конфигурация>`
|
||||
3. `/xdto-validate` — убедиться, что все типы разрешились
|
||||
4. `/db-load-xml` + `/db-update` — загрузить в базу
|
||||
|
||||
Правка существующего пакета: `/xdto-decompile` → правка XSD → `/xdto-compile -Force`.
|
||||
Пара замыкается без потерь, включая свойства объекта метаданных.
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/xdto-validate <каталог>/XDTOPackages/<Имя> — типы, импорты, регистрация
|
||||
```
|
||||
@@ -0,0 +1,772 @@
|
||||
# xdto-compile v1.0 — 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"
|
||||
[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:schema> в пространстве имён $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
|
||||
# Пустое значение пишется самозакрывающимся тегом: <enumeration/>, а не <enumeration></enumeration>
|
||||
$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)</$tagName>`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</$tagName>`r`n")
|
||||
}
|
||||
|
||||
# --- XSD reading helpers ---
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
Add-Attr $p "fixed" (XA $el "fixed")
|
||||
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) ---
|
||||
|
||||
# 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"
|
||||
$particle = if ($seq) { $seq } else { $cho }
|
||||
$isOpen = $false
|
||||
|
||||
# Порядок в XDTO: сначала form="Attribute", потом остальные (верно для 96.5%
|
||||
# типов корпуса). Отклонения приходят зеркалом xdto:order.
|
||||
$elemProps = New-Object System.Collections.ArrayList
|
||||
if ($particle) {
|
||||
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" { [void]$elemProps.Add((Build-Property $c $false)) }
|
||||
"any" { $isOpen = $true }
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($a in (XChildren $body "attribute")) { Add-Child $node (Build-Property $a $true) }
|
||||
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 }
|
||||
switch ($node.get_LocalName()) {
|
||||
"annotation" { }
|
||||
"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("<package xmlns=`"$XDTO_NS`" xmlns:xs=`"$XS_NS`" xmlns:xsi=`"$XSI_NS`"$rootAttrText>`r`n")
|
||||
foreach ($c in $pkgNode.Children) { Serialize-Node $c 2 @{} }
|
||||
[void]$out.Append("</package>")
|
||||
|
||||
$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 '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
M '<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">'
|
||||
M "`t<XDTOPackage uuid=`"$uuid`">"
|
||||
M "`t`t<Properties>"
|
||||
M "`t`t`t<Name>$(EscText $Name)</Name>"
|
||||
M "`t`t`t<Synonym>"
|
||||
foreach ($s in $synItems) {
|
||||
M "`t`t`t`t<v8:item>"
|
||||
M "`t`t`t`t`t<v8:lang>$(EscText $s.Lang)</v8:lang>"
|
||||
M "`t`t`t`t`t<v8:content>$(EscText $s.Content)</v8:content>"
|
||||
M "`t`t`t`t</v8:item>"
|
||||
}
|
||||
M "`t`t`t</Synonym>"
|
||||
if ($Comment) { M "`t`t`t<Comment>$(EscText $Comment)</Comment>" } else { M "`t`t`t<Comment/>" }
|
||||
M "`t`t`t<Namespace>$(EscText $targetNs)</Namespace>"
|
||||
M "`t`t</Properties>"
|
||||
M "`t</XDTOPackage>"
|
||||
[void]$md.Append("</MetaDataObject>")
|
||||
|
||||
[System.IO.File]::WriteAllText($mdFile, $md.ToString(), $encBom)
|
||||
|
||||
# --- Register in Configuration.xml ---
|
||||
|
||||
$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"
|
||||
switch ($regResult) {
|
||||
"added" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> добавлен в ChildObjects" }
|
||||
"already" { Write-Host " Configuration.xml: <XDTOPackage>$Name</XDTOPackage> уже зарегистрирован" }
|
||||
"no-config" { Write-Host " Configuration.xml не найден — регистрация пропущена" }
|
||||
}
|
||||
@@ -0,0 +1,780 @@
|
||||
# xdto-compile v1.0 — 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
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from lxml import etree
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
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"
|
||||
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument("-XsdPath", "-Path", default="")
|
||||
parser.add_argument("-Xsd", default="")
|
||||
parser.add_argument("-OutputDir", required=True)
|
||||
parser.add_argument("-Name", default="")
|
||||
parser.add_argument("-Synonym", default="")
|
||||
parser.add_argument("-Comment", default="")
|
||||
parser.add_argument("-Force", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
# ── support guard (Ext/ParentConfigurations.bin) ─────────────
|
||||
# См. docs/1c-support-state-spec.md. Блокирует правку объектов поставщика
|
||||
# «на замке». Триггер — наличие bin; реакция из .v8-project.json
|
||||
# editingAllowedCheck (deny|warn|off, по умолчанию deny).
|
||||
|
||||
|
||||
def find_v8_project(start_dir):
|
||||
d = os.path.abspath(start_dir)
|
||||
for _ in range(20):
|
||||
pj = os.path.join(d, ".v8-project.json")
|
||||
if os.path.exists(pj):
|
||||
return pj
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
return None
|
||||
|
||||
|
||||
def get_edit_mode(cfg_dir):
|
||||
try:
|
||||
pj = find_v8_project(cfg_dir)
|
||||
if pj:
|
||||
with open(pj, encoding="utf-8-sig") as f:
|
||||
cfg = json.load(f)
|
||||
return str(cfg.get("editingAllowedCheck") or "deny")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return "deny"
|
||||
|
||||
|
||||
def is_external_object_root(xml_path):
|
||||
try:
|
||||
root = etree.parse(xml_path).getroot()
|
||||
for el in root:
|
||||
if isinstance(el.tag, str):
|
||||
return etree.QName(el).localname in ("ExternalDataProcessor", "ExternalReport")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def assert_edit_allowed(target_path):
|
||||
d = os.path.abspath(target_path)
|
||||
for _ in range(20):
|
||||
# Автономный объект (внешняя обработка/отчёт) — граница климба
|
||||
try:
|
||||
for f in os.listdir(d):
|
||||
if f.endswith(".xml") and is_external_object_root(os.path.join(d, f)):
|
||||
return
|
||||
except OSError:
|
||||
pass
|
||||
cfg_xml = os.path.join(d, "Configuration.xml")
|
||||
support_bin = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
||||
if os.path.exists(cfg_xml):
|
||||
if os.path.exists(support_bin):
|
||||
mode = get_edit_mode(d)
|
||||
if mode == "off":
|
||||
return
|
||||
msg = ("Конфигурация находится на поддержке (Ext/ParentConfigurations.bin). "
|
||||
"Правка может быть запрещена.")
|
||||
if mode == "warn":
|
||||
print(f"WARNING: {msg}", file=sys.stderr)
|
||||
return
|
||||
print(f"{msg} Снимите с поддержки (/support-edit) или задайте "
|
||||
"editingAllowedCheck в .v8-project.json.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return
|
||||
parent = os.path.dirname(d)
|
||||
if parent == d:
|
||||
break
|
||||
d = parent
|
||||
|
||||
|
||||
# ── load the schema ──────────────────────────────────────────
|
||||
|
||||
if args.Xsd:
|
||||
xsd_bytes = args.Xsd.encode("utf-8")
|
||||
default_name = "Package"
|
||||
elif args.XsdPath:
|
||||
if not os.path.isfile(args.XsdPath):
|
||||
print(f"Файл XSD не найден: {args.XsdPath}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
with open(args.XsdPath, "rb") as f:
|
||||
xsd_bytes = f.read()
|
||||
default_name = os.path.splitext(os.path.basename(args.XsdPath))[0]
|
||||
else:
|
||||
print("Укажите -XsdPath или -Xsd", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
schema = etree.fromstring(xsd_bytes)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"Не удалось разобрать XSD: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def local(el):
|
||||
return etree.QName(el).localname
|
||||
|
||||
|
||||
if local(schema) != "schema" or etree.QName(schema).namespace != XS_NS:
|
||||
print(f"Ожидался корневой <xs:schema> в пространстве имён {XS_NS}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
target_ns = schema.get("targetNamespace") or ""
|
||||
|
||||
# ── emit-tree primitives ─────────────────────────────────────
|
||||
|
||||
|
||||
class Node:
|
||||
__slots__ = ("tag", "attrs", "children", "text", "prefix", "declare_ns")
|
||||
|
||||
def __init__(self, tag):
|
||||
self.tag = tag
|
||||
self.attrs = [] # список dict: name, value | (ns, local) | list
|
||||
self.children = []
|
||||
self.text = None
|
||||
self.prefix = None
|
||||
self.declare_ns = None
|
||||
|
||||
|
||||
def add_attr(node, name, value):
|
||||
if value is None:
|
||||
return
|
||||
node.attrs.append({"name": name, "value": str(value)})
|
||||
|
||||
|
||||
def add_qattr(node, name, ns, loc):
|
||||
if loc is None:
|
||||
return
|
||||
node.attrs.append({"name": name, "ns": ns, "local": loc})
|
||||
|
||||
|
||||
def add_qlist_attr(node, name, pairs, clark):
|
||||
if not pairs:
|
||||
return
|
||||
node.attrs.append({"name": name, "list": pairs, "clark": clark})
|
||||
|
||||
|
||||
# Канонический порядок атрибутов — топологическая сортировка по корпусу 8.3.24
|
||||
# (acc + erp, 760 пакетов), см. 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"],
|
||||
}
|
||||
|
||||
|
||||
def sort_attrs(node):
|
||||
order = ATTR_ORDER.get(node.tag)
|
||||
if not order:
|
||||
return node.attrs
|
||||
res = []
|
||||
for n in order:
|
||||
res.extend(a for a in node.attrs if a["name"] == n)
|
||||
res.extend(a for a in node.attrs if a["name"] not in order)
|
||||
return res
|
||||
|
||||
|
||||
def esc(s):
|
||||
if s is None:
|
||||
return ""
|
||||
return str(s).replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
||||
|
||||
|
||||
def esc_text(s):
|
||||
if s is None:
|
||||
return ""
|
||||
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
# ── serializer with the dNpM prefix scheme ───────────────────
|
||||
|
||||
out = []
|
||||
|
||||
|
||||
def serialize_node(node, depth, inherited):
|
||||
indent = "\t" * (depth - 1)
|
||||
attrs_sorted = sort_attrs(node)
|
||||
|
||||
# Объявляем здесь только те ns, которых ещё нет в области видимости:
|
||||
# сериализатор платформы объявляет префикс на первом нуждающемся узле,
|
||||
# а потомки переиспользуют — отсюда d2p1 у property внутри objectType.
|
||||
local_ns = []
|
||||
|
||||
def need_prefix(ns):
|
||||
if not ns or ns in (XS_NS, XSI_NS):
|
||||
return
|
||||
if ns in inherited:
|
||||
return
|
||||
if ns not in local_ns:
|
||||
local_ns.append(ns)
|
||||
|
||||
for a in attrs_sorted:
|
||||
if "list" in a:
|
||||
# Нотация Кларка несёт ns в значении и префикса не требует
|
||||
if not a["clark"]:
|
||||
for p in a["list"]:
|
||||
need_prefix(p[0])
|
||||
elif a.get("ns"):
|
||||
need_prefix(a["ns"])
|
||||
|
||||
has_qualified = any(a["name"] == "qualified" for a in attrs_sorted)
|
||||
if has_qualified:
|
||||
need_prefix(XDTO_NS)
|
||||
if node.declare_ns:
|
||||
need_prefix(node.declare_ns)
|
||||
|
||||
prefix_of = dict(inherited)
|
||||
ns_decls = ""
|
||||
for i, ns in enumerate(local_ns):
|
||||
# Осмысленный префикс из исходника (зеркало xdto:prefix) имеет приоритет
|
||||
px = node.prefix if (i == 0 and node.prefix) else f"d{depth}p{i + 1}"
|
||||
prefix_of[ns] = px
|
||||
ns_decls += f' xmlns:{px}="{esc(ns)}"'
|
||||
|
||||
def qval(ns, loc):
|
||||
if not ns:
|
||||
return loc
|
||||
if ns == XS_NS:
|
||||
return f"xs:{loc}"
|
||||
if ns == XSI_NS:
|
||||
return f"xsi:{loc}"
|
||||
return f"{prefix_of[ns]}:{loc}"
|
||||
|
||||
attr_text = ""
|
||||
for a in attrs_sorted:
|
||||
if "list" in a:
|
||||
vals = []
|
||||
for ns, loc in a["list"]:
|
||||
vals.append((f"{{{ns}}}{loc}" if ns else loc) if a["clark"] else qval(ns, loc))
|
||||
attr_text += f' {a["name"]}="{esc(" ".join(vals))}"'
|
||||
elif a.get("ns") or a.get("local"):
|
||||
attr_text += f' {a["name"]}="{esc(qval(a.get("ns"), a["local"]))}"'
|
||||
elif a["name"] == "qualified":
|
||||
attr_text += f' {prefix_of[XDTO_NS]}:qualified="{esc(a["value"])}"'
|
||||
else:
|
||||
attr_text += f' {a["name"]}="{esc(a["value"])}"'
|
||||
|
||||
tag_name = f"{prefix_of[XDTO_NS]}:{node.tag}" if has_qualified else node.tag
|
||||
|
||||
has_children = bool(node.children)
|
||||
# Пустое значение пишется самозакрывающимся тегом: <enumeration/>
|
||||
has_text = node.text is not None and node.text != ""
|
||||
|
||||
if not has_children and not has_text:
|
||||
out.append(f"{indent}<{tag_name}{ns_decls}{attr_text}/>\r\n")
|
||||
return
|
||||
if has_text and not has_children:
|
||||
out.append(f"{indent}<{tag_name}{ns_decls}{attr_text}>{esc_text(node.text)}</{tag_name}>\r\n")
|
||||
return
|
||||
out.append(f"{indent}<{tag_name}{ns_decls}{attr_text}>\r\n")
|
||||
for c in node.children:
|
||||
serialize_node(c, depth + 1, prefix_of)
|
||||
out.append(f"{indent}</{tag_name}>\r\n")
|
||||
|
||||
|
||||
# ── XSD reading helpers ──────────────────────────────────────
|
||||
|
||||
def MA(el, name):
|
||||
# xdto: mirror attribute — литеральное значение для Package.bin.
|
||||
# Ищем по namespace, а не по строке префикса.
|
||||
return el.get(f"{{{XDTO_NS}}}{name}")
|
||||
|
||||
|
||||
def xchildren(el, name):
|
||||
return [c for c in el if isinstance(c.tag, str)
|
||||
and etree.QName(c).namespace == XS_NS and local(c) == name]
|
||||
|
||||
|
||||
def xfirst(el, name):
|
||||
r = xchildren(el, name)
|
||||
return r[0] if r else None
|
||||
|
||||
|
||||
def split_qname(el, qname):
|
||||
if not qname:
|
||||
return None
|
||||
parts = qname.split(":")
|
||||
if len(parts) == 2:
|
||||
ns = el.nsmap.get(parts[0])
|
||||
loc = parts[1]
|
||||
else:
|
||||
# Прощающий ввод: голое имя типа — тип целевого пространства имён
|
||||
ns = el.nsmap.get(None) or target_ns
|
||||
loc = parts[0]
|
||||
return (ns, loc)
|
||||
|
||||
|
||||
def split_qname_list(el, lst):
|
||||
if not lst:
|
||||
return []
|
||||
return [split_qname(el, q) for q in lst.split() if q]
|
||||
|
||||
|
||||
FACETS = ["length", "minLength", "maxLength", "totalDigits", "fractionDigits",
|
||||
"minInclusive", "maxInclusive", "minExclusive", "maxExclusive", "whiteSpace"]
|
||||
|
||||
|
||||
# ── simpleType -> valueType / typeDef(ValueType) ─────────────
|
||||
|
||||
def fill_simple_type(node, st):
|
||||
restriction = xfirst(st, "restriction")
|
||||
lst = xfirst(st, "list")
|
||||
union = xfirst(st, "union")
|
||||
|
||||
if lst is not None:
|
||||
it = split_qname(lst, lst.get("itemType"))
|
||||
mv = MA(lst, "variety")
|
||||
add_attr(node, "variety", mv if mv is not None else "List")
|
||||
if it:
|
||||
add_qattr(node, "itemType", it[0], it[1])
|
||||
return
|
||||
if union is not None:
|
||||
mv = MA(union, "variety")
|
||||
set_attr_value(node, "variety", mv if mv is not None else "Union")
|
||||
members = split_qname_list(union, union.get("memberTypes"))
|
||||
# По умолчанию нотация Кларка — так записано 125 из 135 memberTypes корпуса
|
||||
use_clark = MA(union, "memberTypesForm") != "prefixed"
|
||||
if members:
|
||||
add_qlist_attr(node, "memberTypes", members, use_clark)
|
||||
node.declare_ns = MA(union, "declareNs")
|
||||
for anon in xchildren(union, "simpleType"):
|
||||
# typeDef в контексте простого типа xsi:type не несёт (40 узлов корпуса)
|
||||
td = Node("typeDef")
|
||||
fill_simple_type(td, anon)
|
||||
node.children.append(td)
|
||||
return
|
||||
if restriction is not None:
|
||||
b = split_qname(restriction, restriction.get("base"))
|
||||
if b:
|
||||
add_qattr(node, "base", b[0], b[1])
|
||||
mv = MA(restriction, "variety")
|
||||
if mv is not None:
|
||||
add_attr(node, "variety", mv)
|
||||
# Анонимный базовый тип внутри xs:restriction — typeDef без xsi:type
|
||||
anon_base = xfirst(restriction, "simpleType")
|
||||
if anon_base is not None:
|
||||
td = Node("typeDef")
|
||||
fill_simple_type(td, anon_base)
|
||||
node.children.append(td)
|
||||
for f in FACETS:
|
||||
for fe in xchildren(restriction, f):
|
||||
add_attr(node, f, fe.get("value"))
|
||||
for pe in xchildren(restriction, "pattern"):
|
||||
pn = Node("pattern")
|
||||
pn.text = pe.get("value")
|
||||
node.children.append(pn)
|
||||
for en in xchildren(restriction, "enumeration"):
|
||||
enode = Node("enumeration")
|
||||
mt = MA(en, "type")
|
||||
if mt is not None:
|
||||
q = split_qname(en, mt)
|
||||
add_qattr(enode, "xsi:type", q[0], q[1])
|
||||
enode.text = en.get("value")
|
||||
node.children.append(enode)
|
||||
|
||||
|
||||
def set_attr_value(node, name, value):
|
||||
for a in node.attrs:
|
||||
if a["name"] == name:
|
||||
a["value"] = value
|
||||
return
|
||||
add_attr(node, name, value)
|
||||
|
||||
|
||||
def get_prop_key(p):
|
||||
for a in p.attrs:
|
||||
if a["name"] == "name":
|
||||
return a.get("value")
|
||||
for a in p.attrs:
|
||||
if a["name"] == "ref":
|
||||
return "@" + a["local"]
|
||||
return None
|
||||
|
||||
|
||||
def reorder_properties(node, names):
|
||||
props = [c for c in node.children if c.tag == "property"]
|
||||
if len(props) < 2:
|
||||
return
|
||||
by_key = {}
|
||||
for p in props:
|
||||
k = get_prop_key(p)
|
||||
if k is not None and k not in by_key:
|
||||
by_key[k] = p
|
||||
ordered = []
|
||||
for n in names:
|
||||
if n in by_key:
|
||||
ordered.append(by_key.pop(n))
|
||||
for p in props:
|
||||
if p not in ordered:
|
||||
ordered.append(p)
|
||||
others = [c for c in node.children if c.tag != "property"]
|
||||
node.children = ordered + others
|
||||
|
||||
|
||||
# ── element / attribute -> property ──────────────────────────
|
||||
|
||||
def build_property(el, is_attribute):
|
||||
p = Node("property")
|
||||
|
||||
xsd_name = el.get("name")
|
||||
mirror_name = MA(el, "name")
|
||||
if mirror_name is not None:
|
||||
add_attr(p, "name", mirror_name)
|
||||
local_name = xsd_name
|
||||
else:
|
||||
add_attr(p, "name", xsd_name)
|
||||
local_name = None
|
||||
|
||||
ref_q = split_qname(el, el.get("ref"))
|
||||
if ref_q:
|
||||
add_qattr(p, "ref", ref_q[0], ref_q[1])
|
||||
|
||||
type_q = split_qname(el, el.get("type"))
|
||||
if type_q:
|
||||
add_qattr(p, "type", type_q[0], type_q[1])
|
||||
|
||||
if is_attribute:
|
||||
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", el.get("minOccurs"))
|
||||
max_occ = el.get("maxOccurs")
|
||||
if max_occ is not None:
|
||||
add_attr(p, "upperBound", "-1" if max_occ == "unbounded" else max_occ)
|
||||
add_attr(p, "nillable", el.get("nillable"))
|
||||
|
||||
add_attr(p, "fixed", el.get("fixed"))
|
||||
add_attr(p, "default", el.get("default"))
|
||||
|
||||
if is_attribute:
|
||||
add_attr(p, "form", "Attribute")
|
||||
else:
|
||||
mf = MA(el, "form")
|
||||
if mf is not None:
|
||||
add_attr(p, "form", mf)
|
||||
add_attr(p, "localName", local_name)
|
||||
add_attr(p, "qualified", MA(el, "qualified"))
|
||||
p.prefix = MA(el, "prefix")
|
||||
|
||||
anon_simple = xfirst(el, "simpleType")
|
||||
anon_complex = xfirst(el, "complexType")
|
||||
if anon_simple is not None:
|
||||
td = Node("typeDef")
|
||||
add_attr(td, "xsi:type", "ValueType")
|
||||
fill_simple_type(td, anon_simple)
|
||||
p.children.append(td)
|
||||
elif anon_complex is not None:
|
||||
td = Node("typeDef")
|
||||
add_attr(td, "xsi:type", "ObjectType")
|
||||
fill_complex_type(td, anon_complex)
|
||||
p.children.append(td)
|
||||
return p
|
||||
|
||||
|
||||
# ── complexType -> objectType / typeDef(ObjectType) ──────────
|
||||
|
||||
def set_type_flags(node, ct, is_open, choice):
|
||||
m_open = MA(ct, "open")
|
||||
if m_open is not None:
|
||||
add_attr(node, "open", m_open)
|
||||
elif is_open:
|
||||
add_attr(node, "open", "true")
|
||||
|
||||
m_ordered = MA(ct, "ordered")
|
||||
if m_ordered is not None:
|
||||
add_attr(node, "ordered", m_ordered)
|
||||
elif choice is not None:
|
||||
add_attr(node, "ordered", "false")
|
||||
|
||||
m_seq = MA(ct, "sequenced")
|
||||
if m_seq is not None:
|
||||
add_attr(node, "sequenced", m_seq)
|
||||
|
||||
m_abstract = MA(ct, "abstract")
|
||||
if m_abstract is not None:
|
||||
add_attr(node, "abstract", m_abstract)
|
||||
elif ct.get("abstract") == "true":
|
||||
add_attr(node, "abstract", "true")
|
||||
|
||||
m_mixed = MA(ct, "mixed")
|
||||
if m_mixed is not None:
|
||||
add_attr(node, "mixed", m_mixed)
|
||||
elif ct.get("mixed") == "true":
|
||||
add_attr(node, "mixed", "true")
|
||||
|
||||
|
||||
def fill_complex_type(node, ct):
|
||||
body = ct
|
||||
content = xfirst(ct, "complexContent")
|
||||
if content is not None:
|
||||
ext = xfirst(content, "extension")
|
||||
if ext is not None:
|
||||
b = split_qname(ext, ext.get("base"))
|
||||
if b:
|
||||
add_qattr(node, "base", b[0], b[1])
|
||||
body = ext
|
||||
|
||||
# xs:simpleContent -> свойство "Text", хранящее значение самого элемента
|
||||
simple = xfirst(ct, "simpleContent")
|
||||
if simple is not None:
|
||||
ext = xfirst(simple, "extension")
|
||||
if ext is not None:
|
||||
for a in xchildren(ext, "attribute"):
|
||||
node.children.append(build_property(a, True))
|
||||
tp = Node("property")
|
||||
t_name = MA(ext, "textName")
|
||||
add_attr(tp, "name", t_name if t_name is not None else "__content")
|
||||
b = split_qname(ext, ext.get("base"))
|
||||
if b:
|
||||
add_qattr(tp, "type", b[0], b[1])
|
||||
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")
|
||||
node.children.append(tp)
|
||||
# xs:simpleContent не отменяет флаги самого xs:complexType
|
||||
set_type_flags(node, ct, False, None)
|
||||
return
|
||||
|
||||
seq = xfirst(body, "sequence")
|
||||
cho = xfirst(body, "choice")
|
||||
particle = seq if seq is not None else cho
|
||||
is_open = False
|
||||
|
||||
# Порядок в XDTO: сначала form="Attribute", потом остальные (96.5% типов корпуса)
|
||||
elem_props = []
|
||||
if particle is not None:
|
||||
for c in particle:
|
||||
if not isinstance(c.tag, str) or etree.QName(c).namespace != XS_NS:
|
||||
continue
|
||||
ln = local(c)
|
||||
if ln == "element":
|
||||
elem_props.append(build_property(c, False))
|
||||
elif ln == "any":
|
||||
is_open = True
|
||||
for a in xchildren(body, "attribute"):
|
||||
node.children.append(build_property(a, True))
|
||||
node.children.extend(elem_props)
|
||||
if xchildren(body, "anyAttribute"):
|
||||
is_open = True
|
||||
|
||||
m_order = MA(ct, "order")
|
||||
if m_order is not None:
|
||||
reorder_properties(node, m_order.split("|"))
|
||||
|
||||
set_type_flags(node, ct, is_open, cho)
|
||||
|
||||
|
||||
# ── build the package tree ───────────────────────────────────
|
||||
|
||||
pkg_node = Node("package")
|
||||
add_attr(pkg_node, "targetNamespace", target_ns)
|
||||
|
||||
efq_mirror = MA(schema, "elementFormQualified")
|
||||
afq_mirror = MA(schema, "attributeFormQualified")
|
||||
efd = schema.get("elementFormDefault")
|
||||
afd = schema.get("attributeFormDefault")
|
||||
if efq_mirror is not None:
|
||||
add_attr(pkg_node, "elementFormQualified", efq_mirror)
|
||||
elif efd is not None:
|
||||
add_attr(pkg_node, "elementFormQualified", "true" if efd == "qualified" else "false")
|
||||
if afq_mirror is not None:
|
||||
add_attr(pkg_node, "attributeFormQualified", afq_mirror)
|
||||
elif afd is not None:
|
||||
add_attr(pkg_node, "attributeFormQualified", "true" if afd == "qualified" else "false")
|
||||
|
||||
meta_name = meta_comment = None
|
||||
meta_synonym = []
|
||||
ann = xfirst(schema, "annotation")
|
||||
if ann is not None:
|
||||
appinfo = xfirst(ann, "appinfo")
|
||||
if appinfo is not None:
|
||||
for pk in appinfo:
|
||||
if not isinstance(pk.tag, str) or etree.QName(pk).namespace != XDTO_NS:
|
||||
continue
|
||||
for f in pk:
|
||||
if not isinstance(f.tag, str):
|
||||
continue
|
||||
ln = local(f)
|
||||
if ln == "name":
|
||||
meta_name = f.text or ""
|
||||
elif ln == "comment":
|
||||
meta_comment = f.text or ""
|
||||
elif ln == "synonym":
|
||||
meta_synonym.append({"Lang": f.get("lang") or "", "Content": f.text or ""})
|
||||
|
||||
for node in schema:
|
||||
if not isinstance(node.tag, str) or etree.QName(node).namespace != XS_NS:
|
||||
continue
|
||||
ln = local(node)
|
||||
if ln == "import":
|
||||
n = Node("import")
|
||||
add_attr(n, "namespace", node.get("namespace"))
|
||||
pkg_node.children.append(n)
|
||||
elif ln in ("annotation", "include"):
|
||||
continue
|
||||
elif ln == "element":
|
||||
pkg_node.children.append(build_property(node, False))
|
||||
elif ln == "attribute":
|
||||
pkg_node.children.append(build_property(node, True))
|
||||
elif ln == "simpleType":
|
||||
n = Node("valueType")
|
||||
add_attr(n, "name", node.get("name"))
|
||||
fill_simple_type(n, node)
|
||||
pkg_node.children.append(n)
|
||||
elif ln == "complexType":
|
||||
n = Node("objectType")
|
||||
add_attr(n, "name", node.get("name"))
|
||||
fill_complex_type(n, node)
|
||||
pkg_node.children.append(n)
|
||||
|
||||
# ── serialize Package.bin ────────────────────────────────────
|
||||
|
||||
# Модель XDTO требует строгой последовательности элементов верхнего уровня:
|
||||
# import → property → valueType → objectType. Порядок объявлений в XSD произвольный,
|
||||
# поэтому пересортировываем — иначе платформа отвергает пакет с «Ошибка преобразования
|
||||
# данных XDTO». Все 760 пакетов корпуса этому порядку удовлетворяют.
|
||||
TOP_ORDER = ["import", "property", "valueType", "objectType"]
|
||||
pkg_node.children = (
|
||||
[c for t in TOP_ORDER for c in pkg_node.children if c.tag == t]
|
||||
+ [c for c in pkg_node.children if c.tag not in TOP_ORDER]
|
||||
)
|
||||
|
||||
root_attr_text = "".join(f' {a["name"]}="{esc(a["value"])}"' for a in sort_attrs(pkg_node))
|
||||
out.append(f'<package xmlns="{XDTO_NS}" xmlns:xs="{XS_NS}" xmlns:xsi="{XSI_NS}"{root_attr_text}>\r\n')
|
||||
for c in pkg_node.children:
|
||||
serialize_node(c, 2, {})
|
||||
out.append("</package>")
|
||||
|
||||
bin_text = "".join(out)
|
||||
|
||||
# ── resolve the package name ─────────────────────────────────
|
||||
|
||||
name = args.Name or meta_name or default_name
|
||||
name = re.sub(r"[^\wЀ-ӿ]", "_", name, flags=re.UNICODE)
|
||||
if re.match(r"^\d", name):
|
||||
name = "_" + name
|
||||
|
||||
assert_edit_allowed(args.OutputDir)
|
||||
|
||||
pkg_root = os.path.join(args.OutputDir, "XDTOPackages")
|
||||
pkg_dir = os.path.join(pkg_root, name)
|
||||
ext_dir = os.path.join(pkg_dir, "Ext")
|
||||
md_file = os.path.join(pkg_root, name + ".xml")
|
||||
bin_file = os.path.join(ext_dir, "Package.bin")
|
||||
|
||||
if os.path.exists(bin_file) and not args.Force:
|
||||
print(f"Пакет уже существует: {bin_file}. Используйте -Force для перезаписи.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
os.makedirs(ext_dir, exist_ok=True)
|
||||
|
||||
with open(bin_file, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf" + bin_text.encode("utf-8"))
|
||||
|
||||
# ── metadata object file ─────────────────────────────────────
|
||||
|
||||
if not args.Synonym and meta_synonym:
|
||||
syn_items = meta_synonym
|
||||
elif args.Synonym:
|
||||
syn_items = [{"Lang": "ru", "Content": args.Synonym}]
|
||||
else:
|
||||
syn_items = [{"Lang": "ru", "Content": name}]
|
||||
comment = args.Comment or meta_comment or ""
|
||||
|
||||
md_lines = [
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" '
|
||||
'xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" '
|
||||
'xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" '
|
||||
'xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" '
|
||||
'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" '
|
||||
'xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" '
|
||||
'xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" '
|
||||
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
||||
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">',
|
||||
f'\t<XDTOPackage uuid="{uuid.uuid4()}">',
|
||||
"\t\t<Properties>",
|
||||
f"\t\t\t<Name>{esc_text(name)}</Name>",
|
||||
"\t\t\t<Synonym>",
|
||||
]
|
||||
for s in syn_items:
|
||||
md_lines += [
|
||||
"\t\t\t\t<v8:item>",
|
||||
f'\t\t\t\t\t<v8:lang>{esc_text(s["Lang"])}</v8:lang>',
|
||||
f'\t\t\t\t\t<v8:content>{esc_text(s["Content"])}</v8:content>',
|
||||
"\t\t\t\t</v8:item>",
|
||||
]
|
||||
md_lines.append("\t\t\t</Synonym>")
|
||||
md_lines.append(f"\t\t\t<Comment>{esc_text(comment)}</Comment>" if comment else "\t\t\t<Comment/>")
|
||||
md_lines.append(f"\t\t\t<Namespace>{esc_text(target_ns)}</Namespace>")
|
||||
md_lines += ["\t\t</Properties>", "\t</XDTOPackage>", "</MetaDataObject>"]
|
||||
|
||||
with open(md_file, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf" + "\r\n".join(md_lines).encode("utf-8"))
|
||||
|
||||
# ── register in Configuration.xml ────────────────────────────
|
||||
|
||||
config_xml = os.path.join(args.OutputDir, "Configuration.xml")
|
||||
reg_result = "no-config"
|
||||
if os.path.exists(config_xml):
|
||||
with open(config_xml, "rb") as f:
|
||||
raw = f.read()
|
||||
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
||||
cfg_doc = etree.parse(config_xml)
|
||||
child_objects = cfg_doc.find(f".//{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects")
|
||||
if child_objects is not None:
|
||||
existing = child_objects.findall(f"{{{MD_NS}}}XDTOPackage")
|
||||
if any((e.text or "") == name for e in existing):
|
||||
reg_result = "already"
|
||||
else:
|
||||
new_elem = etree.SubElement(child_objects, f"{{{MD_NS}}}XDTOPackage")
|
||||
new_elem.text = name
|
||||
if existing:
|
||||
last = existing[-1]
|
||||
new_elem.tail = last.tail
|
||||
child_objects.remove(new_elem)
|
||||
last.addnext(new_elem)
|
||||
else:
|
||||
new_elem.tail = child_objects.text
|
||||
data = etree.tostring(cfg_doc, xml_declaration=True, encoding="UTF-8")
|
||||
if had_bom:
|
||||
data = b"\xef\xbb\xbf" + data
|
||||
with open(config_xml, "wb") as f:
|
||||
f.write(data)
|
||||
reg_result = "added"
|
||||
|
||||
type_count = sum(1 for c in pkg_node.children if c.tag in ("objectType", "valueType"))
|
||||
print(f"✓ Пакет XDTO собран: {name}")
|
||||
print(f" Namespace: {target_ns}")
|
||||
print(f" Типов: {type_count}")
|
||||
print(f" Файлы: XDTOPackages/{name}.xml, XDTOPackages/{name}/Ext/Package.bin")
|
||||
if reg_result == "added":
|
||||
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> добавлен в ChildObjects")
|
||||
elif reg_result == "already":
|
||||
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> уже зарегистрирован")
|
||||
else:
|
||||
print(" Configuration.xml не найден — регистрация пропущена")
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: xdto-decompile
|
||||
description: Выгрузка пакета XDTO 1С в XML-схему (XSD). Используй когда нужно прочитать или отредактировать существующий пакет XDTO, отдать схему контрагенту, перенести пакет между конфигурациями
|
||||
argument-hint: <PackagePath> [-OutFile <файл.xsd>]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /xdto-decompile — Выгрузка пакета XDTO в XML-схему
|
||||
|
||||
Превращает `Ext/Package.bin` в обычную XML-схему. Заменяет чтение модели XDTO с её
|
||||
инвертированной кратностью, фасетами-атрибутами и локальными объявлениями префиксов
|
||||
`dNpM` на каждой ссылке.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `PackagePath` | Каталог пакета, путь к `Ext/Package.bin` или к `<Имя>.xml` объекта метаданных |
|
||||
| `OutFile` | Записать схему в файл (UTF-8 BOM). Без него — вывод в stdout |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-decompile.ps1" -PackagePath "<путь>"
|
||||
```
|
||||
|
||||
Примеры:
|
||||
```powershell
|
||||
... -PackagePath C:\cfsrc\erp\XDTOPackages\ClientBankExchange
|
||||
... -PackagePath C:\cfsrc\erp\XDTOPackages\ClientBankExchange -OutFile bank.xsd
|
||||
```
|
||||
|
||||
## Round-trip
|
||||
|
||||
`/xdto-decompile` → правка XSD → `/xdto-compile -Force` возвращает исходный `Package.bin`
|
||||
байт-в-байт. Инвариант проверен на 760 пакетах выгрузок Бухгалтерии и ERP.
|
||||
|
||||
Это основной способ править существующий пакет: точечных операций не требуется, схема
|
||||
целиком читаема и редактируема.
|
||||
|
||||
Свойства объекта метаданных (`Name`, `Synonym`, `Comment`) выгружаются в
|
||||
`xs:annotation/xs:appinfo`, поэтому при обратной сборке не теряются. `Namespace` не
|
||||
дублируется — его несёт `targetNamespace`.
|
||||
|
||||
## Отличия от экспорта XML-схемы в Конфигураторе
|
||||
|
||||
Штатный экспорт Конфигуратора теряет данные: `nillable` у свойств-атрибутов
|
||||
(спецификация XSD не допускает его у атрибутов), а для пакетов с
|
||||
`elementFormQualified="false"` выдаёт невалидную схему — `form="qualified"` на
|
||||
глобальных объявлениях.
|
||||
|
||||
Навык пишет валидную схему, а то, что XSD выразить не может, выносит в атрибуты
|
||||
пространства имён модели XDTO:
|
||||
|
||||
```xml
|
||||
<xs:attribute name="Представление" type="xs:string" xdto:nillable="true"/>
|
||||
```
|
||||
|
||||
Такие атрибуты валидаторы игнорируют, а `/xdto-compile` читает обратно. Схему без
|
||||
потерь можно отдавать контрагенту как есть.
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/xdto-decompile <путь> — схема в stdout
|
||||
/xdto-decompile <путь> -OutFile schema.xsd — в файл
|
||||
```
|
||||
@@ -0,0 +1,603 @@
|
||||
# xdto-decompile v1.0 — Convert 1C XDTO package to XML Schema (XSD)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[Alias('Path')]
|
||||
[string]$PackagePath,
|
||||
[string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[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"
|
||||
|
||||
# --- Resolve paths: accept package dir, Package.bin, or metadata .xml ---
|
||||
|
||||
function Resolve-PackagePaths([string]$p) {
|
||||
$binPath = $null
|
||||
$mdPath = $null
|
||||
|
||||
if (Test-Path $p -PathType Leaf) {
|
||||
if ([System.IO.Path]::GetFileName($p) -eq "Package.bin") {
|
||||
$binPath = $p
|
||||
# <Name>/Ext/Package.bin -> <Name>.xml
|
||||
$extDir = [System.IO.Path]::GetDirectoryName($p)
|
||||
$pkgDir = [System.IO.Path]::GetDirectoryName($extDir)
|
||||
$cand = "$pkgDir.xml"
|
||||
if (Test-Path $cand) { $mdPath = $cand }
|
||||
} elseif ($p.EndsWith(".xml")) {
|
||||
$mdPath = $p
|
||||
$pkgDir = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($p), [System.IO.Path]::GetFileNameWithoutExtension($p))
|
||||
$cand = Join-Path (Join-Path $pkgDir "Ext") "Package.bin"
|
||||
if (Test-Path $cand) { $binPath = $cand }
|
||||
}
|
||||
} elseif (Test-Path $p -PathType Container) {
|
||||
$cand = Join-Path (Join-Path $p "Ext") "Package.bin"
|
||||
if (Test-Path $cand) {
|
||||
$binPath = $cand
|
||||
$mdCand = "$($p.TrimEnd('\','/')).xml"
|
||||
if (Test-Path $mdCand) { $mdPath = $mdCand }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $binPath) { throw "Не найден Ext/Package.bin для пути: $p" }
|
||||
return @{ Bin = $binPath; Md = $mdPath }
|
||||
}
|
||||
|
||||
$paths = Resolve-PackagePaths $PackagePath
|
||||
|
||||
# --- Load package model ---
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
$doc.PreserveWhitespace = $false
|
||||
$doc.Load($paths.Bin)
|
||||
$pkg = $doc.DocumentElement
|
||||
if ($pkg.get_LocalName() -ne "package") { throw "Ожидался корневой <package>, получен <$($pkg.get_LocalName())>" }
|
||||
|
||||
$targetNs = $pkg.GetAttribute("targetNamespace")
|
||||
|
||||
# --- Namespace -> prefix map for the emitted schema ---
|
||||
|
||||
$nsPrefix = @{}
|
||||
$nsPrefix[$XS_NS] = "xs"
|
||||
if ($targetNs) { $nsPrefix[$targetNs] = "tns" }
|
||||
|
||||
$imports = @()
|
||||
foreach ($imp in $pkg.ChildNodes) {
|
||||
if ($imp.NodeType -ne [System.Xml.XmlNodeType]::Element -or $imp.get_LocalName() -ne "import") { continue }
|
||||
$ns = $imp.GetAttribute("namespace")
|
||||
$imports += $ns
|
||||
if (-not $nsPrefix.ContainsKey($ns)) { $nsPrefix[$ns] = "ns" + ($nsPrefix.Count) }
|
||||
}
|
||||
|
||||
# Any foreign namespace referenced but not imported still needs a prefix
|
||||
function Register-Ns([string]$ns) {
|
||||
if (-not $ns) { return }
|
||||
if (-not $nsPrefix.ContainsKey($ns)) { $nsPrefix[$ns] = "ns" + ($nsPrefix.Count) }
|
||||
}
|
||||
|
||||
# --- Output buffer ---
|
||||
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
function X([string]$line) { [void]$sb.Append($line); [void]$sb.Append("`r`n") }
|
||||
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(">", ">")
|
||||
}
|
||||
|
||||
# --- QName conversion: bin prefix -> schema prefix ---
|
||||
|
||||
function Convert-QName([System.Xml.XmlElement]$el, [string]$qname) {
|
||||
if (-not $qname) { return $null }
|
||||
# Нотация Кларка {ns}local — так записаны почти все memberTypes
|
||||
if ($qname.StartsWith("{")) {
|
||||
$close = $qname.IndexOf("}")
|
||||
if ($close -gt 0) {
|
||||
$ns = $qname.Substring(1, $close - 1)
|
||||
$local = $qname.Substring($close + 1)
|
||||
if (-not $ns) { return $local }
|
||||
Register-Ns $ns
|
||||
return "$($nsPrefix[$ns]):$local"
|
||||
}
|
||||
}
|
||||
$parts = $qname.Split(":")
|
||||
if ($parts.Count -eq 2) {
|
||||
$ns = $el.GetNamespaceOfPrefix($parts[0])
|
||||
$local = $parts[1]
|
||||
} else {
|
||||
$ns = $el.GetNamespaceOfPrefix("")
|
||||
$local = $parts[0]
|
||||
}
|
||||
if (-not $ns) { return $qname }
|
||||
Register-Ns $ns
|
||||
return "$($nsPrefix[$ns]):$local"
|
||||
}
|
||||
|
||||
function Convert-QNameList([System.Xml.XmlElement]$el, [string]$list) {
|
||||
if (-not $list) { return $null }
|
||||
$out = @()
|
||||
foreach ($q in ($list -split "\s+")) {
|
||||
if ($q) { $out += (Convert-QName $el $q) }
|
||||
}
|
||||
return ($out -join " ")
|
||||
}
|
||||
|
||||
# --- Attribute helpers ---
|
||||
|
||||
function A([System.Xml.XmlElement]$el, [string]$name) {
|
||||
if ($el.HasAttribute($name)) { return $el.GetAttribute($name) }
|
||||
return $null
|
||||
}
|
||||
|
||||
# Emits `key="value"` pairs, skipping nulls
|
||||
function Attrs([object[]]$pairs) {
|
||||
$out = ""
|
||||
for ($i = 0; $i -lt $pairs.Count; $i += 2) {
|
||||
$v = $pairs[$i + 1]
|
||||
if ($null -ne $v) { $out += " $($pairs[$i])=`"$(Esc ([string]$v))`"" }
|
||||
}
|
||||
return $out
|
||||
}
|
||||
|
||||
# --- xdto: mirror attributes ---
|
||||
# Everything XSD cannot express literally rides as xdto:<same name as in Package.bin>.
|
||||
# Mirrors are emitted only when the literal bin form is not recoverable from the XSD.
|
||||
|
||||
$usesXdtoNs = $false
|
||||
function Mirror([string]$name, $value) {
|
||||
# $value НЕ типизируем: [string]$null коэрсится в "" и зеркало ложно появляется
|
||||
if ($null -eq $value) { return "" }
|
||||
$script:usesXdtoNs = $true
|
||||
return " xdto:$name=`"$(Esc ([string]$value))`""
|
||||
}
|
||||
|
||||
# Обычно префиксы генерируются схемой dNpM, но изредка узел несёт осмысленный
|
||||
# префикс (например dcsset) — его надо сохранить, иначе round-trip не сойдётся.
|
||||
function Mirror-Prefix([System.Xml.XmlElement]$el) {
|
||||
foreach ($a in $el.Attributes) {
|
||||
if ($a.Prefix -ne "xmlns") { continue }
|
||||
if ($a.get_LocalName() -match '^d\d+p\d+$') { continue }
|
||||
return (Mirror "prefix" $a.get_LocalName())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
# --- Facet emission (simple types) ---
|
||||
|
||||
$FACET_ATTRS = @("length", "minLength", "maxLength", "totalDigits", "fractionDigits",
|
||||
"minInclusive", "maxInclusive", "minExclusive", "maxExclusive", "whiteSpace")
|
||||
|
||||
function Emit-Facets([System.Xml.XmlElement]$el, [string]$indent) {
|
||||
foreach ($f in $FACET_ATTRS) {
|
||||
$v = A $el $f
|
||||
if ($null -ne $v) { X "$indent<xs:$f value=`"$(Esc $v)`"/>" }
|
||||
}
|
||||
foreach ($child in $el.ChildNodes) {
|
||||
if ($child.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
if ($child.get_LocalName() -eq "pattern") {
|
||||
X "$indent<xs:pattern value=`"$(Esc $child.InnerText)`"/>"
|
||||
} elseif ($child.get_LocalName() -eq "enumeration") {
|
||||
# xsi:type on enumeration has no XSD counterpart — mirror it
|
||||
$xsiType = $child.GetAttribute("type", $XSI_NS)
|
||||
$m = ""
|
||||
if ($xsiType) { $m = Mirror "type" (Convert-QName $child $xsiType) }
|
||||
X "$indent<xs:enumeration value=`"$(Esc $child.InnerText)`"$m/>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Has-SimpleContent([System.Xml.XmlElement]$el) {
|
||||
foreach ($c in $el.ChildNodes) {
|
||||
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "pattern") { return $true }
|
||||
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "enumeration") { return $true }
|
||||
}
|
||||
foreach ($f in $FACET_ATTRS) { if ($null -ne (A $el $f)) { return $true } }
|
||||
return $false
|
||||
}
|
||||
|
||||
# --- Simple type body (valueType / typeDef xsi:type=ValueType) ---
|
||||
|
||||
function Emit-SimpleTypeBody([System.Xml.XmlElement]$el, [string]$indent) {
|
||||
$variety = A $el "variety"
|
||||
$base = Convert-QName $el (A $el "base")
|
||||
$itemType = Convert-QName $el (A $el "itemType")
|
||||
$memberTypes= Convert-QNameList $el (A $el "memberTypes")
|
||||
|
||||
# variety is mirrored: "Atomic" is written explicitly for only part of the corpus
|
||||
$mv = Mirror "variety" $variety
|
||||
|
||||
# memberTypes почти всегда записаны нотацией Кларка; редкую префиксную форму зеркалим
|
||||
$rawMembers = A $el "memberTypes"
|
||||
if ($null -ne $rawMembers -and -not $rawMembers.StartsWith("{")) {
|
||||
$mv += Mirror "memberTypesForm" "prefixed"
|
||||
}
|
||||
# При нотации Кларка объявление xmlns:dNpM иногда присутствует, иногда нет —
|
||||
# из значения это не выводится (зависит от состояния сериализатора), зеркалим факт
|
||||
if ($null -ne $rawMembers -and $rawMembers.StartsWith("{")) {
|
||||
foreach ($a in $el.Attributes) {
|
||||
if ($a.Prefix -eq "xmlns" -and $a.get_LocalName() -match '^d\d+p\d+$') {
|
||||
$mv += Mirror "declareNs" $a.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($variety -eq "List" -or $itemType) {
|
||||
X "$indent<xs:list$(Attrs @('itemType', $itemType))$mv/>"
|
||||
return
|
||||
}
|
||||
if ($variety -eq "Union" -or $memberTypes) {
|
||||
$anon = @()
|
||||
foreach ($c in $el.ChildNodes) {
|
||||
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { $anon += $c }
|
||||
}
|
||||
if ($anon.Count -eq 0) {
|
||||
X "$indent<xs:union$(Attrs @('memberTypes', $memberTypes))$mv/>"
|
||||
} else {
|
||||
X "$indent<xs:union$(Attrs @('memberTypes', $memberTypes))$mv>"
|
||||
foreach ($c in $anon) {
|
||||
X "$indent`t<xs:simpleType>"
|
||||
Emit-SimpleTypeBody $c "$indent`t`t"
|
||||
X "$indent`t</xs:simpleType>"
|
||||
}
|
||||
X "$indent</xs:union>"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# Базовый тип может быть задан не атрибутом base, а вложенным анонимным typeDef
|
||||
$anonBase = $null
|
||||
foreach ($c in $el.ChildNodes) {
|
||||
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { $anonBase = $c; break }
|
||||
}
|
||||
|
||||
if ((Has-SimpleContent $el) -or $anonBase) {
|
||||
X "$indent<xs:restriction$(Attrs @('base', $base))$mv>"
|
||||
if ($anonBase) {
|
||||
X "$indent`t<xs:simpleType>"
|
||||
Emit-SimpleTypeBody $anonBase "$indent`t`t"
|
||||
X "$indent`t</xs:simpleType>"
|
||||
}
|
||||
Emit-Facets $el "$indent`t"
|
||||
X "$indent</xs:restriction>"
|
||||
} else {
|
||||
X "$indent<xs:restriction$(Attrs @('base', $base))$mv/>"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Property classification ---
|
||||
|
||||
function Get-PropForm([System.Xml.XmlElement]$p) {
|
||||
$f = A $p "form"
|
||||
if ($null -eq $f) { return "Element" }
|
||||
return $f
|
||||
}
|
||||
|
||||
function Get-AnonTypeDef([System.Xml.XmlElement]$p) {
|
||||
foreach ($c in $p.ChildNodes) {
|
||||
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "typeDef") { return $c }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- Property emission ---
|
||||
|
||||
function Emit-Property([System.Xml.XmlElement]$p, [string]$indent, [bool]$isGlobal) {
|
||||
$form = Get-PropForm $p
|
||||
$name = A $p "name"
|
||||
$type = Convert-QName $p (A $p "type")
|
||||
$ref = Convert-QName $p (A $p "ref")
|
||||
$local = A $p "localName"
|
||||
$lower = A $p "lowerBound"
|
||||
$upper = A $p "upperBound"
|
||||
$nill = A $p "nillable"
|
||||
$def = A $p "default"
|
||||
$fix = A $p "fixed"
|
||||
$anon = Get-AnonTypeDef $p
|
||||
# qualified записан как атрибут в пространстве имён XDTO
|
||||
$qual = $p.GetAttribute("qualified", $XDTO_NS)
|
||||
if ($qual -eq "") { $qual = $null }
|
||||
|
||||
# lowerBound/upperBound map 1:1 onto minOccurs/maxOccurs, including "written explicitly"
|
||||
$minOccurs = $lower
|
||||
$maxOccurs = $null
|
||||
if ($null -ne $upper) { $maxOccurs = if ($upper -eq "-1") { "unbounded" } else { $upper } }
|
||||
|
||||
# localName carries the original XML name when it is not a valid 1C identifier
|
||||
$xmlName = if ($null -ne $local) { $local } else { $name }
|
||||
$mirrorName = if ($null -ne $local) { Mirror "name" $name } else { "" }
|
||||
|
||||
$m = ""
|
||||
$isAttr = ($form -eq "Attribute")
|
||||
|
||||
if ($isAttr) {
|
||||
# XSD forbids nillable on attributes, and has no minOccurs/maxOccurs
|
||||
if ($null -ne $qual) { $m += Mirror "qualified" $qual }
|
||||
if ($null -ne $nill) { $m += Mirror "nillable" $nill }
|
||||
if ($null -ne $lower) { $m += Mirror "lowerBound" $lower }
|
||||
if ($null -ne $upper) { $m += Mirror "upperBound" $upper }
|
||||
$m += $mirrorName
|
||||
$body = Attrs @('name', $xmlName, 'ref', $ref, 'type', $type, 'default', $def, 'fixed', $fix)
|
||||
if ($anon) {
|
||||
X "$indent<xs:attribute$body$m>"
|
||||
X "$indent`t<xs:simpleType>"
|
||||
Emit-SimpleTypeBody $anon "$indent`t`t"
|
||||
X "$indent`t</xs:simpleType>"
|
||||
X "$indent</xs:attribute>"
|
||||
} else {
|
||||
X "$indent<xs:attribute$body$m/>"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ($form -eq "Text") {
|
||||
# handled by the owning complexType (xs:simpleContent)
|
||||
return
|
||||
}
|
||||
|
||||
# form="Element" written explicitly is indistinguishable in XSD from the default
|
||||
if ($null -ne (A $p "form")) { $m += Mirror "form" $form }
|
||||
if ($null -ne $qual) { $m += Mirror "qualified" $qual }
|
||||
$m += $mirrorName
|
||||
$m += (Mirror-Prefix $p)
|
||||
|
||||
$body = Attrs @('name', $xmlName, 'ref', $ref, 'type', $type,
|
||||
'minOccurs', $minOccurs, 'maxOccurs', $maxOccurs,
|
||||
'nillable', $nill, 'default', $def, 'fixed', $fix)
|
||||
|
||||
if ($anon) {
|
||||
X "$indent<xs:element$body$m>"
|
||||
if ($anon.GetAttribute("type", $XSI_NS) -eq "ObjectType") {
|
||||
$anonBase = Convert-QName $anon (A $anon "base")
|
||||
if ($anonBase) {
|
||||
X "$indent`t<xs:complexType$(ComplexTypeAttrs $anon)>"
|
||||
X "$indent`t`t<xs:complexContent>"
|
||||
X "$indent`t`t`t<xs:extension$(Attrs @('base', $anonBase))>"
|
||||
Emit-ComplexTypeBody $anon "$indent`t`t`t`t"
|
||||
X "$indent`t`t`t</xs:extension>"
|
||||
X "$indent`t`t</xs:complexContent>"
|
||||
X "$indent`t</xs:complexType>"
|
||||
} else {
|
||||
X "$indent`t<xs:complexType$(ComplexTypeAttrs $anon)>"
|
||||
Emit-ComplexTypeBody $anon "$indent`t`t"
|
||||
X "$indent`t</xs:complexType>"
|
||||
}
|
||||
} else {
|
||||
X "$indent`t<xs:simpleType>"
|
||||
Emit-SimpleTypeBody $anon "$indent`t`t"
|
||||
X "$indent`t</xs:simpleType>"
|
||||
}
|
||||
X "$indent</xs:element>"
|
||||
} else {
|
||||
X "$indent<xs:element$body$m/>"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Complex type body (objectType / typeDef xsi:type=ObjectType) ---
|
||||
|
||||
function Emit-ComplexTypeBody([System.Xml.XmlElement]$el, [string]$indent) {
|
||||
$open = A $el "open"
|
||||
$ordered = A $el "ordered"
|
||||
$sequenced = A $el "sequenced"
|
||||
|
||||
$props = @()
|
||||
foreach ($c in $el.ChildNodes) {
|
||||
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element -and $c.get_LocalName() -eq "property") { $props += $c }
|
||||
}
|
||||
|
||||
$elems = @(); $attrs = @(); $text = $null
|
||||
foreach ($p in $props) {
|
||||
switch (Get-PropForm $p) {
|
||||
"Attribute" { $attrs += $p }
|
||||
"Text" { $text = $p }
|
||||
default { $elems += $p }
|
||||
}
|
||||
}
|
||||
|
||||
# simpleContent: a "Text" property holds the element's own value
|
||||
if ($null -ne $text) {
|
||||
$tType = Convert-QName $text (A $text "type")
|
||||
$tm = ""
|
||||
$tName = A $text "name"
|
||||
if ($tName -ne "__content") { $tm += Mirror "textName" $tName }
|
||||
foreach ($extra in @("lowerBound", "upperBound", "nillable")) {
|
||||
$v = A $text $extra
|
||||
if ($null -ne $v) { $tm += Mirror "text$extra" $v }
|
||||
}
|
||||
X "$indent<xs:simpleContent>"
|
||||
X "$indent`t<xs:extension$(Attrs @('base', $tType))$tm>"
|
||||
foreach ($a in $attrs) { Emit-Property $a "$indent`t`t" $false }
|
||||
X "$indent`t</xs:extension>"
|
||||
X "$indent</xs:simpleContent>"
|
||||
return
|
||||
}
|
||||
|
||||
$particleTag = if ($ordered -eq "false") { "xs:choice" } else { "xs:sequence" }
|
||||
$needParticle = ($elems.Count -gt 0) -or ($open -eq "true")
|
||||
|
||||
if ($needParticle) {
|
||||
X "$indent<$particleTag>"
|
||||
foreach ($e in $elems) { Emit-Property $e "$indent`t" $false }
|
||||
if ($open -eq "true") {
|
||||
X "$indent`t<xs:any namespace=`"##any`" processContents=`"lax`" minOccurs=`"0`" maxOccurs=`"unbounded`"/>"
|
||||
}
|
||||
X "$indent</$particleTag>"
|
||||
}
|
||||
|
||||
foreach ($a in $attrs) { Emit-Property $a "$indent" $false }
|
||||
if ($open -eq "true") {
|
||||
X "$indent<xs:anyAttribute namespace=`"##any`" processContents=`"lax`"/>"
|
||||
}
|
||||
}
|
||||
|
||||
# Attributes of a complexType tag itself (mirrors + XSD-native abstract/mixed)
|
||||
function ComplexTypeAttrs([System.Xml.XmlElement]$el) {
|
||||
$open = A $el "open"
|
||||
$ordered = A $el "ordered"
|
||||
$sequenced = A $el "sequenced"
|
||||
$abstract = A $el "abstract"
|
||||
$mixed = A $el "mixed"
|
||||
|
||||
$out = ""
|
||||
if ($abstract -eq "true") { $out += " abstract=`"true`"" } elseif ($null -ne $abstract) { $out += Mirror "abstract" $abstract }
|
||||
if ($mixed -eq "true") { $out += " mixed=`"true`"" } elseif ($null -ne $mixed) { $out += Mirror "mixed" $mixed }
|
||||
|
||||
# XSD требует объявлять атрибуты после частицы, поэтому исходный порядок свойств
|
||||
# восстановим как «сначала form=Attribute, потом остальные» — это верно для 96.5%
|
||||
# типов корпуса. Расхождения (768 типов) зеркалим списком имён.
|
||||
$order = @(); $kinds = @()
|
||||
foreach ($c in $el.ChildNodes) {
|
||||
if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element -or $c.get_LocalName() -ne "property") { continue }
|
||||
$nm = A $c "name"
|
||||
if ($null -eq $nm) { $nm = "@" + ((A $c "ref") -split ":")[-1] }
|
||||
$order += $nm
|
||||
$kinds += $(if ((Get-PropForm $c) -eq "Attribute") { 0 } else { 1 })
|
||||
}
|
||||
if ($order.Count -gt 1) {
|
||||
$natural = $true
|
||||
for ($i = 1; $i -lt $kinds.Count; $i++) { if ($kinds[$i] -lt $kinds[$i - 1]) { $natural = $false; break } }
|
||||
if (-not $natural) { $out += Mirror "order" ($order -join "|") }
|
||||
}
|
||||
|
||||
# open="true" is rendered as xs:any + xs:anyAttribute; anything else is mirrored
|
||||
if ($null -ne $open -and $open -ne "true") { $out += Mirror "open" $open }
|
||||
# ordered="false" is rendered as xs:choice; "true" written explicitly is mirrored
|
||||
if ($null -ne $ordered -and $ordered -ne "false") { $out += Mirror "ordered" $ordered }
|
||||
# sequenced has no XSD counterpart at all
|
||||
if ($null -ne $sequenced) { $out += Mirror "sequenced" $sequenced }
|
||||
|
||||
return $out
|
||||
}
|
||||
|
||||
# --- Metadata properties (Name/Synonym/Comment) from the object's .xml ---
|
||||
|
||||
function Get-MetadataBlock() {
|
||||
if (-not $paths.Md -or -not (Test-Path $paths.Md)) { return $null }
|
||||
$md = New-Object System.Xml.XmlDocument
|
||||
$md.Load($paths.Md)
|
||||
$nsm = New-Object System.Xml.XmlNamespaceManager($md.NameTable)
|
||||
$nsm.AddNamespace("md", $MD_NS)
|
||||
$nsm.AddNamespace("v8", $V8_NS)
|
||||
$props = $md.SelectSingleNode("//md:XDTOPackage/md:Properties", $nsm)
|
||||
if (-not $props) { return $null }
|
||||
|
||||
$res = @{ Name = $null; Comment = $null; Synonym = @() }
|
||||
$n = $props.SelectSingleNode("md:Name", $nsm); if ($n) { $res.Name = $n.InnerText }
|
||||
$c = $props.SelectSingleNode("md:Comment", $nsm); if ($c) { $res.Comment = $c.InnerText }
|
||||
foreach ($item in $props.SelectNodes("md:Synonym/v8:item", $nsm)) {
|
||||
$lang = $item.SelectSingleNode("v8:lang", $nsm)
|
||||
$cont = $item.SelectSingleNode("v8:content", $nsm)
|
||||
$res.Synonym += @{ Lang = $(if ($lang) { $lang.InnerText } else { "" }); Content = $(if ($cont) { $cont.InnerText } else { "" }) }
|
||||
}
|
||||
return $res
|
||||
}
|
||||
|
||||
$meta = Get-MetadataBlock
|
||||
|
||||
# --- Emit ---
|
||||
# Body first: emitting it registers every namespace actually referenced, so the
|
||||
# schema element can declare a complete prefix map.
|
||||
|
||||
$bodyBuilder = New-Object System.Text.StringBuilder
|
||||
$mainBuilder = $sb
|
||||
$sb = $bodyBuilder
|
||||
|
||||
foreach ($node in $pkg.ChildNodes) {
|
||||
if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
switch ($node.get_LocalName()) {
|
||||
"import" {
|
||||
X "`t<xs:import namespace=`"$(Esc $node.GetAttribute('namespace'))`"/>"
|
||||
}
|
||||
"property" {
|
||||
Emit-Property $node "`t" $true
|
||||
}
|
||||
"valueType" {
|
||||
$name = A $node "name"
|
||||
X "`t<xs:simpleType$(Attrs @('name', $name))>"
|
||||
Emit-SimpleTypeBody $node "`t`t"
|
||||
X "`t</xs:simpleType>"
|
||||
}
|
||||
"objectType" {
|
||||
$name = A $node "name"
|
||||
$base = Convert-QName $node (A $node "base")
|
||||
$cta = ComplexTypeAttrs $node
|
||||
if ($base) {
|
||||
X "`t<xs:complexType$(Attrs @('name', $name))$cta>"
|
||||
X "`t`t<xs:complexContent>"
|
||||
X "`t`t`t<xs:extension$(Attrs @('base', $base))>"
|
||||
Emit-ComplexTypeBody $node "`t`t`t`t"
|
||||
X "`t`t`t</xs:extension>"
|
||||
X "`t`t</xs:complexContent>"
|
||||
X "`t</xs:complexType>"
|
||||
} else {
|
||||
$hasBody = $false
|
||||
foreach ($c in $node.ChildNodes) { if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element) { $hasBody = $true; break } }
|
||||
if (-not $hasBody -and (A $node "open") -ne "true") {
|
||||
X "`t<xs:complexType$(Attrs @('name', $name))$cta/>"
|
||||
} else {
|
||||
X "`t<xs:complexType$(Attrs @('name', $name))$cta>"
|
||||
Emit-ComplexTypeBody $node "`t`t"
|
||||
X "`t</xs:complexType>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sb = $mainBuilder
|
||||
|
||||
# --- Schema element ---
|
||||
|
||||
$nsDecls = ""
|
||||
foreach ($kv in ($nsPrefix.GetEnumerator() | Sort-Object { $_.Value })) {
|
||||
$nsDecls += " xmlns:$($kv.Value)=`"$(Esc $kv.Key)`""
|
||||
}
|
||||
if ($usesXdtoNs) { $nsDecls += " xmlns:xdto=`"$XDTO_NS`"" }
|
||||
|
||||
$schemaAttrs = ""
|
||||
$efq = A $pkg "elementFormQualified"
|
||||
$afq = A $pkg "attributeFormQualified"
|
||||
if ($null -ne $efq) { $schemaAttrs += " elementFormDefault=`"$(if ($efq -eq 'true') { 'qualified' } else { 'unqualified' })`"" }
|
||||
if ($null -ne $afq) { $schemaAttrs += " attributeFormDefault=`"$(if ($afq -eq 'true') { 'qualified' } else { 'unqualified' })`"" }
|
||||
|
||||
X "<xs:schema$nsDecls$(Attrs @('targetNamespace', $targetNs))$schemaAttrs>"
|
||||
|
||||
if ($meta) {
|
||||
X "`t<xs:annotation>"
|
||||
X "`t`t<xs:appinfo>"
|
||||
X "`t`t`t<xdto:package xmlns:xdto=`"$XDTO_NS`">"
|
||||
if ($null -ne $meta.Name) { X "`t`t`t`t<xdto:name>$(EscText $meta.Name)</xdto:name>" }
|
||||
if ($null -ne $meta.Comment -and $meta.Comment -ne "") { X "`t`t`t`t<xdto:comment>$(EscText $meta.Comment)</xdto:comment>" }
|
||||
foreach ($s in $meta.Synonym) {
|
||||
X "`t`t`t`t<xdto:synonym lang=`"$(Esc $s.Lang)`">$(EscText $s.Content)</xdto:synonym>"
|
||||
}
|
||||
X "`t`t`t</xdto:package>"
|
||||
X "`t`t</xs:appinfo>"
|
||||
X "`t</xs:annotation>"
|
||||
}
|
||||
|
||||
[void]$sb.Append($bodyBuilder.ToString())
|
||||
X "</xs:schema>"
|
||||
|
||||
# --- Output (UTF-8 with BOM, CRLF — matches the Designer's own XSD export) ---
|
||||
|
||||
$text = $sb.ToString()
|
||||
if ($OutFile) {
|
||||
$dir = [System.IO.Path]::GetDirectoryName($OutFile)
|
||||
if ($dir -and -not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
|
||||
$enc = New-Object System.Text.UTF8Encoding($true)
|
||||
[System.IO.File]::WriteAllText($OutFile, $text, $enc)
|
||||
Write-Host "✓ XSD записана: $OutFile"
|
||||
Write-Host " targetNamespace: $targetNs"
|
||||
} else {
|
||||
[Console]::Out.Write($text)
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
# xdto-decompile v1.0 — Convert 1C XDTO package to XML Schema (XSD) (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from lxml import etree
|
||||
|
||||
# newline="" — иначе Windows транслирует \n и CRLF схемы удваивается в \r\r\n
|
||||
sys.stdout.reconfigure(encoding="utf-8", newline="")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
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"
|
||||
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument("-PackagePath", "-Path", required=True)
|
||||
parser.add_argument("-OutFile", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
# ── resolve paths ────────────────────────────────────────────
|
||||
|
||||
package_path = args.PackagePath
|
||||
bin_path = None
|
||||
md_path = None
|
||||
|
||||
if os.path.isfile(package_path):
|
||||
if os.path.basename(package_path) == "Package.bin":
|
||||
bin_path = package_path
|
||||
pkg_dir = os.path.dirname(os.path.dirname(package_path))
|
||||
if os.path.exists(pkg_dir + ".xml"):
|
||||
md_path = pkg_dir + ".xml"
|
||||
elif package_path.endswith(".xml"):
|
||||
md_path = package_path
|
||||
stem = os.path.join(os.path.dirname(package_path),
|
||||
os.path.splitext(os.path.basename(package_path))[0])
|
||||
c = os.path.join(stem, "Ext", "Package.bin")
|
||||
if os.path.exists(c):
|
||||
bin_path = c
|
||||
elif os.path.isdir(package_path):
|
||||
c = os.path.join(package_path, "Ext", "Package.bin")
|
||||
if os.path.exists(c):
|
||||
bin_path = c
|
||||
m = package_path.rstrip("\\/") + ".xml"
|
||||
if os.path.exists(m):
|
||||
md_path = m
|
||||
|
||||
if not bin_path:
|
||||
print(f"Не найден Ext/Package.bin для пути: {package_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
doc = etree.parse(bin_path)
|
||||
pkg = doc.getroot()
|
||||
|
||||
|
||||
def local(el):
|
||||
return etree.QName(el).localname
|
||||
|
||||
|
||||
if local(pkg) != "package":
|
||||
print(f"Ожидался корневой <package>, получен <{local(pkg)}>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
target_ns = pkg.get("targetNamespace")
|
||||
|
||||
# ── namespace -> prefix map for the emitted schema ───────────
|
||||
|
||||
ns_prefix = {XS_NS: "xs"}
|
||||
if target_ns:
|
||||
ns_prefix[target_ns] = "tns"
|
||||
|
||||
imports = []
|
||||
for imp in pkg:
|
||||
if isinstance(imp.tag, str) and local(imp) == "import":
|
||||
ns = imp.get("namespace")
|
||||
imports.append(ns)
|
||||
if ns not in ns_prefix:
|
||||
ns_prefix[ns] = "ns" + str(len(ns_prefix))
|
||||
|
||||
|
||||
def register_ns(ns):
|
||||
if ns and ns not in ns_prefix:
|
||||
ns_prefix[ns] = "ns" + str(len(ns_prefix))
|
||||
|
||||
|
||||
# ── output buffer ────────────────────────────────────────────
|
||||
|
||||
lines = []
|
||||
|
||||
|
||||
def X(line):
|
||||
lines.append(line)
|
||||
|
||||
|
||||
def esc(s):
|
||||
if s is None:
|
||||
return ""
|
||||
return str(s).replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
||||
|
||||
|
||||
def esc_text(s):
|
||||
if s is None:
|
||||
return ""
|
||||
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
|
||||
|
||||
# ── QName conversion: bin prefix -> schema prefix ────────────
|
||||
|
||||
def convert_qname(el, qname):
|
||||
if not qname:
|
||||
return None
|
||||
# Нотация Кларка {ns}local — так записаны почти все memberTypes
|
||||
if qname.startswith("{"):
|
||||
close = qname.find("}")
|
||||
if close > 0:
|
||||
ns = qname[1:close]
|
||||
loc = qname[close + 1:]
|
||||
if not ns:
|
||||
return loc
|
||||
register_ns(ns)
|
||||
return f"{ns_prefix[ns]}:{loc}"
|
||||
parts = qname.split(":")
|
||||
if len(parts) == 2:
|
||||
ns = el.nsmap.get(parts[0])
|
||||
loc = parts[1]
|
||||
else:
|
||||
ns = el.nsmap.get(None)
|
||||
loc = parts[0]
|
||||
if not ns:
|
||||
return qname
|
||||
register_ns(ns)
|
||||
return f"{ns_prefix[ns]}:{loc}"
|
||||
|
||||
|
||||
def convert_qname_list(el, lst):
|
||||
if not lst:
|
||||
return None
|
||||
return " ".join(convert_qname(el, q) for q in lst.split() if q)
|
||||
|
||||
|
||||
def attrs(pairs):
|
||||
out = ""
|
||||
for i in range(0, len(pairs), 2):
|
||||
v = pairs[i + 1]
|
||||
if v is not None:
|
||||
out += f' {pairs[i]}="{esc(v)}"'
|
||||
return out
|
||||
|
||||
|
||||
# ── xdto: mirror attributes ──────────────────────────────────
|
||||
|
||||
state = {"uses_xdto": False}
|
||||
|
||||
|
||||
def mirror(name, value):
|
||||
if value is None:
|
||||
return ""
|
||||
state["uses_xdto"] = True
|
||||
return f' xdto:{name}="{esc(value)}"'
|
||||
|
||||
|
||||
DNPM = re.compile(r"^d\d+p\d+$")
|
||||
|
||||
|
||||
def ns_decls_of(el):
|
||||
"""Локальные объявления xmlns на самом узле, как (префикс, uri)."""
|
||||
parent_map = el.getparent().nsmap if el.getparent() is not None else {}
|
||||
for px, uri in el.nsmap.items():
|
||||
if px is None:
|
||||
continue
|
||||
if parent_map.get(px) == uri:
|
||||
continue
|
||||
yield px, uri
|
||||
|
||||
|
||||
def mirror_prefix(el):
|
||||
# Обычно префиксы генерируются схемой dNpM, но изредка узел несёт осмысленный
|
||||
# префикс (например dcsset) — его надо сохранить, иначе round-trip не сойдётся.
|
||||
for px, _uri in ns_decls_of(el):
|
||||
if DNPM.match(px):
|
||||
continue
|
||||
return mirror("prefix", px)
|
||||
return ""
|
||||
|
||||
|
||||
FACET_ATTRS = ["length", "minLength", "maxLength", "totalDigits", "fractionDigits",
|
||||
"minInclusive", "maxInclusive", "minExclusive", "maxExclusive", "whiteSpace"]
|
||||
|
||||
|
||||
def emit_facets(el, indent):
|
||||
for f in FACET_ATTRS:
|
||||
v = el.get(f)
|
||||
if v is not None:
|
||||
X(f'{indent}<xs:{f} value="{esc(v)}"/>')
|
||||
for child in el:
|
||||
if not isinstance(child.tag, str):
|
||||
continue
|
||||
ln = local(child)
|
||||
if ln == "pattern":
|
||||
X(f'{indent}<xs:pattern value="{esc(child.text or "")}"/>')
|
||||
elif ln == "enumeration":
|
||||
xsi_type = child.get(f"{{{XSI_NS}}}type")
|
||||
m = mirror("type", convert_qname(child, xsi_type)) if xsi_type else ""
|
||||
X(f'{indent}<xs:enumeration value="{esc(child.text or "")}"{m}/>')
|
||||
|
||||
|
||||
def has_simple_content(el):
|
||||
for c in el:
|
||||
if isinstance(c.tag, str) and local(c) in ("pattern", "enumeration"):
|
||||
return True
|
||||
return any(el.get(f) is not None for f in FACET_ATTRS)
|
||||
|
||||
|
||||
# ── simple type body (valueType / typeDef xsi:type=ValueType) ─
|
||||
|
||||
def emit_simple_type_body(el, indent):
|
||||
variety = el.get("variety")
|
||||
base = convert_qname(el, el.get("base"))
|
||||
item_type = convert_qname(el, el.get("itemType"))
|
||||
member_types = convert_qname_list(el, el.get("memberTypes"))
|
||||
|
||||
mv = mirror("variety", variety)
|
||||
|
||||
raw_members = el.get("memberTypes")
|
||||
if raw_members is not None and not raw_members.startswith("{"):
|
||||
mv += mirror("memberTypesForm", "prefixed")
|
||||
# При нотации Кларка объявление xmlns:dNpM иногда присутствует, иногда нет —
|
||||
# из значения это не выводится (зависит от состояния сериализатора), зеркалим факт
|
||||
if raw_members is not None and raw_members.startswith("{"):
|
||||
for px, uri in ns_decls_of(el):
|
||||
if DNPM.match(px):
|
||||
mv += mirror("declareNs", uri)
|
||||
break
|
||||
|
||||
if variety == "List" or item_type:
|
||||
X(f'{indent}<xs:list{attrs(["itemType", item_type])}{mv}/>')
|
||||
return
|
||||
if variety == "Union" or member_types:
|
||||
anon = [c for c in el if isinstance(c.tag, str) and local(c) == "typeDef"]
|
||||
if not anon:
|
||||
X(f'{indent}<xs:union{attrs(["memberTypes", member_types])}{mv}/>')
|
||||
else:
|
||||
X(f'{indent}<xs:union{attrs(["memberTypes", member_types])}{mv}>')
|
||||
for c in anon:
|
||||
X(f"{indent}\t<xs:simpleType>")
|
||||
emit_simple_type_body(c, indent + "\t\t")
|
||||
X(f"{indent}\t</xs:simpleType>")
|
||||
X(f"{indent}</xs:union>")
|
||||
return
|
||||
|
||||
# Базовый тип может быть задан не атрибутом base, а вложенным анонимным typeDef
|
||||
anon_base = next((c for c in el if isinstance(c.tag, str) and local(c) == "typeDef"), None)
|
||||
|
||||
if has_simple_content(el) or anon_base is not None:
|
||||
X(f'{indent}<xs:restriction{attrs(["base", base])}{mv}>')
|
||||
if anon_base is not None:
|
||||
X(f"{indent}\t<xs:simpleType>")
|
||||
emit_simple_type_body(anon_base, indent + "\t\t")
|
||||
X(f"{indent}\t</xs:simpleType>")
|
||||
emit_facets(el, indent + "\t")
|
||||
X(f"{indent}</xs:restriction>")
|
||||
else:
|
||||
X(f'{indent}<xs:restriction{attrs(["base", base])}{mv}/>')
|
||||
|
||||
|
||||
# ── property classification ──────────────────────────────────
|
||||
|
||||
def prop_form(p):
|
||||
f = p.get("form")
|
||||
return "Element" if f is None else f
|
||||
|
||||
|
||||
def anon_type_def(p):
|
||||
return next((c for c in p if isinstance(c.tag, str) and local(c) == "typeDef"), None)
|
||||
|
||||
|
||||
# ── property emission ────────────────────────────────────────
|
||||
|
||||
def emit_property(p, indent):
|
||||
form = prop_form(p)
|
||||
name = p.get("name")
|
||||
type_ = convert_qname(p, p.get("type"))
|
||||
ref = convert_qname(p, p.get("ref"))
|
||||
local_name = p.get("localName")
|
||||
lower = p.get("lowerBound")
|
||||
upper = p.get("upperBound")
|
||||
nill = p.get("nillable")
|
||||
default = p.get("default")
|
||||
fixed = p.get("fixed")
|
||||
anon = anon_type_def(p)
|
||||
qual = p.get(f"{{{XDTO_NS}}}qualified")
|
||||
|
||||
min_occurs = lower
|
||||
max_occurs = None
|
||||
if upper is not None:
|
||||
max_occurs = "unbounded" if upper == "-1" else upper
|
||||
|
||||
xml_name = local_name if local_name is not None else name
|
||||
mirror_name = mirror("name", name) if local_name is not None else ""
|
||||
|
||||
m = ""
|
||||
if form == "Attribute":
|
||||
if qual is not None:
|
||||
m += mirror("qualified", qual)
|
||||
if nill is not None:
|
||||
m += mirror("nillable", nill)
|
||||
if lower is not None:
|
||||
m += mirror("lowerBound", lower)
|
||||
if upper is not None:
|
||||
m += mirror("upperBound", upper)
|
||||
m += mirror_name
|
||||
body = attrs(["name", xml_name, "ref", ref, "type", type_, "default", default, "fixed", fixed])
|
||||
if anon is not None:
|
||||
X(f"{indent}<xs:attribute{body}{m}>")
|
||||
X(f"{indent}\t<xs:simpleType>")
|
||||
emit_simple_type_body(anon, indent + "\t\t")
|
||||
X(f"{indent}\t</xs:simpleType>")
|
||||
X(f"{indent}</xs:attribute>")
|
||||
else:
|
||||
X(f"{indent}<xs:attribute{body}{m}/>")
|
||||
return
|
||||
|
||||
if form == "Text":
|
||||
# handled by the owning complexType (xs:simpleContent)
|
||||
return
|
||||
|
||||
if p.get("form") is not None:
|
||||
m += mirror("form", form)
|
||||
if qual is not None:
|
||||
m += mirror("qualified", qual)
|
||||
m += mirror_name
|
||||
m += mirror_prefix(p)
|
||||
|
||||
body = attrs(["name", xml_name, "ref", ref, "type", type_,
|
||||
"minOccurs", min_occurs, "maxOccurs", max_occurs,
|
||||
"nillable", nill, "default", default, "fixed", fixed])
|
||||
|
||||
if anon is not None:
|
||||
X(f"{indent}<xs:element{body}{m}>")
|
||||
if anon.get(f"{{{XSI_NS}}}type") == "ObjectType":
|
||||
anon_base = convert_qname(anon, anon.get("base"))
|
||||
if anon_base:
|
||||
X(f"{indent}\t<xs:complexType{complex_type_attrs(anon)}>")
|
||||
X(f"{indent}\t\t<xs:complexContent>")
|
||||
X(f'{indent}\t\t\t<xs:extension{attrs(["base", anon_base])}>')
|
||||
emit_complex_type_body(anon, indent + "\t\t\t\t")
|
||||
X(f"{indent}\t\t\t</xs:extension>")
|
||||
X(f"{indent}\t\t</xs:complexContent>")
|
||||
X(f"{indent}\t</xs:complexType>")
|
||||
else:
|
||||
X(f"{indent}\t<xs:complexType{complex_type_attrs(anon)}>")
|
||||
emit_complex_type_body(anon, indent + "\t\t")
|
||||
X(f"{indent}\t</xs:complexType>")
|
||||
else:
|
||||
X(f"{indent}\t<xs:simpleType>")
|
||||
emit_simple_type_body(anon, indent + "\t\t")
|
||||
X(f"{indent}\t</xs:simpleType>")
|
||||
X(f"{indent}</xs:element>")
|
||||
else:
|
||||
X(f"{indent}<xs:element{body}{m}/>")
|
||||
|
||||
|
||||
# ── complex type body ────────────────────────────────────────
|
||||
|
||||
def emit_complex_type_body(el, indent):
|
||||
open_ = el.get("open")
|
||||
ordered = el.get("ordered")
|
||||
|
||||
props = [c for c in el if isinstance(c.tag, str) and local(c) == "property"]
|
||||
elems, attr_props, text = [], [], None
|
||||
for p in props:
|
||||
f = prop_form(p)
|
||||
if f == "Attribute":
|
||||
attr_props.append(p)
|
||||
elif f == "Text":
|
||||
text = p
|
||||
else:
|
||||
elems.append(p)
|
||||
|
||||
if text is not None:
|
||||
t_type = convert_qname(text, text.get("type"))
|
||||
tm = ""
|
||||
t_name = text.get("name")
|
||||
if t_name != "__content":
|
||||
tm += mirror("textName", t_name)
|
||||
for extra in ("lowerBound", "upperBound", "nillable"):
|
||||
v = text.get(extra)
|
||||
if v is not None:
|
||||
tm += mirror("text" + extra, v)
|
||||
X(f"{indent}<xs:simpleContent>")
|
||||
X(f'{indent}\t<xs:extension{attrs(["base", t_type])}{tm}>')
|
||||
for a in attr_props:
|
||||
emit_property(a, indent + "\t\t")
|
||||
X(f"{indent}\t</xs:extension>")
|
||||
X(f"{indent}</xs:simpleContent>")
|
||||
return
|
||||
|
||||
particle_tag = "xs:choice" if ordered == "false" else "xs:sequence"
|
||||
if elems or open_ == "true":
|
||||
X(f"{indent}<{particle_tag}>")
|
||||
for e in elems:
|
||||
emit_property(e, indent + "\t")
|
||||
if open_ == "true":
|
||||
X(f'{indent}\t<xs:any namespace="##any" processContents="lax" minOccurs="0" maxOccurs="unbounded"/>')
|
||||
X(f"{indent}</{particle_tag}>")
|
||||
|
||||
for a in attr_props:
|
||||
emit_property(a, indent)
|
||||
if open_ == "true":
|
||||
X(f'{indent}<xs:anyAttribute namespace="##any" processContents="lax"/>')
|
||||
|
||||
|
||||
def complex_type_attrs(el):
|
||||
open_ = el.get("open")
|
||||
ordered = el.get("ordered")
|
||||
sequenced = el.get("sequenced")
|
||||
abstract = el.get("abstract")
|
||||
mixed = el.get("mixed")
|
||||
|
||||
out = ""
|
||||
if abstract == "true":
|
||||
out += ' abstract="true"'
|
||||
elif abstract is not None:
|
||||
out += mirror("abstract", abstract)
|
||||
if mixed == "true":
|
||||
out += ' mixed="true"'
|
||||
elif mixed is not None:
|
||||
out += mirror("mixed", mixed)
|
||||
|
||||
# XSD требует объявлять атрибуты после частицы, поэтому исходный порядок свойств
|
||||
# восстановим как «сначала form=Attribute, потом остальные» — верно для 96.5% типов
|
||||
order, kinds = [], []
|
||||
for c in el:
|
||||
if not isinstance(c.tag, str) or local(c) != "property":
|
||||
continue
|
||||
nm = c.get("name")
|
||||
if nm is None:
|
||||
nm = "@" + (c.get("ref") or "").split(":")[-1]
|
||||
order.append(nm)
|
||||
kinds.append(0 if prop_form(c) == "Attribute" else 1)
|
||||
if len(order) > 1:
|
||||
natural = all(kinds[i] >= kinds[i - 1] for i in range(1, len(kinds)))
|
||||
if not natural:
|
||||
out += mirror("order", "|".join(order))
|
||||
|
||||
if open_ is not None and open_ != "true":
|
||||
out += mirror("open", open_)
|
||||
if ordered is not None and ordered != "false":
|
||||
out += mirror("ordered", ordered)
|
||||
if sequenced is not None:
|
||||
out += mirror("sequenced", sequenced)
|
||||
return out
|
||||
|
||||
|
||||
# ── metadata properties ──────────────────────────────────────
|
||||
|
||||
meta = None
|
||||
if md_path and os.path.exists(md_path):
|
||||
md = etree.parse(md_path)
|
||||
props_el = md.find(f".//{{{MD_NS}}}XDTOPackage/{{{MD_NS}}}Properties")
|
||||
if props_el is not None:
|
||||
meta = {"Name": None, "Comment": None, "Synonym": []}
|
||||
n = props_el.find(f"{{{MD_NS}}}Name")
|
||||
if n is not None:
|
||||
meta["Name"] = n.text or ""
|
||||
c = props_el.find(f"{{{MD_NS}}}Comment")
|
||||
if c is not None:
|
||||
meta["Comment"] = c.text or ""
|
||||
for item in props_el.iterfind(f"{{{MD_NS}}}Synonym/{{{V8_NS}}}item"):
|
||||
lang = item.find(f"{{{V8_NS}}}lang")
|
||||
cont = item.find(f"{{{V8_NS}}}content")
|
||||
meta["Synonym"].append({
|
||||
"Lang": (lang.text or "") if lang is not None else "",
|
||||
"Content": (cont.text or "") if cont is not None else "",
|
||||
})
|
||||
|
||||
# ── emit ─────────────────────────────────────────────────────
|
||||
# Тело первым: при его генерации регистрируются все использованные пространства
|
||||
# имён, поэтому корневой элемент может объявить полную карту префиксов.
|
||||
|
||||
for node in pkg:
|
||||
if not isinstance(node.tag, str):
|
||||
continue
|
||||
ln = local(node)
|
||||
if ln == "import":
|
||||
X(f'\t<xs:import namespace="{esc(node.get("namespace"))}"/>')
|
||||
elif ln == "property":
|
||||
emit_property(node, "\t")
|
||||
elif ln == "valueType":
|
||||
X(f'\t<xs:simpleType{attrs(["name", node.get("name")])}>')
|
||||
emit_simple_type_body(node, "\t\t")
|
||||
X("\t</xs:simpleType>")
|
||||
elif ln == "objectType":
|
||||
name = node.get("name")
|
||||
base = convert_qname(node, node.get("base"))
|
||||
cta = complex_type_attrs(node)
|
||||
if base:
|
||||
X(f'\t<xs:complexType{attrs(["name", name])}{cta}>')
|
||||
X("\t\t<xs:complexContent>")
|
||||
X(f'\t\t\t<xs:extension{attrs(["base", base])}>')
|
||||
emit_complex_type_body(node, "\t\t\t\t")
|
||||
X("\t\t\t</xs:extension>")
|
||||
X("\t\t</xs:complexContent>")
|
||||
X("\t</xs:complexType>")
|
||||
else:
|
||||
has_body = any(isinstance(c.tag, str) for c in node)
|
||||
if not has_body and node.get("open") != "true":
|
||||
X(f'\t<xs:complexType{attrs(["name", name])}{cta}/>')
|
||||
else:
|
||||
X(f'\t<xs:complexType{attrs(["name", name])}{cta}>')
|
||||
emit_complex_type_body(node, "\t\t")
|
||||
X("\t</xs:complexType>")
|
||||
|
||||
body_lines = lines
|
||||
lines = []
|
||||
|
||||
# ── schema element ───────────────────────────────────────────
|
||||
|
||||
ns_decls = ""
|
||||
for uri, px in sorted(ns_prefix.items(), key=lambda kv: kv[1]):
|
||||
ns_decls += f' xmlns:{px}="{esc(uri)}"'
|
||||
if state["uses_xdto"]:
|
||||
ns_decls += f' xmlns:xdto="{XDTO_NS}"'
|
||||
|
||||
schema_attrs = ""
|
||||
efq = pkg.get("elementFormQualified")
|
||||
afq = pkg.get("attributeFormQualified")
|
||||
if efq is not None:
|
||||
schema_attrs += f' elementFormDefault="{"qualified" if efq == "true" else "unqualified"}"'
|
||||
if afq is not None:
|
||||
schema_attrs += f' attributeFormDefault="{"qualified" if afq == "true" else "unqualified"}"'
|
||||
|
||||
X(f'<xs:schema{ns_decls}{attrs(["targetNamespace", target_ns])}{schema_attrs}>')
|
||||
|
||||
if meta:
|
||||
X("\t<xs:annotation>")
|
||||
X("\t\t<xs:appinfo>")
|
||||
X(f'\t\t\t<xdto:package xmlns:xdto="{XDTO_NS}">')
|
||||
if meta["Name"] is not None:
|
||||
X(f'\t\t\t\t<xdto:name>{esc_text(meta["Name"])}</xdto:name>')
|
||||
if meta["Comment"]:
|
||||
X(f'\t\t\t\t<xdto:comment>{esc_text(meta["Comment"])}</xdto:comment>')
|
||||
for s in meta["Synonym"]:
|
||||
X(f'\t\t\t\t<xdto:synonym lang="{esc(s["Lang"])}">{esc_text(s["Content"])}</xdto:synonym>')
|
||||
X("\t\t\t</xdto:package>")
|
||||
X("\t\t</xs:appinfo>")
|
||||
X("\t</xs:annotation>")
|
||||
|
||||
lines.extend(body_lines)
|
||||
X("</xs:schema>")
|
||||
|
||||
text = "\r\n".join(lines) + "\r\n"
|
||||
|
||||
if args.OutFile:
|
||||
d = os.path.dirname(args.OutFile)
|
||||
if d and not os.path.isdir(d):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
with open(args.OutFile, "wb") as f:
|
||||
f.write(b"\xef\xbb\xbf" + text.encode("utf-8"))
|
||||
print(f"✓ XSD записана: {args.OutFile}")
|
||||
print(f" targetNamespace: {target_ns}")
|
||||
else:
|
||||
sys.stdout.write(text)
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: xdto-validate
|
||||
description: Валидация пакета XDTO 1С. Используй после создания или модификации пакета XDTO для проверки корректности
|
||||
argument-hint: <PackagePath> [-ConfigDir <каталог>] [-Detailed] [-MaxErrors N] [-OutFile <файл>]
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /xdto-validate — Валидация пакета XDTO
|
||||
|
||||
Проверяет модель пакета, объект метаданных и его связь с конфигурацией.
|
||||
Exit code `1` при наличии ошибок.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Описание |
|
||||
|----------|----------|
|
||||
| `PackagePath` | Каталог пакета, `Ext/Package.bin` или `<Имя>.xml` объекта метаданных |
|
||||
| `ConfigDir` | Каталог конфигурации. По умолчанию определяется по расположению пакета |
|
||||
| `Detailed` | Показывать успешные проверки, а не только ошибки |
|
||||
| `MaxErrors` | Остановиться после N ошибок (по умолчанию 20) |
|
||||
| `OutFile` | Записать отчёт в файл |
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/xdto-validate.ps1" -PackagePath "<путь>"
|
||||
```
|
||||
|
||||
## Проверки
|
||||
|
||||
**Модель**
|
||||
- Package.bin — корректный XML с корнем `<package>`, задан `targetNamespace`, кодировка UTF-8 с BOM
|
||||
- Имена типов уникальны внутри пакета
|
||||
- Каждая ссылка (`type`, `base`, `ref`, `itemType`, `memberTypes`) разрешается: собственный
|
||||
тип, импортированное пространство имён или `xs:`. Поддержана нотация Кларка `{ns}local`
|
||||
- Префиксы объявлены (нет ссылок на необъявленный `dNpM`)
|
||||
- Фасеты согласованы: `length` не вместе с `minLength`/`maxLength`, `minLength ≤ maxLength`,
|
||||
`fractionDigits ≤ totalDigits`, допустимые `whiteSpace` и `variety`
|
||||
- Свойства: допустимая `form`, `lowerBound ≤ upperBound`, есть `name` либо `ref`
|
||||
|
||||
**Связь с конфигурацией**
|
||||
- Объект метаданных существует, `Name` совпадает с именем каталога, `Namespace` — с `targetNamespace`
|
||||
- Пакет зарегистрирован в `ChildObjects` файла `Configuration.xml`
|
||||
- `targetNamespace` уникален среди пакетов конфигурации
|
||||
|
||||
**Предупреждения о тихих дефектах**
|
||||
|
||||
Два класса проблем платформа не диагностирует — их ловит навык:
|
||||
|
||||
- **`xs:anyType` при объявленных импортах.** При импорте XML-схемы тип из чужого
|
||||
пространства имён, для которого в конфигурации нет пакета, заменяется на `xs:anyType`
|
||||
молча — ни ошибки, ни предупреждения. Пакет выглядит загруженным, а `ФабрикаXDTO`
|
||||
потом отдаёт бесструктурное значение. Сопутствующий признак — объявленный, но
|
||||
неиспользуемый `<import>`.
|
||||
- **`nillable="true"` вместе с `form="Attribute"`.** Спецификация XSD не допускает
|
||||
`nillable` у атрибутов, поэтому экспорт XML-схемы в Конфигураторе такое свойство теряет.
|
||||
На саму работу пакета не влияет, но ломает перенос схемы через Конфигуратор.
|
||||
|
||||
## Типичный workflow
|
||||
|
||||
1. `/xdto-compile` или `/xdto-decompile` → правка → `/xdto-compile -Force`
|
||||
2. `/xdto-validate <путь>` — до загрузки в базу
|
||||
3. `/db-load-xml` + `/db-update`
|
||||
|
||||
## Верификация
|
||||
|
||||
```
|
||||
/xdto-validate <путь> — только проблемы
|
||||
/xdto-validate <путь> -Detailed — все проверки
|
||||
```
|
||||
@@ -0,0 +1,402 @@
|
||||
# xdto-validate v1.0 — Validate a 1C XDTO package
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[Alias('Path')]
|
||||
[string]$PackagePath,
|
||||
|
||||
[string]$ConfigDir,
|
||||
|
||||
[switch]$Detailed,
|
||||
|
||||
[int]$MaxErrors = 20,
|
||||
|
||||
[string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
$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"
|
||||
|
||||
# --- Reporting ---
|
||||
|
||||
$script:errors = 0
|
||||
$script:warnings = 0
|
||||
$script:okCount = 0
|
||||
$script:stopped = $false
|
||||
$script:output = New-Object System.Text.StringBuilder
|
||||
|
||||
function Out-Line([string]$s) { [void]$script:output.AppendLine($s) }
|
||||
function Report-OK([string]$msg) {
|
||||
$script:okCount++
|
||||
if ($Detailed) { Out-Line "[OK] $msg" }
|
||||
}
|
||||
function Report-Error([string]$msg) {
|
||||
$script:errors++
|
||||
Out-Line "[ERROR] $msg"
|
||||
if ($script:errors -ge $MaxErrors) { $script:stopped = $true }
|
||||
}
|
||||
function Report-Warn([string]$msg) {
|
||||
$script:warnings++
|
||||
Out-Line "[WARN] $msg"
|
||||
}
|
||||
|
||||
# --- Resolve paths ---
|
||||
|
||||
if (-not [System.IO.Path]::IsPathRooted($PackagePath)) {
|
||||
$PackagePath = Join-Path (Get-Location).Path $PackagePath
|
||||
}
|
||||
|
||||
$binPath = $null
|
||||
$mdPath = $null
|
||||
if (Test-Path $PackagePath -PathType Leaf) {
|
||||
if ([System.IO.Path]::GetFileName($PackagePath) -eq "Package.bin") {
|
||||
$binPath = $PackagePath
|
||||
$pkgDir = [System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($PackagePath))
|
||||
if (Test-Path "$pkgDir.xml") { $mdPath = "$pkgDir.xml" }
|
||||
} elseif ($PackagePath.EndsWith(".xml")) {
|
||||
$mdPath = $PackagePath
|
||||
$stem = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($PackagePath), [System.IO.Path]::GetFileNameWithoutExtension($PackagePath))
|
||||
$c = Join-Path (Join-Path $stem "Ext") "Package.bin"
|
||||
if (Test-Path $c) { $binPath = $c }
|
||||
}
|
||||
} elseif (Test-Path $PackagePath -PathType Container) {
|
||||
$c = Join-Path (Join-Path $PackagePath "Ext") "Package.bin"
|
||||
if (Test-Path $c) {
|
||||
$binPath = $c
|
||||
$m = "$($PackagePath.TrimEnd('\','/')).xml"
|
||||
if (Test-Path $m) { $mdPath = $m }
|
||||
}
|
||||
}
|
||||
|
||||
$fileName = if ($binPath) { [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($binPath))) } else { $PackagePath }
|
||||
|
||||
if (-not $binPath) {
|
||||
Write-Host "[ERROR] Не найден Ext/Package.bin для пути: $PackagePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Каталог конфигурации — для проверок регистрации и зависимостей
|
||||
if (-not $ConfigDir) {
|
||||
$d = [System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($binPath)))
|
||||
# .../XDTOPackages/<Имя>/Ext/Package.bin -> .../XDTOPackages -> корень
|
||||
$ConfigDir = [System.IO.Path]::GetDirectoryName($d)
|
||||
}
|
||||
|
||||
$finalize = {
|
||||
$checks = $script:okCount + $script:errors + $script:warnings
|
||||
if ($script:errors -eq 0 -and $script:warnings -eq 0 -and -not $Detailed) {
|
||||
$result = "=== Validation OK: $fileName ($checks checks) ==="
|
||||
} else {
|
||||
Out-Line ""
|
||||
Out-Line "=== Result: $($script:errors) errors, $($script:warnings) warnings ($checks checks) ==="
|
||||
$result = $script:output.ToString()
|
||||
}
|
||||
Write-Host $result
|
||||
if ($OutFile) {
|
||||
$utf8Bom = New-Object System.Text.UTF8Encoding $true
|
||||
[System.IO.File]::WriteAllText($OutFile, $script:output.ToString(), $utf8Bom)
|
||||
}
|
||||
}
|
||||
|
||||
# --- 1. Well-formedness ---
|
||||
|
||||
$doc = New-Object System.Xml.XmlDocument
|
||||
try {
|
||||
$doc.Load($binPath)
|
||||
} catch {
|
||||
Report-Error "Package.bin не является корректным XML: $($_.Exception.Message)"
|
||||
& $finalize
|
||||
exit 1
|
||||
}
|
||||
$pkg = $doc.DocumentElement
|
||||
if ($pkg.get_LocalName() -ne "package") {
|
||||
Report-Error "Ожидался корневой <package>, найден <$($pkg.get_LocalName())>"
|
||||
& $finalize
|
||||
exit 1
|
||||
}
|
||||
Report-OK "Package.bin: корректный XML, корень <package>"
|
||||
|
||||
$targetNs = $pkg.GetAttribute("targetNamespace")
|
||||
if (-not $targetNs) {
|
||||
Report-Error "У <package> не задан targetNamespace"
|
||||
} else {
|
||||
Report-OK "targetNamespace: $targetNs"
|
||||
}
|
||||
|
||||
# --- 2. Encoding / EOL ---
|
||||
|
||||
$bytes = [System.IO.File]::ReadAllBytes($binPath)
|
||||
if (-not ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF)) {
|
||||
Report-Warn "Package.bin без BOM UTF-8 — платформа пишет файл с BOM"
|
||||
} else {
|
||||
Report-OK "Кодировка: UTF-8 с BOM"
|
||||
}
|
||||
|
||||
# --- 3. Collect declarations ---
|
||||
|
||||
$imports = New-Object System.Collections.ArrayList
|
||||
$localTypes = New-Object System.Collections.Generic.HashSet[string]
|
||||
$globalProps = New-Object System.Collections.Generic.HashSet[string]
|
||||
|
||||
foreach ($n in $pkg.ChildNodes) {
|
||||
if ($n.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
switch ($n.get_LocalName()) {
|
||||
"import" { [void]$imports.Add($n.GetAttribute("namespace")) }
|
||||
"objectType" { [void]$localTypes.Add($n.GetAttribute("name")) }
|
||||
"valueType" { [void]$localTypes.Add($n.GetAttribute("name")) }
|
||||
"property" { if ($n.HasAttribute("name")) { [void]$globalProps.Add($n.GetAttribute("name")) } }
|
||||
}
|
||||
}
|
||||
|
||||
# --- 4. Duplicate type names ---
|
||||
|
||||
$seen = @{}
|
||||
foreach ($n in $pkg.ChildNodes) {
|
||||
if ($n.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
if ($n.get_LocalName() -ne "objectType" -and $n.get_LocalName() -ne "valueType") { continue }
|
||||
$nm = $n.GetAttribute("name")
|
||||
if (-not $nm) { Report-Error "<$($n.get_LocalName())> без атрибута name"; continue }
|
||||
if ($seen.ContainsKey($nm)) {
|
||||
Report-Error "Дублирующееся имя типа: $nm"
|
||||
} else {
|
||||
$seen[$nm] = $true
|
||||
}
|
||||
}
|
||||
if ($localTypes.Count -gt 0) { Report-OK "$($localTypes.Count) тип(ов), имена уникальны" }
|
||||
|
||||
# --- 5. Type references resolve ---
|
||||
|
||||
$usedNamespaces = New-Object System.Collections.Generic.HashSet[string]
|
||||
$anyTypeProps = New-Object System.Collections.ArrayList
|
||||
$refAttrs = @("type", "base", "ref", "itemType")
|
||||
|
||||
function Resolve-Ref([System.Xml.XmlElement]$el, [string]$attr, [string]$raw) {
|
||||
if (-not $raw) { return }
|
||||
# Нотация Кларка {ns}local
|
||||
if ($raw.StartsWith("{")) {
|
||||
$close = $raw.IndexOf("}")
|
||||
if ($close -lt 0) { Report-Error "Некорректная нотация Кларка в $attr=`"$raw`""; return }
|
||||
$ns = $raw.Substring(1, $close - 1)
|
||||
$local = $raw.Substring($close + 1)
|
||||
} else {
|
||||
$parts = $raw.Split(":")
|
||||
if ($parts.Count -eq 2) {
|
||||
$ns = $el.GetNamespaceOfPrefix($parts[0])
|
||||
$local = $parts[1]
|
||||
if (-not $ns) {
|
||||
Report-Error "Префикс `"$($parts[0])`" не объявлен: $attr=`"$raw`" (тип $($el.get_LocalName()))"
|
||||
return
|
||||
}
|
||||
} else {
|
||||
$ns = $null
|
||||
$local = $parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
if ($ns -eq $XS_NS -or $ns -eq $XSI_NS) {
|
||||
if ($local -eq "anyType") { [void]$anyTypeProps.Add($el) }
|
||||
return
|
||||
}
|
||||
if ($ns -eq $targetNs) {
|
||||
if ($attr -eq "ref") {
|
||||
if (-not $globalProps.Contains($local)) {
|
||||
Report-Error "ref=`"$raw`" не разрешается: в пакете нет глобального свойства `"$local`""
|
||||
}
|
||||
} elseif (-not $localTypes.Contains($local)) {
|
||||
Report-Error "$attr=`"$raw`" не разрешается: в пакете нет типа `"$local`""
|
||||
}
|
||||
return
|
||||
}
|
||||
if ($ns) {
|
||||
[void]$usedNamespaces.Add($ns)
|
||||
if (-not $imports.Contains($ns)) {
|
||||
Report-Error "$attr=`"$raw`" ссылается на `"$ns`", но <import namespace=`"$ns`"/> не объявлен"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$nodeCount = 0
|
||||
foreach ($el in $pkg.SelectNodes("//*")) {
|
||||
if ($el.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
$nodeCount++
|
||||
foreach ($a in $refAttrs) {
|
||||
if ($el.HasAttribute($a)) { Resolve-Ref $el $a $el.GetAttribute($a) }
|
||||
}
|
||||
if ($el.HasAttribute("memberTypes")) {
|
||||
foreach ($m in ($el.GetAttribute("memberTypes") -split "\s+")) {
|
||||
if ($m) { Resolve-Ref $el "memberTypes" $m }
|
||||
}
|
||||
}
|
||||
if ($script:stopped) { break }
|
||||
}
|
||||
if (-not $script:stopped) { Report-OK "$nodeCount узлов: ссылки на типы разрешаются" }
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- 6. Silent degradation to xs:anyType ---
|
||||
# Платформа при импорте XML-схемы молча подменяет неразрешённый чужой тип на anyType.
|
||||
|
||||
if ($anyTypeProps.Count -gt 0 -and $imports.Count -gt 0) {
|
||||
$names = @()
|
||||
foreach ($p in $anyTypeProps) { if ($p.HasAttribute("name")) { $names += $p.GetAttribute("name") } }
|
||||
$shown = ($names | Select-Object -First 5) -join ", "
|
||||
Report-Warn "Свойств с type=`"xs:anyType`": $($anyTypeProps.Count) при объявленных импортах ($shown). Возможна тихая деградация: при импорте XML-схемы платформа заменяет неразрешённый чужой тип на anyType без ошибки"
|
||||
}
|
||||
|
||||
# --- 7. Unused imports ---
|
||||
|
||||
foreach ($imp in $imports) {
|
||||
if (-not $usedNamespaces.Contains($imp)) {
|
||||
Report-Warn "<import namespace=`"$imp`"/> объявлен, но ни один тип из этого пространства имён не используется"
|
||||
}
|
||||
}
|
||||
if ($imports.Count -gt 0 -and $script:warnings -eq 0) { Report-OK "$($imports.Count) импорт(ов) — все используются" }
|
||||
|
||||
# --- 8. nillable on attribute-form properties ---
|
||||
|
||||
$nillAttrs = @()
|
||||
foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) {
|
||||
if ($p.GetAttribute("form") -eq "Attribute" -and $p.GetAttribute("nillable") -eq "true") {
|
||||
$nillAttrs += $p.GetAttribute("name")
|
||||
}
|
||||
}
|
||||
if ($nillAttrs.Count -gt 0) {
|
||||
Report-Warn "Свойств с nillable=`"true`" и form=`"Attribute`": $($nillAttrs.Count) ($(($nillAttrs | Select-Object -First 5) -join ', ')). Спецификация XSD не допускает nillable у атрибутов — экспорт XML-схемы в Конфигураторе их потеряет"
|
||||
}
|
||||
|
||||
# --- 9. Facet consistency ---
|
||||
|
||||
$FACET_NUM = @("totalDigits", "fractionDigits")
|
||||
$facetChecked = 0
|
||||
foreach ($t in $pkg.SelectNodes("//*[local-name()='valueType' or local-name()='typeDef']")) {
|
||||
if ($t.get_LocalName() -eq "typeDef" -and $t.GetAttribute("type", $XSI_NS) -eq "ObjectType") { continue }
|
||||
$facetChecked++
|
||||
$nm = if ($t.HasAttribute("name")) { $t.GetAttribute("name") } else { "(анонимный тип)" }
|
||||
|
||||
$len = $t.GetAttribute("length")
|
||||
if ($len -and ($t.HasAttribute("minLength") -or $t.HasAttribute("maxLength"))) {
|
||||
Report-Error "$nm : length несовместим с minLength/maxLength"
|
||||
}
|
||||
$minL = $t.GetAttribute("minLength"); $maxL = $t.GetAttribute("maxLength")
|
||||
if ($minL -and $maxL -and ([int]$minL -gt [int]$maxL)) {
|
||||
Report-Error "$nm : minLength ($minL) больше maxLength ($maxL)"
|
||||
}
|
||||
$td = $t.GetAttribute("totalDigits"); $fd = $t.GetAttribute("fractionDigits")
|
||||
if ($td -and $fd -and ([int]$fd -gt [int]$td)) {
|
||||
Report-Error "$nm : fractionDigits ($fd) больше totalDigits ($td)"
|
||||
}
|
||||
$ws = $t.GetAttribute("whiteSpace")
|
||||
if ($ws -and @("preserve", "replace", "collapse") -notcontains $ws) {
|
||||
Report-Error "$nm : недопустимое whiteSpace=`"$ws`""
|
||||
}
|
||||
$var = $t.GetAttribute("variety")
|
||||
if ($var -and @("Atomic", "List", "Union") -notcontains $var) {
|
||||
Report-Error "$nm : недопустимое variety=`"$var`""
|
||||
}
|
||||
if ($var -eq "List" -and -not $t.HasAttribute("itemType")) {
|
||||
Report-Warn "$nm : variety=`"List`" без itemType"
|
||||
}
|
||||
if ($script:stopped) { break }
|
||||
}
|
||||
if ($facetChecked -gt 0 -and -not $script:stopped) { Report-OK "$facetChecked простых тип(ов): фасеты согласованы" }
|
||||
|
||||
if ($script:stopped) { & $finalize; exit 1 }
|
||||
|
||||
# --- 10. property form / bounds ---
|
||||
|
||||
foreach ($p in $pkg.SelectNodes("//*[local-name()='property']")) {
|
||||
$form = $p.GetAttribute("form")
|
||||
if ($form -and @("Element", "Attribute", "Text") -notcontains $form) {
|
||||
Report-Error "Свойство `"$($p.GetAttribute('name'))`": недопустимое form=`"$form`""
|
||||
}
|
||||
$ub = $p.GetAttribute("upperBound")
|
||||
if ($ub -and $ub -ne "-1" -and ([int]$ub -lt 1)) {
|
||||
Report-Error "Свойство `"$($p.GetAttribute('name'))`": upperBound=`"$ub`" (допустимы -1 или число ≥ 1)"
|
||||
}
|
||||
$lb = $p.GetAttribute("lowerBound")
|
||||
if ($lb -and $ub -and $ub -ne "-1" -and ([int]$lb -gt [int]$ub)) {
|
||||
Report-Error "Свойство `"$($p.GetAttribute('name'))`": lowerBound ($lb) больше upperBound ($ub)"
|
||||
}
|
||||
if (-not $p.HasAttribute("name") -and -not $p.HasAttribute("ref")) {
|
||||
Report-Error "Свойство без name и без ref"
|
||||
}
|
||||
if ($script:stopped) { break }
|
||||
}
|
||||
if (-not $script:stopped) { Report-OK "Свойства: form и кратности корректны" }
|
||||
|
||||
# --- 11. Metadata object ---
|
||||
|
||||
if ($mdPath) {
|
||||
$md = New-Object System.Xml.XmlDocument
|
||||
$md.Load($mdPath)
|
||||
$nsm = New-Object System.Xml.XmlNamespaceManager($md.NameTable)
|
||||
$nsm.AddNamespace("md", $MD_NS)
|
||||
$mdName = $md.SelectSingleNode("//md:XDTOPackage/md:Properties/md:Name", $nsm)
|
||||
$mdNs = $md.SelectSingleNode("//md:XDTOPackage/md:Properties/md:Namespace", $nsm)
|
||||
if (-not $mdName) {
|
||||
Report-Error "В объекте метаданных не задано <Name>"
|
||||
} elseif ($mdName.InnerText -ne $fileName) {
|
||||
Report-Error "<Name>$($mdName.InnerText)</Name> не совпадает с именем каталога `"$fileName`""
|
||||
} else {
|
||||
Report-OK "Объект метаданных: Name = $($mdName.InnerText)"
|
||||
}
|
||||
if ($mdNs -and $mdNs.InnerText -ne $targetNs) {
|
||||
Report-Error "<Namespace>$($mdNs.InnerText)</Namespace> не совпадает с targetNamespace пакета ($targetNs)"
|
||||
} elseif ($mdNs) {
|
||||
Report-OK "Namespace объекта метаданных совпадает с targetNamespace"
|
||||
}
|
||||
} else {
|
||||
Report-Warn "Файл объекта метаданных <Имя>.xml не найден рядом с каталогом пакета"
|
||||
}
|
||||
|
||||
# --- 12. Registration in Configuration.xml + namespace uniqueness ---
|
||||
|
||||
$configXml = Join-Path $ConfigDir "Configuration.xml"
|
||||
if (Test-Path $configXml) {
|
||||
$cfg = New-Object System.Xml.XmlDocument
|
||||
$cfg.Load($configXml)
|
||||
$nsm2 = New-Object System.Xml.XmlNamespaceManager($cfg.NameTable)
|
||||
$nsm2.AddNamespace("md", $MD_NS)
|
||||
$registered = $false
|
||||
foreach ($e in $cfg.SelectNodes("//md:Configuration/md:ChildObjects/md:XDTOPackage", $nsm2)) {
|
||||
if ($e.InnerText -eq $fileName) { $registered = $true; break }
|
||||
}
|
||||
if ($registered) {
|
||||
Report-OK "Зарегистрирован в Configuration.xml"
|
||||
} else {
|
||||
Report-Error "<XDTOPackage>$fileName</XDTOPackage> отсутствует в ChildObjects файла Configuration.xml — платформа пакет не увидит"
|
||||
}
|
||||
|
||||
# Уникальность targetNamespace среди пакетов конфигурации
|
||||
$pkgRoot = Join-Path $ConfigDir "XDTOPackages"
|
||||
if (Test-Path $pkgRoot) {
|
||||
$clash = @()
|
||||
foreach ($other in (Get-ChildItem $pkgRoot -Directory -ErrorAction SilentlyContinue)) {
|
||||
if ($other.Name -eq $fileName) { continue }
|
||||
$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)
|
||||
if ($od.DocumentElement.GetAttribute("targetNamespace") -eq $targetNs) { $clash += $other.Name }
|
||||
} catch {}
|
||||
}
|
||||
if ($clash.Count -gt 0) {
|
||||
Report-Error "targetNamespace `"$targetNs`" уже занят пакет(ами): $($clash -join ', '). Платформа не допускает два пакета с одним пространством имён"
|
||||
} else {
|
||||
Report-OK "targetNamespace уникален в конфигурации"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Report-Warn "Configuration.xml не найден ($ConfigDir) — проверки регистрации и уникальности namespace пропущены"
|
||||
}
|
||||
|
||||
# --- Final ---
|
||||
|
||||
& $finalize
|
||||
if ($script:errors -gt 0) { exit 1 }
|
||||
exit 0
|
||||
@@ -0,0 +1,386 @@
|
||||
# xdto-validate v1.0 — Validate a 1C XDTO package (Python port)
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from lxml import etree
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
|
||||
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"
|
||||
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument("-PackagePath", "-Path", required=True)
|
||||
parser.add_argument("-ConfigDir", default="")
|
||||
parser.add_argument("-Detailed", action="store_true")
|
||||
parser.add_argument("-MaxErrors", type=int, default=20)
|
||||
parser.add_argument("-OutFile", default="")
|
||||
args = parser.parse_args()
|
||||
|
||||
package_path = os.path.abspath(args.PackagePath)
|
||||
detailed = args.Detailed
|
||||
max_errors = args.MaxErrors
|
||||
out_file = args.OutFile
|
||||
|
||||
# ── reporting ────────────────────────────────────────────────
|
||||
|
||||
state = {"errors": 0, "warnings": 0, "ok": 0, "stopped": False}
|
||||
output = []
|
||||
|
||||
|
||||
def out_line(s):
|
||||
output.append(s)
|
||||
|
||||
|
||||
def report_ok(msg):
|
||||
state["ok"] += 1
|
||||
if detailed:
|
||||
out_line(f"[OK] {msg}")
|
||||
|
||||
|
||||
def report_error(msg):
|
||||
state["errors"] += 1
|
||||
out_line(f"[ERROR] {msg}")
|
||||
if state["errors"] >= max_errors:
|
||||
state["stopped"] = True
|
||||
|
||||
|
||||
def report_warn(msg):
|
||||
state["warnings"] += 1
|
||||
out_line(f"[WARN] {msg}")
|
||||
|
||||
|
||||
# ── resolve paths ────────────────────────────────────────────
|
||||
|
||||
bin_path = None
|
||||
md_path = None
|
||||
|
||||
if os.path.isfile(package_path):
|
||||
if os.path.basename(package_path) == "Package.bin":
|
||||
bin_path = package_path
|
||||
pkg_dir = os.path.dirname(os.path.dirname(package_path))
|
||||
if os.path.exists(pkg_dir + ".xml"):
|
||||
md_path = pkg_dir + ".xml"
|
||||
elif package_path.endswith(".xml"):
|
||||
md_path = package_path
|
||||
stem = os.path.join(os.path.dirname(package_path),
|
||||
os.path.splitext(os.path.basename(package_path))[0])
|
||||
c = os.path.join(stem, "Ext", "Package.bin")
|
||||
if os.path.exists(c):
|
||||
bin_path = c
|
||||
elif os.path.isdir(package_path):
|
||||
c = os.path.join(package_path, "Ext", "Package.bin")
|
||||
if os.path.exists(c):
|
||||
bin_path = c
|
||||
m = package_path.rstrip("\\/") + ".xml"
|
||||
if os.path.exists(m):
|
||||
md_path = m
|
||||
|
||||
if not bin_path:
|
||||
print(f"[ERROR] Не найден Ext/Package.bin для пути: {package_path}")
|
||||
sys.exit(1)
|
||||
|
||||
file_name = os.path.basename(os.path.dirname(os.path.dirname(bin_path)))
|
||||
|
||||
config_dir = args.ConfigDir
|
||||
if not config_dir:
|
||||
# .../XDTOPackages/<Имя>/Ext/Package.bin -> .../XDTOPackages -> корень
|
||||
config_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(bin_path))))
|
||||
|
||||
|
||||
def finalize():
|
||||
checks = state["ok"] + state["errors"] + state["warnings"]
|
||||
if state["errors"] == 0 and state["warnings"] == 0 and not detailed:
|
||||
result = f"=== Validation OK: {file_name} ({checks} checks) ==="
|
||||
else:
|
||||
out_line("")
|
||||
out_line(f"=== Result: {state['errors']} errors, {state['warnings']} warnings ({checks} checks) ===")
|
||||
result = "\n".join(output)
|
||||
print(result)
|
||||
if out_file:
|
||||
with open(out_file, "w", encoding="utf-8-sig", newline="") as f:
|
||||
f.write("\n".join(output))
|
||||
|
||||
|
||||
def local(el):
|
||||
return etree.QName(el).localname
|
||||
|
||||
|
||||
# ── 1. well-formedness ───────────────────────────────────────
|
||||
|
||||
try:
|
||||
doc = etree.parse(bin_path)
|
||||
except Exception as e: # noqa: BLE001
|
||||
report_error(f"Package.bin не является корректным XML: {e}")
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
|
||||
pkg = doc.getroot()
|
||||
if local(pkg) != "package":
|
||||
report_error(f"Ожидался корневой <package>, найден <{local(pkg)}>")
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
report_ok("Package.bin: корректный XML, корень <package>")
|
||||
|
||||
target_ns = pkg.get("targetNamespace")
|
||||
if not target_ns:
|
||||
report_error("У <package> не задан targetNamespace")
|
||||
else:
|
||||
report_ok(f"targetNamespace: {target_ns}")
|
||||
|
||||
# ── 2. encoding ──────────────────────────────────────────────
|
||||
|
||||
with open(bin_path, "rb") as f:
|
||||
head = f.read(3)
|
||||
if head != b"\xef\xbb\xbf":
|
||||
report_warn("Package.bin без BOM UTF-8 — платформа пишет файл с BOM")
|
||||
else:
|
||||
report_ok("Кодировка: UTF-8 с BOM")
|
||||
|
||||
# ── 3. declarations ──────────────────────────────────────────
|
||||
|
||||
imports = []
|
||||
local_types = set()
|
||||
global_props = set()
|
||||
|
||||
for n in pkg:
|
||||
if not isinstance(n.tag, str):
|
||||
continue
|
||||
ln = local(n)
|
||||
if ln == "import":
|
||||
imports.append(n.get("namespace"))
|
||||
elif ln in ("objectType", "valueType"):
|
||||
local_types.add(n.get("name"))
|
||||
elif ln == "property" and n.get("name"):
|
||||
global_props.add(n.get("name"))
|
||||
|
||||
# ── 4. duplicate type names ──────────────────────────────────
|
||||
|
||||
seen = set()
|
||||
for n in pkg:
|
||||
if not isinstance(n.tag, str) or local(n) not in ("objectType", "valueType"):
|
||||
continue
|
||||
nm = n.get("name")
|
||||
if not nm:
|
||||
report_error(f"<{local(n)}> без атрибута name")
|
||||
continue
|
||||
if nm in seen:
|
||||
report_error(f"Дублирующееся имя типа: {nm}")
|
||||
else:
|
||||
seen.add(nm)
|
||||
if local_types:
|
||||
report_ok(f"{len(local_types)} тип(ов), имена уникальны")
|
||||
|
||||
# ── 5. type references resolve ───────────────────────────────
|
||||
|
||||
used_namespaces = set()
|
||||
any_type_props = []
|
||||
REF_ATTRS = ("type", "base", "ref", "itemType")
|
||||
|
||||
|
||||
def resolve_ref(el, attr, raw):
|
||||
if not raw:
|
||||
return
|
||||
if raw.startswith("{"):
|
||||
close = raw.find("}")
|
||||
if close < 0:
|
||||
report_error(f'Некорректная нотация Кларка в {attr}="{raw}"')
|
||||
return
|
||||
ns = raw[1:close]
|
||||
loc = raw[close + 1:]
|
||||
else:
|
||||
parts = raw.split(":")
|
||||
if len(parts) == 2:
|
||||
ns = el.nsmap.get(parts[0])
|
||||
loc = parts[1]
|
||||
if not ns:
|
||||
report_error(f'Префикс "{parts[0]}" не объявлен: {attr}="{raw}" (тип {local(el)})')
|
||||
return
|
||||
else:
|
||||
ns = None
|
||||
loc = parts[0]
|
||||
|
||||
if ns in (XS_NS, XSI_NS):
|
||||
if loc == "anyType":
|
||||
any_type_props.append(el)
|
||||
return
|
||||
if ns == target_ns:
|
||||
if attr == "ref":
|
||||
if loc not in global_props:
|
||||
report_error(f'ref="{raw}" не разрешается: в пакете нет глобального свойства "{loc}"')
|
||||
elif loc not in local_types:
|
||||
report_error(f'{attr}="{raw}" не разрешается: в пакете нет типа "{loc}"')
|
||||
return
|
||||
if ns:
|
||||
used_namespaces.add(ns)
|
||||
if ns not in imports:
|
||||
report_error(f'{attr}="{raw}" ссылается на "{ns}", но <import namespace="{ns}"/> не объявлен')
|
||||
|
||||
|
||||
node_count = 0
|
||||
for el in pkg.iter():
|
||||
if not isinstance(el.tag, str):
|
||||
continue
|
||||
node_count += 1
|
||||
for a in REF_ATTRS:
|
||||
if el.get(a) is not None:
|
||||
resolve_ref(el, a, el.get(a))
|
||||
if el.get("memberTypes"):
|
||||
for m in el.get("memberTypes").split():
|
||||
resolve_ref(el, "memberTypes", m)
|
||||
if state["stopped"]:
|
||||
break
|
||||
if not state["stopped"]:
|
||||
report_ok(f"{node_count} узлов: ссылки на типы разрешаются")
|
||||
|
||||
if state["stopped"]:
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
|
||||
# ── 6. silent degradation to xs:anyType ──────────────────────
|
||||
|
||||
if any_type_props and imports:
|
||||
names = [p.get("name") for p in any_type_props if p.get("name")]
|
||||
shown = ", ".join(names[:5])
|
||||
report_warn(
|
||||
f'Свойств с type="xs:anyType": {len(any_type_props)} при объявленных импортах ({shown}). '
|
||||
"Возможна тихая деградация: при импорте XML-схемы платформа заменяет неразрешённый "
|
||||
"чужой тип на anyType без ошибки"
|
||||
)
|
||||
|
||||
# ── 7. unused imports ────────────────────────────────────────
|
||||
|
||||
for imp in imports:
|
||||
if imp not in used_namespaces:
|
||||
report_warn(f'<import namespace="{imp}"/> объявлен, но ни один тип из этого пространства имён не используется')
|
||||
if imports and state["warnings"] == 0:
|
||||
report_ok(f"{len(imports)} импорт(ов) — все используются")
|
||||
|
||||
# ── 8. nillable on attribute-form properties ─────────────────
|
||||
|
||||
nill_attrs = [p.get("name") for p in pkg.iter()
|
||||
if isinstance(p.tag, str) and local(p) == "property"
|
||||
and p.get("form") == "Attribute" and p.get("nillable") == "true"]
|
||||
if nill_attrs:
|
||||
report_warn(
|
||||
f'Свойств с nillable="true" и form="Attribute": {len(nill_attrs)} ({", ".join(nill_attrs[:5])}). '
|
||||
"Спецификация XSD не допускает nillable у атрибутов — экспорт XML-схемы в Конфигураторе их потеряет"
|
||||
)
|
||||
|
||||
# ── 9. facet consistency ─────────────────────────────────────
|
||||
|
||||
facet_checked = 0
|
||||
for t in pkg.iter():
|
||||
if not isinstance(t.tag, str) or local(t) not in ("valueType", "typeDef"):
|
||||
continue
|
||||
if local(t) == "typeDef" and t.get(f"{{{XSI_NS}}}type") == "ObjectType":
|
||||
continue
|
||||
facet_checked += 1
|
||||
nm = t.get("name") or "(анонимный тип)"
|
||||
|
||||
if t.get("length") and (t.get("minLength") or t.get("maxLength")):
|
||||
report_error(f"{nm} : length несовместим с minLength/maxLength")
|
||||
min_l, max_l = t.get("minLength"), t.get("maxLength")
|
||||
if min_l and max_l and int(min_l) > int(max_l):
|
||||
report_error(f"{nm} : minLength ({min_l}) больше maxLength ({max_l})")
|
||||
td, fd = t.get("totalDigits"), t.get("fractionDigits")
|
||||
if td and fd and int(fd) > int(td):
|
||||
report_error(f"{nm} : fractionDigits ({fd}) больше totalDigits ({td})")
|
||||
ws = t.get("whiteSpace")
|
||||
if ws and ws not in ("preserve", "replace", "collapse"):
|
||||
report_error(f'{nm} : недопустимое whiteSpace="{ws}"')
|
||||
var = t.get("variety")
|
||||
if var and var not in ("Atomic", "List", "Union"):
|
||||
report_error(f'{nm} : недопустимое variety="{var}"')
|
||||
if var == "List" and t.get("itemType") is None:
|
||||
report_warn(f'{nm} : variety="List" без itemType')
|
||||
if state["stopped"]:
|
||||
break
|
||||
if facet_checked and not state["stopped"]:
|
||||
report_ok(f"{facet_checked} простых тип(ов): фасеты согласованы")
|
||||
|
||||
if state["stopped"]:
|
||||
finalize()
|
||||
sys.exit(1)
|
||||
|
||||
# ── 10. property form / bounds ───────────────────────────────
|
||||
|
||||
for p in pkg.iter():
|
||||
if not isinstance(p.tag, str) or local(p) != "property":
|
||||
continue
|
||||
form = p.get("form")
|
||||
if form and form not in ("Element", "Attribute", "Text"):
|
||||
report_error(f'Свойство "{p.get("name")}": недопустимое form="{form}"')
|
||||
ub = p.get("upperBound")
|
||||
if ub and ub != "-1" and int(ub) < 1:
|
||||
report_error(f'Свойство "{p.get("name")}": upperBound="{ub}" (допустимы -1 или число ≥ 1)')
|
||||
lb = p.get("lowerBound")
|
||||
if lb and ub and ub != "-1" and int(lb) > int(ub):
|
||||
report_error(f'Свойство "{p.get("name")}": lowerBound ({lb}) больше upperBound ({ub})')
|
||||
if p.get("name") is None and p.get("ref") is None:
|
||||
report_error("Свойство без name и без ref")
|
||||
if state["stopped"]:
|
||||
break
|
||||
if not state["stopped"]:
|
||||
report_ok("Свойства: form и кратности корректны")
|
||||
|
||||
# ── 11. metadata object ──────────────────────────────────────
|
||||
|
||||
if md_path:
|
||||
md = etree.parse(md_path)
|
||||
md_name = md.find(f".//{{{MD_NS}}}XDTOPackage/{{{MD_NS}}}Properties/{{{MD_NS}}}Name")
|
||||
md_ns = md.find(f".//{{{MD_NS}}}XDTOPackage/{{{MD_NS}}}Properties/{{{MD_NS}}}Namespace")
|
||||
if md_name is None:
|
||||
report_error("В объекте метаданных не задано <Name>")
|
||||
elif (md_name.text or "") != file_name:
|
||||
report_error(f'<Name>{md_name.text}</Name> не совпадает с именем каталога "{file_name}"')
|
||||
else:
|
||||
report_ok(f"Объект метаданных: Name = {md_name.text}")
|
||||
if md_ns is not None and (md_ns.text or "") != target_ns:
|
||||
report_error(f"<Namespace>{md_ns.text}</Namespace> не совпадает с targetNamespace пакета ({target_ns})")
|
||||
elif md_ns is not None:
|
||||
report_ok("Namespace объекта метаданных совпадает с targetNamespace")
|
||||
else:
|
||||
report_warn("Файл объекта метаданных <Имя>.xml не найден рядом с каталогом пакета")
|
||||
|
||||
# ── 12. registration + namespace uniqueness ──────────────────
|
||||
|
||||
config_xml = os.path.join(config_dir, "Configuration.xml")
|
||||
if os.path.exists(config_xml):
|
||||
cfg = etree.parse(config_xml)
|
||||
registered = any((e.text or "") == file_name
|
||||
for e in cfg.iterfind(f".//{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects/{{{MD_NS}}}XDTOPackage"))
|
||||
if registered:
|
||||
report_ok("Зарегистрирован в Configuration.xml")
|
||||
else:
|
||||
report_error(f"<XDTOPackage>{file_name}</XDTOPackage> отсутствует в ChildObjects файла "
|
||||
"Configuration.xml — платформа пакет не увидит")
|
||||
|
||||
pkg_root = os.path.join(config_dir, "XDTOPackages")
|
||||
if os.path.isdir(pkg_root):
|
||||
clash = []
|
||||
for other in sorted(os.listdir(pkg_root)):
|
||||
if other == file_name:
|
||||
continue
|
||||
ob = os.path.join(pkg_root, other, "Ext", "Package.bin")
|
||||
if not os.path.exists(ob):
|
||||
continue
|
||||
try:
|
||||
if etree.parse(ob).getroot().get("targetNamespace") == target_ns:
|
||||
clash.append(other)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
if clash:
|
||||
report_error(f'targetNamespace "{target_ns}" уже занят пакет(ами): {", ".join(clash)}. '
|
||||
"Платформа не допускает два пакета с одним пространством имён")
|
||||
else:
|
||||
report_ok("targetNamespace уникален в конфигурации")
|
||||
else:
|
||||
report_warn(f"Configuration.xml не найден ({config_dir}) — проверки регистрации и уникальности namespace пропущены")
|
||||
|
||||
finalize()
|
||||
sys.exit(1 if state["errors"] > 0 else 0)
|
||||
Reference in New Issue
Block a user