mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-12 08:54:57 +03:00
Add mxl-info and mxl-validate skills for SpreadsheetDocument analysis
- /mxl-info: extracts compact template structure (areas, parameters, column sets) from Template.xml. Supports -WithText for cell content, -Format json, and output truncation protection. - /mxl-validate: 12 structural checks (format/font/line indices, column bounds per column set, named area ranges, merge bounds, columnsID references). Exit code 1 on errors. Tested on 3 real templates: label (simple), invoice (medium), УКД (complex — 7 column sets, Rectangle areas). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
---
|
||||
name: mxl-info
|
||||
description: Analyze SpreadsheetDocument (MXL) template structure — areas, parameters, column sets
|
||||
argument-hint: <TemplatePath> or <ProcessorName> <TemplateName>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /mxl-info — Template Structure Analyzer
|
||||
|
||||
Reads a SpreadsheetDocument Template.xml and outputs a compact summary: named areas, parameters, column sets. Replaces the need to read thousands of XML lines.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/mxl-info <TemplatePath>
|
||||
/mxl-info <ProcessorName> <TemplateName>
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|---------------|:--------:|---------|------------------------------------------|
|
||||
| TemplatePath | no | — | Direct path to Template.xml |
|
||||
| ProcessorName | no | — | Processor name (alternative to path) |
|
||||
| TemplateName | no | — | Template name (alternative to path) |
|
||||
| SrcDir | no | `src` | Source directory |
|
||||
| Format | no | `text` | Output format: `text` or `json` |
|
||||
| WithText | no | false | Include static text and template content |
|
||||
| MaxParams | no | 10 | Max parameters listed per area |
|
||||
| Limit | no | 150 | Max output lines (truncation protection) |
|
||||
| Offset | no | 0 | Skip N lines (for pagination) |
|
||||
|
||||
Specify either `-TemplatePath` or both `-ProcessorName` and `-TemplateName`.
|
||||
|
||||
## Command
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/mxl-info/scripts/mxl-info.ps1 -TemplatePath "<path>"
|
||||
```
|
||||
|
||||
Or with processor/template names:
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/mxl-info/scripts/mxl-info.ps1 -ProcessorName "<Name>" -TemplateName "<Template>" [-SrcDir "<dir>"]
|
||||
```
|
||||
|
||||
Additional flags:
|
||||
```powershell
|
||||
... -WithText # include cell text content
|
||||
... -Format json # JSON output for programmatic use
|
||||
... -MaxParams 20 # show more parameters per area
|
||||
... -Offset 150 # pagination: skip first 150 lines
|
||||
```
|
||||
|
||||
## Output (text mode)
|
||||
|
||||
```
|
||||
=== TemplName ===
|
||||
Rows: 40, Columns: 33
|
||||
Column sets: 1 (default only)
|
||||
|
||||
--- Named areas ---
|
||||
Заголовок Rows rows 1-4 (1 params)
|
||||
Строка Rows rows 14-14 (8 params)
|
||||
Итого Rows rows 16-17 (1 params)
|
||||
|
||||
--- Parameters by area ---
|
||||
Заголовок: ТекстЗаголовка
|
||||
Строка: НомерСтроки, Товар, Количество, Цена, Сумма, ... (+3)
|
||||
Итого: Всего
|
||||
|
||||
--- Stats ---
|
||||
Merges: 43
|
||||
Drawings: 0
|
||||
```
|
||||
|
||||
With `-WithText`, adds a section showing static text (labels, headers) and template strings:
|
||||
|
||||
```
|
||||
--- Text content ---
|
||||
ШапкаТаблицы:
|
||||
Text: "№", "Товар", "Ед. изм.", "Кол-во", "Цена", "Сумма"
|
||||
Строка:
|
||||
Templates: "[НомерСтроки]", "[Товар] ([Артикул])"
|
||||
```
|
||||
|
||||
## When to Use
|
||||
|
||||
- **Before writing fill code**: run `/mxl-info` to understand the template structure, then write BSL code based on area names and parameter lists
|
||||
- **With `-WithText`**: when you need context about what labels/headers surround the parameters
|
||||
- **With `-Format json`**: when you need structured data for programmatic processing
|
||||
- **For existing templates**: analyze uploaded or configuration templates without reading raw XML
|
||||
|
||||
## Truncation Protection
|
||||
|
||||
Output is limited to 150 lines by default. If exceeded:
|
||||
```
|
||||
[TRUNCATED] Shown 150 of 220 lines. Use -Offset 150 to continue.
|
||||
```
|
||||
|
||||
Use `-Offset N` and `-Limit N` to paginate through large outputs.
|
||||
@@ -0,0 +1,417 @@
|
||||
param(
|
||||
[string]$TemplatePath,
|
||||
[string]$ProcessorName,
|
||||
[string]$TemplateName,
|
||||
[string]$SrcDir = "src",
|
||||
[ValidateSet("text", "json")]
|
||||
[string]$Format = "text",
|
||||
[switch]$WithText,
|
||||
[int]$MaxParams = 10,
|
||||
[int]$Limit = 150,
|
||||
[int]$Offset = 0
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve template path ---
|
||||
|
||||
if (-not $TemplatePath) {
|
||||
if (-not $ProcessorName -or -not $TemplateName) {
|
||||
Write-Error "Specify -TemplatePath or both -ProcessorName and -TemplateName"
|
||||
exit 1
|
||||
}
|
||||
$TemplatePath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $SrcDir $ProcessorName) "Templates") $TemplateName) "Ext") "Template.xml"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $TemplatePath)) {
|
||||
Write-Error "File not found: $TemplatePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Load XML ---
|
||||
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $false
|
||||
$xmlDoc.Load((Resolve-Path $TemplatePath).Path)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$nsMgr.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet")
|
||||
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
|
||||
$root = $xmlDoc.DocumentElement
|
||||
|
||||
# --- Column sets ---
|
||||
|
||||
$columnSets = @()
|
||||
$defaultColCount = 0
|
||||
|
||||
foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) {
|
||||
$sizeNode = $cols.SelectSingleNode("d:size", $nsMgr)
|
||||
$idNode = $cols.SelectSingleNode("d:id", $nsMgr)
|
||||
$size = if ($sizeNode) { [int]$sizeNode.InnerText } else { 0 }
|
||||
|
||||
if ($idNode) {
|
||||
$columnSets += @{ Id = $idNode.InnerText; Size = $size }
|
||||
} else {
|
||||
$defaultColCount = $size
|
||||
}
|
||||
}
|
||||
|
||||
# --- Rows: collect row data ---
|
||||
|
||||
$rowNodes = $root.SelectNodes("d:rowsItem", $nsMgr)
|
||||
$totalRows = $rowNodes.Count
|
||||
|
||||
$heightNode = $root.SelectSingleNode("d:height", $nsMgr)
|
||||
$docHeight = if ($heightNode) { [int]$heightNode.InnerText } else { $totalRows }
|
||||
|
||||
# --- Named items ---
|
||||
|
||||
$namedAreas = @()
|
||||
$namedDrawings = @()
|
||||
|
||||
foreach ($ni in $root.SelectNodes("d:namedItem", $nsMgr)) {
|
||||
$niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$name = $ni.SelectSingleNode("d:name", $nsMgr).InnerText
|
||||
|
||||
if ($niType -like "*NamedItemCells*") {
|
||||
$area = $ni.SelectSingleNode("d:area", $nsMgr)
|
||||
$areaType = $area.SelectSingleNode("d:type", $nsMgr).InnerText
|
||||
$beginRow = [int]$area.SelectSingleNode("d:beginRow", $nsMgr).InnerText
|
||||
$endRow = [int]$area.SelectSingleNode("d:endRow", $nsMgr).InnerText
|
||||
$beginCol = [int]$area.SelectSingleNode("d:beginColumn", $nsMgr).InnerText
|
||||
$endCol = [int]$area.SelectSingleNode("d:endColumn", $nsMgr).InnerText
|
||||
$colsId = $null
|
||||
$colsIdNode = $area.SelectSingleNode("d:columnsID", $nsMgr)
|
||||
if ($colsIdNode) { $colsId = $colsIdNode.InnerText }
|
||||
|
||||
$namedAreas += @{
|
||||
Name = $name
|
||||
AreaType = $areaType
|
||||
BeginRow = $beginRow
|
||||
EndRow = $endRow
|
||||
BeginCol = $beginCol
|
||||
EndCol = $endCol
|
||||
ColumnsID = $colsId
|
||||
}
|
||||
} elseif ($niType -like "*NamedItemDrawing*") {
|
||||
$drawId = $ni.SelectSingleNode("d:drawingID", $nsMgr).InnerText
|
||||
$namedDrawings += @{ Name = $name; DrawingID = $drawId }
|
||||
}
|
||||
}
|
||||
|
||||
# --- Scan rows for parameters and text ---
|
||||
|
||||
# Build row index map: rowIndex -> XmlNode
|
||||
$rowMap = @{}
|
||||
foreach ($ri in $rowNodes) {
|
||||
$idx = [int]$ri.SelectSingleNode("d:index", $nsMgr).InnerText
|
||||
$rowMap[$idx] = $ri
|
||||
}
|
||||
|
||||
function Get-CellData {
|
||||
param($rowNode, [System.Xml.XmlNamespaceManager]$ns, [bool]$includeText)
|
||||
|
||||
$row = $rowNode.SelectSingleNode("d:row", $ns)
|
||||
if (-not $row) { return @() }
|
||||
|
||||
$results = @()
|
||||
foreach ($cGroup in $row.SelectNodes("d:c", $ns)) {
|
||||
$cell = $cGroup.SelectSingleNode("d:c", $ns)
|
||||
if (-not $cell) { continue }
|
||||
|
||||
$param = $cell.SelectSingleNode("d:parameter", $ns)
|
||||
$detail = $cell.SelectSingleNode("d:detailParameter", $ns)
|
||||
$tl = $cell.SelectSingleNode("d:tl", $ns)
|
||||
|
||||
if ($param) {
|
||||
$entry = @{ Kind = "Parameter"; Value = $param.InnerText }
|
||||
if ($detail) { $entry.Detail = $detail.InnerText }
|
||||
$results += $entry
|
||||
}
|
||||
|
||||
if ($includeText -and $tl) {
|
||||
$content = $tl.SelectSingleNode("v8:item/v8:content", $ns)
|
||||
if ($content -and $content.InnerText) {
|
||||
$text = $content.InnerText
|
||||
# Detect if this is a Template (has [...] placeholders)
|
||||
if ($text -match '\[.+\]') {
|
||||
$results += @{ Kind = "Template"; Value = $text }
|
||||
} else {
|
||||
$results += @{ Kind = "Text"; Value = $text }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $results
|
||||
}
|
||||
|
||||
function Get-AreaCellData {
|
||||
param(
|
||||
[hashtable]$area,
|
||||
[hashtable]$rowMap,
|
||||
[System.Xml.XmlNamespaceManager]$ns,
|
||||
[bool]$includeText
|
||||
)
|
||||
|
||||
$params = @()
|
||||
$texts = @()
|
||||
$templates = @()
|
||||
|
||||
$startRow = $area.BeginRow
|
||||
$endRow = $area.EndRow
|
||||
if ($startRow -eq -1) { $startRow = 0 }
|
||||
if ($endRow -eq -1) { $endRow = $docHeight - 1 }
|
||||
|
||||
for ($r = $startRow; $r -le $endRow; $r++) {
|
||||
if ($rowMap.ContainsKey($r)) {
|
||||
$cells = Get-CellData -rowNode $rowMap[$r] -ns $ns -includeText $includeText
|
||||
foreach ($c in $cells) {
|
||||
switch ($c.Kind) {
|
||||
"Parameter" { $params += $c.Value }
|
||||
"Text" { $texts += $c.Value }
|
||||
"Template" { $templates += $c.Value }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return @{ Params = $params; Texts = $texts; Templates = $templates }
|
||||
}
|
||||
|
||||
# Collect data for each area
|
||||
$areaData = @()
|
||||
$coveredRows = @{}
|
||||
|
||||
foreach ($area in $namedAreas) {
|
||||
$data = Get-AreaCellData -area $area -rowMap $rowMap -ns $nsMgr -includeText $WithText
|
||||
$areaData += @{
|
||||
Area = $area
|
||||
Params = $data.Params
|
||||
Texts = $data.Texts
|
||||
Templates = $data.Templates
|
||||
}
|
||||
|
||||
# Track covered rows
|
||||
$sr = $area.BeginRow
|
||||
$er = $area.EndRow
|
||||
if ($sr -ne -1 -and $er -ne -1) {
|
||||
for ($r = $sr; $r -le $er; $r++) {
|
||||
$coveredRows[$r] = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Find parameters outside named areas
|
||||
$outsideParams = @()
|
||||
$outsideTexts = @()
|
||||
$outsideTemplates = @()
|
||||
|
||||
foreach ($r in $rowMap.Keys | Sort-Object) {
|
||||
if (-not $coveredRows.ContainsKey($r)) {
|
||||
$cells = Get-CellData -rowNode $rowMap[$r] -ns $nsMgr -includeText $WithText
|
||||
foreach ($c in $cells) {
|
||||
switch ($c.Kind) {
|
||||
"Parameter" { $outsideParams += $c.Value }
|
||||
"Text" { $outsideTexts += $c.Value }
|
||||
"Template" { $outsideTemplates += $c.Value }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Counts ---
|
||||
|
||||
$mergeCount = $root.SelectNodes("d:merge", $nsMgr).Count
|
||||
$drawingNodes = $root.SelectNodes("d:drawing", $nsMgr)
|
||||
$drawingCount = $drawingNodes.Count
|
||||
|
||||
# --- Output ---
|
||||
|
||||
function Truncate-List {
|
||||
param([string[]]$items, [int]$max)
|
||||
if ($items.Count -le $max) {
|
||||
return ($items -join ", ")
|
||||
}
|
||||
$shown = ($items[0..($max - 1)] -join ", ")
|
||||
$remaining = $items.Count - $max
|
||||
return "$shown, ... (+$remaining)"
|
||||
}
|
||||
|
||||
# Determine template name from path
|
||||
$templateName = [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($TemplatePath)))
|
||||
|
||||
if ($Format -eq "json") {
|
||||
$result = @{
|
||||
name = $templateName
|
||||
rows = $docHeight
|
||||
columns = $defaultColCount
|
||||
columnSets = @($columnSets)
|
||||
areas = @()
|
||||
outsideParams = @($outsideParams)
|
||||
mergeCount = $mergeCount
|
||||
drawingCount = $drawingCount
|
||||
}
|
||||
|
||||
foreach ($ad in $areaData) {
|
||||
$areaObj = @{
|
||||
name = $ad.Area.Name
|
||||
type = $ad.Area.AreaType
|
||||
beginRow = $ad.Area.BeginRow
|
||||
endRow = $ad.Area.EndRow
|
||||
beginCol = $ad.Area.BeginCol
|
||||
endCol = $ad.Area.EndCol
|
||||
params = @($ad.Params)
|
||||
}
|
||||
if ($ad.Area.ColumnsID) { $areaObj.columnsID = $ad.Area.ColumnsID }
|
||||
if ($WithText) {
|
||||
$areaObj.texts = @($ad.Texts)
|
||||
$areaObj.templates = @($ad.Templates)
|
||||
}
|
||||
$result.areas += $areaObj
|
||||
}
|
||||
|
||||
if ($WithText) {
|
||||
$result.outsideTexts = @($outsideTexts)
|
||||
$result.outsideTemplates = @($outsideTemplates)
|
||||
}
|
||||
|
||||
foreach ($nd in $namedDrawings) {
|
||||
$result.areas += @{
|
||||
name = $nd.Name
|
||||
type = "Drawing"
|
||||
drawingID = $nd.DrawingID
|
||||
}
|
||||
}
|
||||
|
||||
$result | ConvertTo-Json -Depth 5
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Text format output ---
|
||||
|
||||
$lines = @()
|
||||
|
||||
$lines += "=== $templateName ==="
|
||||
$lines += " Rows: $docHeight, Columns: $defaultColCount"
|
||||
|
||||
if ($columnSets.Count -eq 0) {
|
||||
$lines += " Column sets: 1 (default only)"
|
||||
} else {
|
||||
$lines += " Column sets: $($columnSets.Count + 1) (default + $($columnSets.Count) additional)"
|
||||
}
|
||||
|
||||
$lines += ""
|
||||
$lines += "--- Named areas ---"
|
||||
|
||||
foreach ($ad in $areaData) {
|
||||
$a = $ad.Area
|
||||
$paramCount = $ad.Params.Count
|
||||
$rowRange = ""
|
||||
|
||||
switch ($a.AreaType) {
|
||||
"Rows" { $rowRange = "rows $($a.BeginRow)-$($a.EndRow)" }
|
||||
"Columns" { $rowRange = "cols $($a.BeginCol)-$($a.EndCol)" }
|
||||
"Rectangle" { $rowRange = "rows $($a.BeginRow)-$($a.EndRow), cols $($a.BeginCol)-$($a.EndCol)" }
|
||||
}
|
||||
|
||||
$colsInfo = ""
|
||||
if ($a.ColumnsID) {
|
||||
$colsInfo = " [colset]"
|
||||
}
|
||||
|
||||
$paramInfo = "($paramCount params)"
|
||||
$nameStr = $a.Name.PadRight(25)
|
||||
$typeStr = $a.AreaType.PadRight(12)
|
||||
$lines += " $nameStr $typeStr $rowRange $paramInfo$colsInfo"
|
||||
}
|
||||
|
||||
foreach ($nd in $namedDrawings) {
|
||||
$nameStr = $nd.Name.PadRight(25)
|
||||
$lines += " $nameStr Drawing drawingID=$($nd.DrawingID)"
|
||||
}
|
||||
|
||||
# Parameters by area
|
||||
$hasParams = ($areaData | Where-Object { $_.Params.Count -gt 0 }) -or ($outsideParams.Count -gt 0)
|
||||
|
||||
if ($hasParams) {
|
||||
$lines += ""
|
||||
$lines += "--- Parameters by area ---"
|
||||
foreach ($ad in $areaData) {
|
||||
if ($ad.Params.Count -gt 0) {
|
||||
$paramStr = Truncate-List -items $ad.Params -max $MaxParams
|
||||
$lines += " $($ad.Area.Name): $paramStr"
|
||||
}
|
||||
}
|
||||
if ($outsideParams.Count -gt 0) {
|
||||
$paramStr = Truncate-List -items $outsideParams -max $MaxParams
|
||||
$lines += " (outside areas): $paramStr"
|
||||
}
|
||||
}
|
||||
|
||||
# WithText sections
|
||||
if ($WithText) {
|
||||
$hasText = ($areaData | Where-Object { $_.Texts.Count -gt 0 -or $_.Templates.Count -gt 0 }) -or ($outsideTexts.Count -gt 0) -or ($outsideTemplates.Count -gt 0)
|
||||
|
||||
if ($hasText) {
|
||||
$lines += ""
|
||||
$lines += "--- Text content ---"
|
||||
foreach ($ad in $areaData) {
|
||||
if ($ad.Texts.Count -gt 0 -or $ad.Templates.Count -gt 0) {
|
||||
$lines += " $($ad.Area.Name):"
|
||||
if ($ad.Texts.Count -gt 0) {
|
||||
$textItems = $ad.Texts | ForEach-Object { "`"$_`"" }
|
||||
$textStr = Truncate-List -items $textItems -max $MaxParams
|
||||
$lines += " Text: $textStr"
|
||||
}
|
||||
if ($ad.Templates.Count -gt 0) {
|
||||
$tplItems = $ad.Templates | ForEach-Object { "`"$_`"" }
|
||||
$tplStr = Truncate-List -items $tplItems -max $MaxParams
|
||||
$lines += " Templates: $tplStr"
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($outsideTexts.Count -gt 0 -or $outsideTemplates.Count -gt 0) {
|
||||
$lines += " (outside areas):"
|
||||
if ($outsideTexts.Count -gt 0) {
|
||||
$textItems = $outsideTexts | ForEach-Object { "`"$_`"" }
|
||||
$textStr = Truncate-List -items $textItems -max $MaxParams
|
||||
$lines += " Text: $textStr"
|
||||
}
|
||||
if ($outsideTemplates.Count -gt 0) {
|
||||
$tplItems = $outsideTemplates | ForEach-Object { "`"$_`"" }
|
||||
$tplStr = Truncate-List -items $tplItems -max $MaxParams
|
||||
$lines += " Templates: $tplStr"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$lines += ""
|
||||
$lines += "--- Stats ---"
|
||||
$lines += " Merges: $mergeCount"
|
||||
$lines += " Drawings: $drawingCount"
|
||||
|
||||
# --- Truncation protection ---
|
||||
|
||||
$totalLines = $lines.Count
|
||||
|
||||
if ($Offset -gt 0) {
|
||||
if ($Offset -ge $totalLines) {
|
||||
Write-Host "[INFO] Offset $Offset exceeds total lines ($totalLines). Nothing to show."
|
||||
exit 0
|
||||
}
|
||||
$lines = $lines[$Offset..($totalLines - 1)]
|
||||
}
|
||||
|
||||
if ($lines.Count -gt $Limit) {
|
||||
$shown = $lines[0..($Limit - 1)]
|
||||
foreach ($l in $shown) { Write-Host $l }
|
||||
$remaining = $totalLines - $Offset - $Limit
|
||||
Write-Host ""
|
||||
Write-Host "[TRUNCATED] Shown $Limit of $totalLines lines. Use -Offset $($Offset + $Limit) to continue."
|
||||
} else {
|
||||
foreach ($l in $lines) { Write-Host $l }
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: mxl-validate
|
||||
description: Validate SpreadsheetDocument (MXL) template for structural correctness
|
||||
argument-hint: <TemplatePath> or <ProcessorName> <TemplateName>
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Glob
|
||||
---
|
||||
|
||||
# /mxl-validate — Template Validator
|
||||
|
||||
Checks Template.xml for structural errors that the 1C platform may silently ignore (potentially causing data loss or template corruption).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/mxl-validate <TemplatePath>
|
||||
/mxl-validate <ProcessorName> <TemplateName>
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
| Parameter | Required | Default | Description |
|
||||
|---------------|:--------:|---------|------------------------------------------|
|
||||
| TemplatePath | no | — | Direct path to Template.xml |
|
||||
| ProcessorName | no | — | Processor name (alternative to path) |
|
||||
| TemplateName | no | — | Template name (alternative to path) |
|
||||
| SrcDir | no | `src` | Source directory |
|
||||
| MaxErrors | no | 20 | Stop after N errors |
|
||||
|
||||
Specify either `-TemplatePath` or both `-ProcessorName` and `-TemplateName`.
|
||||
|
||||
## Command
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/mxl-validate/scripts/mxl-validate.ps1 -TemplatePath "<path>"
|
||||
```
|
||||
|
||||
Or with processor/template names:
|
||||
```powershell
|
||||
powershell.exe -NoProfile -File .claude/skills/mxl-validate/scripts/mxl-validate.ps1 -ProcessorName "<Name>" -TemplateName "<Template>" [-SrcDir "<dir>"]
|
||||
```
|
||||
|
||||
## Checks Performed
|
||||
|
||||
| # | Check | Severity |
|
||||
|---|---|---|
|
||||
| 1 | `<height>` >= max row index + 1 | ERROR |
|
||||
| 2 | `<vgRows>` <= `<height>` | WARN |
|
||||
| 3 | Cell format indices (`<f>`) within format palette | ERROR |
|
||||
| 4 | Row/column `<formatIndex>` within format palette | ERROR |
|
||||
| 5 | Cell column indices (`<i>`) within column count (per column set) | ERROR |
|
||||
| 6 | Row `<columnsID>` references existing column set | ERROR |
|
||||
| 7 | Merge/namedItem `<columnsID>` references existing column set | ERROR |
|
||||
| 8 | Named area row/column ranges within document bounds | ERROR |
|
||||
| 9 | Merge ranges within document bounds | ERROR |
|
||||
| 10 | Font indices in formats within font palette | ERROR |
|
||||
| 11 | Border/line indices in formats within line palette | ERROR |
|
||||
| 12 | Drawing `pictureIndex` references existing picture | ERROR |
|
||||
|
||||
## Output
|
||||
|
||||
```
|
||||
=== Validation: TemplateName ===
|
||||
|
||||
[OK] height (40) >= max row index + 1 (40), rowsItem count=34
|
||||
[OK] Font refs: max=3, palette size=4
|
||||
[ERROR] Row 15: cell format index 38 > format palette size (37)
|
||||
[OK] Column indices: max in default set=32, default column count=33
|
||||
---
|
||||
Errors: 1, Warnings: 0
|
||||
```
|
||||
|
||||
Exit code: 0 = all checks passed, 1 = errors found.
|
||||
|
||||
## When to Use
|
||||
|
||||
- **After generating a template**: run validator to catch structural errors before building EPF
|
||||
- **After editing Template.xml**: verify indices and references are still valid
|
||||
- **On errors**: fix the reported issues and re-run until all checks pass
|
||||
|
||||
## Error Protection
|
||||
|
||||
Stops after 20 errors by default (configurable with `-MaxErrors`). Summary line always shows total counts.
|
||||
@@ -0,0 +1,395 @@
|
||||
param(
|
||||
[string]$TemplatePath,
|
||||
[string]$ProcessorName,
|
||||
[string]$TemplateName,
|
||||
[string]$SrcDir = "src",
|
||||
[int]$MaxErrors = 20
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
# --- Resolve template path ---
|
||||
|
||||
if (-not $TemplatePath) {
|
||||
if (-not $ProcessorName -or -not $TemplateName) {
|
||||
Write-Error "Specify -TemplatePath or both -ProcessorName and -TemplateName"
|
||||
exit 1
|
||||
}
|
||||
$TemplatePath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $SrcDir $ProcessorName) "Templates") $TemplateName) "Ext") "Template.xml"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $TemplatePath)) {
|
||||
Write-Error "File not found: $TemplatePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Load XML ---
|
||||
|
||||
$xmlDoc = New-Object System.Xml.XmlDocument
|
||||
$xmlDoc.PreserveWhitespace = $false
|
||||
$xmlDoc.Load((Resolve-Path $TemplatePath).Path)
|
||||
|
||||
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
|
||||
$nsMgr.AddNamespace("d", "http://v8.1c.ru/8.2/data/spreadsheet")
|
||||
$nsMgr.AddNamespace("v8", "http://v8.1c.ru/8.1/data/core")
|
||||
$nsMgr.AddNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
|
||||
$root = $xmlDoc.DocumentElement
|
||||
|
||||
# --- Counters ---
|
||||
|
||||
$errors = 0
|
||||
$warnings = 0
|
||||
$stopped = $false
|
||||
|
||||
function Report-OK {
|
||||
param([string]$msg)
|
||||
Write-Host "[OK] $msg"
|
||||
}
|
||||
|
||||
function Report-Error {
|
||||
param([string]$msg)
|
||||
$script:errors++
|
||||
Write-Host "[ERROR] $msg"
|
||||
if ($script:errors -ge $MaxErrors) {
|
||||
$script:stopped = $true
|
||||
}
|
||||
}
|
||||
|
||||
function Report-Warn {
|
||||
param([string]$msg)
|
||||
$script:warnings++
|
||||
Write-Host "[WARN] $msg"
|
||||
}
|
||||
|
||||
$templateName = [System.IO.Path]::GetFileName([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetDirectoryName($TemplatePath)))
|
||||
Write-Host "=== Validation: $templateName ==="
|
||||
Write-Host ""
|
||||
|
||||
# --- Collect palettes ---
|
||||
|
||||
$lineNodes = $root.SelectNodes("d:line", $nsMgr)
|
||||
$lineCount = $lineNodes.Count
|
||||
|
||||
$fontNodes = @()
|
||||
foreach ($node in $root.ChildNodes) {
|
||||
if ($node.LocalName -eq "font") { $fontNodes += $node }
|
||||
}
|
||||
$fontCount = $fontNodes.Count
|
||||
|
||||
$formatNodes = @()
|
||||
foreach ($node in $root.ChildNodes) {
|
||||
if ($node.LocalName -eq "format") { $formatNodes += $node }
|
||||
}
|
||||
$formatCount = $formatNodes.Count
|
||||
|
||||
$pictureNodes = $root.SelectNodes("d:picture", $nsMgr)
|
||||
$pictureCount = $pictureNodes.Count
|
||||
|
||||
# --- Collect column sets ---
|
||||
|
||||
$columnSets = @{} # id -> size
|
||||
$defaultColCount = 0
|
||||
|
||||
foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) {
|
||||
$sizeNode = $cols.SelectSingleNode("d:size", $nsMgr)
|
||||
$idNode = $cols.SelectSingleNode("d:id", $nsMgr)
|
||||
$size = if ($sizeNode) { [int]$sizeNode.InnerText } else { 0 }
|
||||
|
||||
if ($idNode) {
|
||||
$columnSets[$idNode.InnerText] = $size
|
||||
} else {
|
||||
$defaultColCount = $size
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 1: height vs actual rows ---
|
||||
|
||||
$rowNodes = $root.SelectNodes("d:rowsItem", $nsMgr)
|
||||
$heightNode = $root.SelectSingleNode("d:height", $nsMgr)
|
||||
$docHeight = if ($heightNode) { [int]$heightNode.InnerText } else { 0 }
|
||||
|
||||
# Find max row index (not all rows have rowsItem - implicit rows are skipped)
|
||||
$maxRowIndex = -1
|
||||
foreach ($ri in $rowNodes) {
|
||||
$idxNode = $ri.SelectSingleNode("d:index", $nsMgr)
|
||||
if ($idxNode) {
|
||||
$idx = [int]$idxNode.InnerText
|
||||
if ($idx -gt $maxRowIndex) { $maxRowIndex = $idx }
|
||||
}
|
||||
}
|
||||
|
||||
$expectedMinHeight = $maxRowIndex + 1
|
||||
if ($docHeight -ge $expectedMinHeight) {
|
||||
Report-OK "height ($docHeight) >= max row index + 1 ($expectedMinHeight), rowsItem count=$($rowNodes.Count)"
|
||||
} else {
|
||||
Report-Error "height=$docHeight but max row index=$maxRowIndex (need at least $expectedMinHeight)"
|
||||
}
|
||||
# --- Check 2: vgRows <= height ---
|
||||
|
||||
$vgRowsNode = $root.SelectSingleNode("d:vgRows", $nsMgr)
|
||||
if ($vgRowsNode) {
|
||||
$vgRows = [int]$vgRowsNode.InnerText
|
||||
if ($vgRows -le $docHeight) {
|
||||
Report-OK "vgRows ($vgRows) <= height ($docHeight)"
|
||||
} else {
|
||||
Report-Warn "vgRows ($vgRows) > height ($docHeight)"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Build row data for checks ---
|
||||
|
||||
$maxFormatRef = 0
|
||||
$maxFontRef = 0
|
||||
$maxLineRef = 0
|
||||
|
||||
# Check format palette references in formats (font, border indices)
|
||||
foreach ($fmt in $formatNodes) {
|
||||
$fontIdx = $fmt.SelectSingleNode("d:font", $nsMgr)
|
||||
if ($fontIdx) {
|
||||
$val = [int]$fontIdx.InnerText
|
||||
if ($val -gt $maxFontRef) { $maxFontRef = $val }
|
||||
}
|
||||
|
||||
foreach ($border in @("d:leftBorder", "d:topBorder", "d:rightBorder", "d:bottomBorder", "d:drawingBorder")) {
|
||||
$borderNode = $fmt.SelectSingleNode($border, $nsMgr)
|
||||
if ($borderNode) {
|
||||
$val = [int]$borderNode.InnerText
|
||||
if ($val -gt $maxLineRef) { $maxLineRef = $val }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 10: font indices in formats ---
|
||||
|
||||
if ($fontCount -gt 0) {
|
||||
if ($maxFontRef -lt $fontCount) {
|
||||
Report-OK "Font refs: max=$maxFontRef, palette size=$fontCount"
|
||||
} else {
|
||||
Report-Error "Font index $maxFontRef exceeds palette size ($fontCount)"
|
||||
}
|
||||
} elseif ($maxFontRef -gt 0) {
|
||||
Report-Error "Font index $maxFontRef referenced but no fonts defined"
|
||||
} else {
|
||||
Report-OK "No font references"
|
||||
}
|
||||
|
||||
# --- Check 11: line/border indices in formats ---
|
||||
|
||||
if ($lineCount -gt 0) {
|
||||
if ($maxLineRef -lt $lineCount) {
|
||||
Report-OK "Line/border refs: max=$maxLineRef, palette size=$lineCount"
|
||||
} else {
|
||||
Report-Error "Line index $maxLineRef exceeds palette size ($lineCount)"
|
||||
}
|
||||
} elseif ($maxLineRef -gt 0) {
|
||||
Report-Error "Line index $maxLineRef referenced but no lines defined"
|
||||
} else {
|
||||
Report-OK "No line/border references"
|
||||
}
|
||||
|
||||
# --- Check 3, 4, 5, 6: row/cell checks ---
|
||||
|
||||
$maxCellFormatRef = 0
|
||||
$maxRowFormatRef = 0
|
||||
$maxDefaultColIdx = 0
|
||||
$rowIndex = 0
|
||||
|
||||
foreach ($ri in $rowNodes) {
|
||||
if ($stopped) { break }
|
||||
|
||||
$idxNode = $ri.SelectSingleNode("d:index", $nsMgr)
|
||||
$rowIndex = if ($idxNode) { [int]$idxNode.InnerText } else { $rowIndex }
|
||||
|
||||
$row = $ri.SelectSingleNode("d:row", $nsMgr)
|
||||
if (-not $row) { continue }
|
||||
|
||||
# Row formatIndex
|
||||
$rowFmtNode = $row.SelectSingleNode("d:formatIndex", $nsMgr)
|
||||
if ($rowFmtNode) {
|
||||
$val = [int]$rowFmtNode.InnerText
|
||||
if ($val -gt $maxRowFormatRef) { $maxRowFormatRef = $val }
|
||||
if ($val -gt $formatCount) {
|
||||
Report-Error "Row ${rowIndex}: formatIndex=$val > format palette size ($formatCount)"
|
||||
}
|
||||
}
|
||||
|
||||
# Check columnsID
|
||||
$rowColsId = $null
|
||||
$colsIdNode = $row.SelectSingleNode("d:columnsID", $nsMgr)
|
||||
if ($colsIdNode) {
|
||||
$rowColsId = $colsIdNode.InnerText
|
||||
if (-not $columnSets.ContainsKey($rowColsId)) {
|
||||
Report-Error "Row ${rowIndex}: columnsID '$($rowColsId.Substring(0,8))...' not found in column sets"
|
||||
}
|
||||
}
|
||||
|
||||
# Determine column count for this row
|
||||
$rowColCount = $defaultColCount
|
||||
if ($rowColsId -and $columnSets.ContainsKey($rowColsId)) {
|
||||
$rowColCount = $columnSets[$rowColsId]
|
||||
}
|
||||
|
||||
# Cell checks
|
||||
foreach ($cGroup in $row.SelectNodes("d:c", $nsMgr)) {
|
||||
$iNode = $cGroup.SelectSingleNode("d:i", $nsMgr)
|
||||
if ($iNode) {
|
||||
$colIdx = [int]$iNode.InnerText
|
||||
# Track max index for default column set only
|
||||
if (-not $rowColsId -and $colIdx -gt $maxDefaultColIdx) {
|
||||
$maxDefaultColIdx = $colIdx
|
||||
}
|
||||
# Check against row's column count
|
||||
if ($rowColCount -gt 0 -and $colIdx -ge $rowColCount) {
|
||||
Report-Error "Row ${rowIndex}: column index $colIdx >= column count ($rowColCount)"
|
||||
}
|
||||
}
|
||||
|
||||
$cell = $cGroup.SelectSingleNode("d:c", $nsMgr)
|
||||
if ($cell) {
|
||||
$fNode = $cell.SelectSingleNode("d:f", $nsMgr)
|
||||
if ($fNode) {
|
||||
$val = [int]$fNode.InnerText
|
||||
if ($val -gt $maxCellFormatRef) { $maxCellFormatRef = $val }
|
||||
if ($val -gt $formatCount) {
|
||||
Report-Error "Row ${rowIndex}: cell format index $val > format palette size ($formatCount)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rowIndex++
|
||||
}
|
||||
|
||||
# Summary checks for format refs
|
||||
if (-not $stopped) {
|
||||
if ($maxCellFormatRef -le $formatCount -and $maxRowFormatRef -le $formatCount) {
|
||||
Report-OK "Format refs: max cell=$maxCellFormatRef, max row=$maxRowFormatRef, palette size=$formatCount"
|
||||
}
|
||||
}
|
||||
|
||||
# Check column format indices
|
||||
foreach ($cols in $root.SelectNodes("d:columns", $nsMgr)) {
|
||||
if ($stopped) { break }
|
||||
foreach ($ci in $cols.SelectNodes("d:columnsItem", $nsMgr)) {
|
||||
$col = $ci.SelectSingleNode("d:column", $nsMgr)
|
||||
if ($col) {
|
||||
$fmtNode = $col.SelectSingleNode("d:formatIndex", $nsMgr)
|
||||
if ($fmtNode) {
|
||||
$val = [int]$fmtNode.InnerText
|
||||
if ($val -gt $formatCount) {
|
||||
$colIdx = $ci.SelectSingleNode("d:index", $nsMgr).InnerText
|
||||
Report-Error "Column ${colIdx}: formatIndex=$val > format palette size ($formatCount)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 5: column index summary ---
|
||||
|
||||
if (-not $stopped) {
|
||||
Report-OK "Column indices: max in default set=$maxDefaultColIdx, default column count=$defaultColCount"
|
||||
}
|
||||
|
||||
# --- Check 7, 8: named areas ---
|
||||
|
||||
foreach ($ni in $root.SelectNodes("d:namedItem", $nsMgr)) {
|
||||
if ($stopped) { break }
|
||||
|
||||
$niType = $ni.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
|
||||
$name = $ni.SelectSingleNode("d:name", $nsMgr).InnerText
|
||||
|
||||
if ($niType -like "*NamedItemCells*") {
|
||||
$area = $ni.SelectSingleNode("d:area", $nsMgr)
|
||||
$beginRow = [int]$area.SelectSingleNode("d:beginRow", $nsMgr).InnerText
|
||||
$endRow = [int]$area.SelectSingleNode("d:endRow", $nsMgr).InnerText
|
||||
|
||||
# Check row bounds (skip -1 which means "all")
|
||||
if ($beginRow -ne -1 -and $beginRow -ge $docHeight) {
|
||||
Report-Error "Area '$name': beginRow=$beginRow >= height=$docHeight"
|
||||
}
|
||||
if ($endRow -ne -1 -and $endRow -ge $docHeight) {
|
||||
Report-Error "Area '$name': endRow=$endRow >= height=$docHeight"
|
||||
}
|
||||
|
||||
# Check columnsID reference
|
||||
$colsIdNode = $area.SelectSingleNode("d:columnsID", $nsMgr)
|
||||
if ($colsIdNode) {
|
||||
$colsId = $colsIdNode.InnerText
|
||||
if (-not $columnSets.ContainsKey($colsId)) {
|
||||
Report-Error "Area '$name': columnsID '$($colsId.Substring(0,8))...' not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 9: merge bounds ---
|
||||
|
||||
foreach ($merge in $root.SelectNodes("d:merge", $nsMgr)) {
|
||||
if ($stopped) { break }
|
||||
|
||||
$r = [int]$merge.SelectSingleNode("d:r", $nsMgr).InnerText
|
||||
$c = [int]$merge.SelectSingleNode("d:c", $nsMgr).InnerText
|
||||
$wNode = $merge.SelectSingleNode("d:w", $nsMgr)
|
||||
$hNode = $merge.SelectSingleNode("d:h", $nsMgr)
|
||||
|
||||
# r=-1 means all rows, skip bound check
|
||||
if ($r -ne -1 -and $r -ge $docHeight) {
|
||||
Report-Error "Merge at row=${r}, col=${c}: row >= height ($docHeight)"
|
||||
}
|
||||
|
||||
if ($hNode -and $r -ne -1) {
|
||||
$h = [int]$hNode.InnerText
|
||||
if (($r + $h) -ge $docHeight) {
|
||||
Report-Error "Merge at row=${r}: extends to row $($r + $h) >= height ($docHeight)"
|
||||
}
|
||||
}
|
||||
|
||||
# Check columnsID in merge
|
||||
$colsIdNode = $merge.SelectSingleNode("d:columnsID", $nsMgr)
|
||||
if ($colsIdNode) {
|
||||
$colsId = $colsIdNode.InnerText
|
||||
if (-not $columnSets.ContainsKey($colsId)) {
|
||||
Report-Error "Merge at row=${r}, col=${c}: columnsID '$($colsId.Substring(0,8))...' not found"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Check 12: drawing picture indices ---
|
||||
|
||||
foreach ($drawing in $root.SelectNodes("d:drawing", $nsMgr)) {
|
||||
if ($stopped) { break }
|
||||
|
||||
$picIdxNode = $drawing.SelectSingleNode("d:pictureIndex", $nsMgr)
|
||||
if ($picIdxNode) {
|
||||
$picIdx = [int]$picIdxNode.InnerText
|
||||
if ($picIdx -gt $pictureCount) {
|
||||
$drawId = $drawing.SelectSingleNode("d:id", $nsMgr).InnerText
|
||||
Report-Error "Drawing id=${drawId}: pictureIndex=$picIdx > picture count ($pictureCount)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Summary ---
|
||||
|
||||
# :finish label equivalent
|
||||
Write-Host ""
|
||||
Write-Host "---"
|
||||
|
||||
if ($stopped) {
|
||||
Write-Host "Stopped after $MaxErrors errors. Fix and re-run."
|
||||
}
|
||||
|
||||
if ($errors -eq 0 -and $warnings -eq 0) {
|
||||
Write-Host "All checks passed."
|
||||
} else {
|
||||
Write-Host "Errors: $errors, Warnings: $warnings"
|
||||
}
|
||||
|
||||
if ($errors -gt 0) {
|
||||
exit 1
|
||||
} else {
|
||||
exit 0
|
||||
}
|
||||
@@ -16,6 +16,8 @@
|
||||
| `/epf-dump` | `<EpfFile>` | Разобрать EPF в XML (документация команды 1cv8.exe) |
|
||||
| `/epf-bsp-init` | `<ProcessorName> <Вид>` | Добавить регистрацию БСП (СведенияОВнешнейОбработке) |
|
||||
| `/epf-bsp-add-command` | `<ProcessorName> <Идентификатор>` | Добавить команду в обработку БСП |
|
||||
| `/mxl-info` | `<TemplatePath>` | Анализ структуры табличного документа (области, параметры, колонки) |
|
||||
| `/mxl-validate` | `<TemplatePath>` | Валидация табличного документа (индексы, ссылки, границы) |
|
||||
|
||||
Навыки удаления (`epf-remove-*`) не вызываются Claude автоматически — только по явной команде пользователя.
|
||||
|
||||
@@ -138,11 +140,14 @@ src/
|
||||
├── epf-dump/ # SKILL.md (только документация)
|
||||
├── epf-add-help/ # SKILL.md + scripts/add-help.ps1
|
||||
├── epf-bsp-init/ # SKILL.md (шаблоны кода, без скриптов)
|
||||
└── epf-bsp-add-command/ # SKILL.md (шаблоны кода, без скриптов)
|
||||
├── epf-bsp-add-command/ # SKILL.md (шаблоны кода, без скриптов)
|
||||
├── mxl-info/ # SKILL.md + scripts/mxl-info.ps1
|
||||
└── mxl-validate/ # SKILL.md + scripts/mxl-validate.ps1
|
||||
docs/
|
||||
├── 1c-xml-format-spec.md # Спецификация XML-формата выгрузки
|
||||
├── 1c-help-spec.md # Спецификация встроенной справки
|
||||
└── build-spec.md # Спецификация команд сборки/разборки
|
||||
├── build-spec.md # Спецификация команд сборки/разборки
|
||||
└── 1c-spreadsheet-spec.md # Спецификация табличного документа (MXL)
|
||||
```
|
||||
|
||||
## Спецификации
|
||||
@@ -150,6 +155,7 @@ docs/
|
||||
- [XML-формат выгрузки обработок](docs/1c-xml-format-spec.md) — полное описание структуры XML-файлов, namespace'ов, элементов форм
|
||||
- [Встроенная справка](docs/1c-help-spec.md) — Help.xml, HTML-страницы, кнопка справки на форме
|
||||
- [Сборка и разборка EPF](docs/build-spec.md) — команды `1cv8.exe`, параметры, коды возврата
|
||||
- [Табличный документ (MXL)](docs/1c-spreadsheet-spec.md) — XML-формат SpreadsheetDocument, совместимость версий
|
||||
|
||||
## Технические детали
|
||||
|
||||
|
||||
Reference in New Issue
Block a user