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:
Nick Shirokov
2026-02-08 15:59:49 +03:00
parent fdf661192d
commit 76a0dd80be
5 changed files with 1008 additions and 2 deletions
+103
View File
@@ -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 }
}
+85
View File
@@ -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
}
+8 -2
View File
@@ -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, совместимость версий
## Технические детали