Auto-build: copilot (python) from 7fa279c

This commit is contained in:
github-actions[bot]
2026-05-17 11:22:33 +00:00
commit bbd2f7a8c1
207 changed files with 98901 additions and 0 deletions
@@ -0,0 +1,733 @@
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$JsonPath,
[Parameter(Mandatory)]
[string]$OutputPath
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- 1. Load and validate JSON ---
if (-not (Test-Path $JsonPath)) {
Write-Error "File not found: $JsonPath"
exit 1
}
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
if (-not $def.columns) {
Write-Error "Required field 'columns' is missing"
exit 1
}
if (-not $def.areas) {
Write-Error "Required field 'areas' is missing"
exit 1
}
$totalColumns = [int]$def.columns
$defaultWidth = if ($def.defaultWidth) { [int]$def.defaultWidth } else { 10 }
# --- 2. Build font palette ---
$fontMap = [ordered]@{} # name -> 0-based index
$fontEntries = @() # array of hashtables
function Add-Font {
param([string]$name, $fontDef)
$face = if ($fontDef.face) { $fontDef.face } else { "Arial" }
$size = if ($fontDef.size) { [int]$fontDef.size } else { 10 }
$bold = if ($fontDef.bold -eq $true) { "true" } else { "false" }
$italic = if ($fontDef.italic -eq $true) { "true" } else { "false" }
$underline = if ($fontDef.underline -eq $true) { "true" } else { "false" }
$strikeout = if ($fontDef.strikeout -eq $true) { "true" } else { "false" }
$idx = $script:fontEntries.Count
$script:fontMap[$name] = $idx
$script:fontEntries += @{
Face = $face
Size = $size
Bold = $bold
Italic = $italic
Underline = $underline
Strikeout = $strikeout
}
}
# Add user-defined fonts
$hasDefault = $false
if ($def.fonts) {
foreach ($prop in $def.fonts.PSObject.Properties) {
if ($prop.Name -eq "default") { $hasDefault = $true }
Add-Font -name $prop.Name -fontDef $prop.Value
}
}
# Ensure default font exists
if (-not $hasDefault) {
$defaultDef = New-Object PSObject -Property @{ face = "Arial"; size = 10 }
Add-Font -name "default" -fontDef $defaultDef
}
# --- 3. Determine line palette ---
$hasThinBorders = $false
$hasThickBorders = $false
# Scan styles for border usage
if ($def.styles) {
foreach ($prop in $def.styles.PSObject.Properties) {
$s = $prop.Value
if ($s.border -and $s.border -ne "none") {
if ($s.borderWidth -eq "thick") {
$hasThickBorders = $true
} else {
$hasThinBorders = $true
}
}
}
}
$thinLineIndex = -1
$thickLineIndex = -1
$lineCount = 0
if ($hasThinBorders) {
$thinLineIndex = $lineCount; $lineCount++
}
if ($hasThickBorders) {
$thickLineIndex = $lineCount; $lineCount++
}
# --- 4. Parse column width specs ---
function Parse-ColumnSpec {
param([string]$spec)
$cols = @()
foreach ($part in $spec -split ',') {
$part = $part.Trim()
if ($part -match '^(\d+)-(\d+)$') {
$from = [int]$Matches[1]
$to = [int]$Matches[2]
for ($i = $from; $i -le $to; $i++) { $cols += $i }
} else {
$cols += [int]$part
}
}
return $cols
}
# --- 4a. Auto-calculate defaultWidth from page format ---
$pageTargets = @{
"A4-landscape" = 780
"A4-portrait" = 540
}
if ($def.page) {
$pageName = "$($def.page)"
$targetWidth = $null
if ($pageName -match '^\d+$') {
$targetWidth = [int]$pageName
} elseif ($pageTargets.ContainsKey($pageName)) {
$targetWidth = $pageTargets[$pageName]
} else {
Write-Warning "Unknown page format '$pageName'. Known: $($pageTargets.Keys -join ', '), or a number."
}
if ($targetWidth) {
$totalUnits = 0.0
$absoluteSum = 0
$specifiedCols = @{}
if ($def.columnWidths) {
foreach ($prop in $def.columnWidths.PSObject.Properties) {
$val = "$($prop.Value)"
$cols = Parse-ColumnSpec $prop.Name
foreach ($c in $cols) {
$specifiedCols[[int]$c] = $true
if ($val -match '^([0-9.]+)x$') {
$totalUnits += [double]$Matches[1]
} else {
$absoluteSum += [int]$val
}
}
}
}
for ($c = 1; $c -le $totalColumns; $c++) {
if (-not $specifiedCols.ContainsKey($c)) {
$totalUnits += 1.0
}
}
if ($totalUnits -gt 0) {
$defaultWidth = [int][math]::Round(($targetWidth - $absoluteSum) / $totalUnits)
}
}
}
# Build column width map: 1-based col -> width
$colWidthMap = @{}
if ($def.columnWidths) {
foreach ($prop in $def.columnWidths.PSObject.Properties) {
$val = "$($prop.Value)"
if ($val -match '^([0-9.]+)x$') {
$width = [int][math]::Round([double]$Matches[1] * $defaultWidth)
} else {
$width = [int]$val
}
$columns = Parse-ColumnSpec $prop.Name
foreach ($c in $columns) {
$colWidthMap[$c] = $width
}
}
}
# --- 5. Style resolver ---
function Resolve-Style {
param([string]$styleName, [string]$fillType)
$fontIdx = $fontMap["default"]
$lb = -1; $tb = -1; $rb = -1; $bb = -1
$ha = ""; $va = ""; $nf = ""
$wrap = $false
if ($styleName -and $def.styles) {
$style = $def.styles.$styleName
if ($style) {
# Font
if ($style.font -and $fontMap.Contains($style.font)) {
$fontIdx = $fontMap[$style.font]
}
# Borders
if ($style.border -and $style.border -ne "none") {
$lineIdx = if ($style.borderWidth -eq "thick") { $thickLineIndex } else { $thinLineIndex }
foreach ($side in ($style.border -split ',')) {
switch ($side.Trim()) {
"all" { $lb = $lineIdx; $tb = $lineIdx; $rb = $lineIdx; $bb = $lineIdx }
"left" { $lb = $lineIdx }
"top" { $tb = $lineIdx }
"right" { $rb = $lineIdx }
"bottom" { $bb = $lineIdx }
}
}
}
# Alignment
if ($style.align) {
switch ($style.align) {
"left" { $ha = "Left" }
"center" { $ha = "Center" }
"right" { $ha = "Right" }
}
}
if ($style.valign) {
switch ($style.valign) {
"top" { $va = "Top" }
"center" { $va = "Center" }
}
}
# Wrap
if ($style.wrap -eq $true) { $wrap = $true }
# Number format
if ($style.format) { $nf = $style.format }
}
}
return @{
FontIdx = $fontIdx
LB = $lb; TB = $tb; RB = $rb; BB = $bb
HA = $ha; VA = $va
Wrap = $wrap
FillType = $fillType
NumberFormat = $nf
}
}
# --- 6. Format palette builder ---
$formatRegistry = [ordered]@{} # key -> hashtable with properties
$formatOrder = @() # ordered keys for index assignment
function Get-FormatKey {
param(
[int]$fontIdx = -1,
[int]$lb = -1, [int]$tb = -1, [int]$rb = -1, [int]$bb = -1,
[string]$ha = "", [string]$va = "",
[bool]$wrap = $false,
[string]$fillType = "",
[string]$numberFormat = "",
[int]$width = -1,
[int]$height = -1
)
return "f=$fontIdx|lb=$lb|tb=$tb|rb=$rb|bb=$bb|ha=$ha|va=$va|wr=$wrap|ft=$fillType|nf=$numberFormat|w=$width|h=$height"
}
function Register-Format {
param([string]$key, [hashtable]$props)
if (-not $script:formatRegistry.Contains($key)) {
$script:formatRegistry[$key] = $props
$script:formatOrder += $key
}
# Return 1-based index
$idx = 0
foreach ($k in $script:formatRegistry.Keys) {
$idx++
if ($k -eq $key) { return $idx }
}
return $idx
}
# 6a. Default width format
$defaultFormatKey = Get-FormatKey -width $defaultWidth
$defaultFormatIndex = Register-Format -key $defaultFormatKey -props @{ Width = $defaultWidth }
# 6b. Column width formats
$colFormatMap = @{} # 1-based col -> format index
foreach ($col in ($colWidthMap.Keys | Sort-Object)) {
$w = $colWidthMap[$col]
$key = Get-FormatKey -width $w
$idx = Register-Format -key $key -props @{ Width = $w }
$colFormatMap[[int]$col] = $idx
}
# 6c. Scan areas for row heights and cell formats
# We need to do two passes: first collect all formats, then generate XML
# Helper: escape XML special characters
function Esc-Xml {
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
}
# Helper: determine fillType from cell content
function Get-FillType {
param($cell)
if ($cell.param) { return "Parameter" }
if ($cell.template) { return "Template" }
if ($cell.text) { return "Text" }
return ""
}
# Helper: register a cell format and return its index
function Register-CellFormat {
param($styleName, [string]$fillType)
$resolved = Resolve-Style -styleName $styleName -fillType $fillType
$key = Get-FormatKey -fontIdx $resolved.FontIdx `
-lb $resolved.LB -tb $resolved.TB -rb $resolved.RB -bb $resolved.BB `
-ha $resolved.HA -va $resolved.VA `
-wrap $resolved.Wrap -fillType $resolved.FillType `
-numberFormat $resolved.NumberFormat
$props = @{
FontIdx = $resolved.FontIdx
LB = $resolved.LB; TB = $resolved.TB
RB = $resolved.RB; BB = $resolved.BB
HA = $resolved.HA; VA = $resolved.VA
Wrap = $resolved.Wrap
FillType = $resolved.FillType
NumberFormat = $resolved.NumberFormat
}
return Register-Format -key $key -props $props
}
# Pre-register all formats from areas
foreach ($area in $def.areas) {
foreach ($row in $area.rows) {
# Skip empty row placeholder
if ($row.empty) { continue }
# Row height format
if ($row.height) {
$hKey = Get-FormatKey -height ([int]$row.height)
Register-Format -key $hKey -props @{ Height = [int]$row.height } | Out-Null
}
# rowStyle gap-fill format (no content → no fillType)
if ($row.rowStyle) {
Register-CellFormat -styleName $row.rowStyle -fillType "" | Out-Null
}
# Explicit cell formats
if ($row.cells) {
foreach ($cell in $row.cells) {
$cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" }
$ft = Get-FillType $cell
Register-CellFormat -styleName $cellStyle -fillType $ft | Out-Null
}
}
}
}
# --- 7. Generate XML ---
$xml = New-Object System.Text.StringBuilder 4096
function X {
param([string]$text)
$script:xml.AppendLine($text) | Out-Null
}
# 7a. Header
X '<?xml version="1.0" encoding="UTF-8"?>'
X '<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">'
# 7b. Language settings
X "`t<languageSettings>"
X "`t`t<currentLanguage>ru</currentLanguage>"
X "`t`t<defaultLanguage>ru</defaultLanguage>"
X "`t`t<languageInfo>"
X "`t`t`t<id>ru</id>"
X "`t`t`t<code>Русский</code>"
X "`t`t`t<description>Русский</description>"
X "`t`t</languageInfo>"
X "`t</languageSettings>"
# 7c. Columns
X "`t<columns>"
X "`t`t<size>$totalColumns</size>"
# Emit columnsItem for columns with non-default widths
foreach ($col in ($colFormatMap.Keys | Sort-Object)) {
$fmtIdx = $colFormatMap[$col]
$colIdx = $col - 1 # Convert to 0-based
X "`t`t<columnsItem>"
X "`t`t`t<index>$colIdx</index>"
X "`t`t`t<column>"
X "`t`t`t`t<formatIndex>$fmtIdx</formatIndex>"
X "`t`t`t</column>"
X "`t`t</columnsItem>"
}
X "`t</columns>"
# 7d. Rows — main generation loop
$globalRow = 0
$merges = @()
$namedItems = @()
$totalRowCount = 0
foreach ($area in $def.areas) {
$areaStartRow = $globalRow
$areaName = $area.name
$activeRowspans = @() # @{ColStart=1-based; ColEnd=1-based; EndLocalRow=int}
$localRow = 0
foreach ($row in $area.rows) {
# Empty row placeholder: emit N empty rows
if ($row.empty) {
$count = [int]$row.empty
for ($ei = 0; $ei -lt $count; $ei++) {
X "`t<rowsItem>"
X "`t`t<index>$globalRow</index>"
X "`t`t<row>"
X "`t`t`t<empty>true</empty>"
X "`t`t</row>"
X "`t</rowsItem>"
$globalRow++; $localRow++
}
continue
}
# Build set of columns occupied by rowspans from previous rows
$rowspanOccupied = @{} # 1-based col -> $true
foreach ($rs in $activeRowspans) {
if ($localRow -gt $rs.StartLocalRow -and $localRow -le $rs.EndLocalRow) {
for ($c = $rs.ColStart; $c -le $rs.ColEnd; $c++) {
$rowspanOccupied[$c] = $true
}
}
}
$rowHasContent = $false
$rowCells = @() # array of { Col(0-based), FormatIdx, Content }
# Determine row height format
$rowFormatIdx = 0
if ($row.height) {
$hKey = Get-FormatKey -height ([int]$row.height)
# Find format index for this key
$rIdx = 0
foreach ($k in $formatRegistry.Keys) {
$rIdx++
if ($k -eq $hKey) { $rowFormatIdx = $rIdx; break }
}
}
if ($row.cells -and $row.cells.Count -gt 0) {
$rowHasContent = $true
# Build set of occupied columns (1-based): explicit cells + rowspan from above
$occupiedCols = @{}
foreach ($rsk in $rowspanOccupied.Keys) { $occupiedCols[$rsk] = $true }
foreach ($cell in $row.cells) {
$colStart = [int]$cell.col
$colSpan = if ($cell.span) { [int]$cell.span } else { 1 }
for ($c = $colStart; $c -lt ($colStart + $colSpan); $c++) {
$occupiedCols[$c] = $true
}
}
# Generate explicit cells
foreach ($cell in $row.cells) {
$colStart = [int]$cell.col
$colSpan = if ($cell.span) { [int]$cell.span } else { 1 }
$rowspan = if ($cell.rowspan) { [int]$cell.rowspan } else { 1 }
$cellStyle = if ($cell.style) { $cell.style } elseif ($row.rowStyle) { $row.rowStyle } else { "default" }
$ft = Get-FillType $cell
$fmtIdx = Register-CellFormat -styleName $cellStyle -fillType $ft
$cellInfo = @{
Col = $colStart - 1 # 0-based
FormatIdx = $fmtIdx
Param = $cell.param
Detail = $cell.detail
Text = $cell.text
Template = $cell.template
}
$rowCells += $cellInfo
# Track rowspan for subsequent rows
if ($rowspan -gt 1) {
$activeRowspans += @{
ColStart = $colStart
ColEnd = $colStart + $colSpan - 1
StartLocalRow = $localRow
EndLocalRow = $localRow + $rowspan - 1
}
}
# Collect merge (horizontal, vertical, or both)
if ($colSpan -gt 1 -or $rowspan -gt 1) {
$merge = @{ R = $globalRow; C = $colStart - 1; W = $colSpan - 1 }
if ($rowspan -gt 1) { $merge.H = $rowspan - 1 }
$merges += $merge
}
}
# Generate gap-fill cells for rowStyle
if ($row.rowStyle) {
$gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType ""
for ($c = 1; $c -le $totalColumns; $c++) {
if (-not $occupiedCols.ContainsKey($c)) {
$rowCells += @{
Col = $c - 1 # 0-based
FormatIdx = $gapFmtIdx
Param = $null
Detail = $null
Text = $null
Template = $null
}
}
}
}
# Sort cells by column
$rowCells = $rowCells | Sort-Object { $_.Col }
} elseif ($row.rowStyle) {
# Row with only rowStyle, no explicit cells — fill non-rowspan columns
$rowHasContent = $true
$gapFmtIdx = Register-CellFormat -styleName $row.rowStyle -fillType ""
for ($c = 1; $c -le $totalColumns; $c++) {
if ($rowspanOccupied.ContainsKey($c)) { continue }
$rowCells += @{
Col = $c - 1
FormatIdx = $gapFmtIdx
Param = $null
Detail = $null
Text = $null
Template = $null
}
}
}
# Emit rowsItem
X "`t<rowsItem>"
X "`t`t<index>$globalRow</index>"
X "`t`t<row>"
if ($rowFormatIdx -gt 0) {
X "`t`t`t<formatIndex>$rowFormatIdx</formatIndex>"
}
if (-not $rowHasContent) {
X "`t`t`t<empty>true</empty>"
} else {
foreach ($cellInfo in $rowCells) {
X "`t`t`t<c>"
X "`t`t`t`t<i>$($cellInfo.Col)</i>"
X "`t`t`t`t<c>"
X "`t`t`t`t`t<f>$($cellInfo.FormatIdx)</f>"
if ($cellInfo.Param) {
X "`t`t`t`t`t<parameter>$($cellInfo.Param)</parameter>"
if ($cellInfo.Detail) {
X "`t`t`t`t`t<detailParameter>$($cellInfo.Detail)</detailParameter>"
}
}
if ($cellInfo.Text) {
X "`t`t`t`t`t<tl>"
X "`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellInfo.Text)</v8:content>"
X "`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t</tl>"
}
if ($cellInfo.Template) {
X "`t`t`t`t`t<tl>"
X "`t`t`t`t`t`t<v8:item>"
X "`t`t`t`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t`t`t`t<v8:content>$(Esc-Xml $cellInfo.Template)</v8:content>"
X "`t`t`t`t`t`t</v8:item>"
X "`t`t`t`t`t</tl>"
}
X "`t`t`t`t</c>"
X "`t`t`t</c>"
}
}
X "`t`t</row>"
X "`t</rowsItem>"
$localRow++
$globalRow++
}
$areaEndRow = $globalRow - 1
$namedItems += @{
Name = $areaName
BeginRow = $areaStartRow
EndRow = $areaEndRow
}
}
$totalRowCount = $globalRow
# 7e. Scalar metadata
X "`t<templateMode>true</templateMode>"
X "`t<defaultFormatIndex>$defaultFormatIndex</defaultFormatIndex>"
X "`t<height>$totalRowCount</height>"
X "`t<vgRows>$totalRowCount</vgRows>"
# 7f. Merges
foreach ($m in $merges) {
X "`t<merge>"
X "`t`t<r>$($m.R)</r>"
X "`t`t<c>$($m.C)</c>"
if ($m.H) { X "`t`t<h>$($m.H)</h>" }
X "`t`t<w>$($m.W)</w>"
X "`t</merge>"
}
# 7g. Named items
foreach ($ni in $namedItems) {
X "`t<namedItem xsi:type=`"NamedItemCells`">"
X "`t`t<name>$($ni.Name)</name>"
X "`t`t<area>"
X "`t`t`t<type>Rows</type>"
X "`t`t`t<beginRow>$($ni.BeginRow)</beginRow>"
X "`t`t`t<endRow>$($ni.EndRow)</endRow>"
X "`t`t`t<beginColumn>-1</beginColumn>"
X "`t`t`t<endColumn>-1</endColumn>"
X "`t`t</area>"
X "`t</namedItem>"
}
# 7h. Line palette
if ($hasThinBorders) {
X "`t<line width=`"1`" gap=`"false`">"
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
X "`t</line>"
}
if ($hasThickBorders) {
X "`t<line width=`"2`" gap=`"false`">"
X "`t`t<v8ui:style xsi:type=`"v8ui:SpreadsheetDocumentCellLineType`">Solid</v8ui:style>"
X "`t</line>"
}
# 7i. Font palette
foreach ($fe in $fontEntries) {
X "`t<font faceName=`"$($fe.Face)`" height=`"$($fe.Size)`" bold=`"$($fe.Bold)`" italic=`"$($fe.Italic)`" underline=`"$($fe.Underline)`" strikeout=`"$($fe.Strikeout)`" kind=`"Absolute`" scale=`"100`"/>"
}
# 7j. Format palette
foreach ($key in $formatRegistry.Keys) {
$fmt = $formatRegistry[$key]
X "`t<format>"
if ($fmt.FontIdx -ne $null -and $fmt.FontIdx -ge 0) {
X "`t`t<font>$($fmt.FontIdx)</font>"
}
if ($fmt.LB -ne $null -and $fmt.LB -ge 0) {
X "`t`t<leftBorder>$($fmt.LB)</leftBorder>"
}
if ($fmt.TB -ne $null -and $fmt.TB -ge 0) {
X "`t`t<topBorder>$($fmt.TB)</topBorder>"
}
if ($fmt.RB -ne $null -and $fmt.RB -ge 0) {
X "`t`t<rightBorder>$($fmt.RB)</rightBorder>"
}
if ($fmt.BB -ne $null -and $fmt.BB -ge 0) {
X "`t`t<bottomBorder>$($fmt.BB)</bottomBorder>"
}
if ($fmt.Width) {
X "`t`t<width>$($fmt.Width)</width>"
}
if ($fmt.Height) {
X "`t`t<height>$($fmt.Height)</height>"
}
if ($fmt.HA) {
X "`t`t<horizontalAlignment>$($fmt.HA)</horizontalAlignment>"
}
if ($fmt.VA) {
X "`t`t<verticalAlignment>$($fmt.VA)</verticalAlignment>"
}
if ($fmt.Wrap -eq $true) {
X "`t`t<textPlacement>Wrap</textPlacement>"
}
if ($fmt.FillType) {
X "`t`t<fillType>$($fmt.FillType)</fillType>"
}
if ($fmt.NumberFormat) {
X "`t`t<format>"
X "`t`t`t<v8:item>"
X "`t`t`t`t<v8:lang>ru</v8:lang>"
X "`t`t`t`t<v8:content>$(Esc-Xml $fmt.NumberFormat)</v8:content>"
X "`t`t`t</v8:item>"
X "`t`t</format>"
}
X "`t</format>"
}
# 7k. Close document
X '</document>'
# --- 8. Write output ---
$enc = New-Object System.Text.UTF8Encoding($true)
$resolvedPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
[System.IO.File]::WriteAllText($resolvedPath, $xml.ToString(), $enc)
# --- 9. Summary ---
Write-Host "[OK] Compiled: $OutputPath"
if ($def.page) {
Write-Host " Page: $pageName -> target $targetWidth, defaultWidth=$defaultWidth"
}
Write-Host " Areas: $($namedItems.Count), Rows: $totalRowCount, Columns: $totalColumns"
Write-Host " Fonts: $($fontEntries.Count), Lines: $lineCount, Formats: $($formatRegistry.Count)"
Write-Host " Merges: $($merges.Count)"
@@ -0,0 +1,636 @@
#!/usr/bin/env python3
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import math
import os
import re
import sys
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content)
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description='Compile 1C spreadsheet from JSON', allow_abbrev=False)
parser.add_argument('-JsonPath', type=str, required=True)
parser.add_argument('-OutputPath', type=str, required=True)
args = parser.parse_args()
# --- 1. Load and validate JSON ---
json_path = args.JsonPath
if not os.path.exists(json_path):
print(f"File not found: {json_path}", file=sys.stderr)
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.load(f)
if not defn.get('columns'):
print("Required field 'columns' is missing", file=sys.stderr)
sys.exit(1)
if not defn.get('areas'):
print("Required field 'areas' is missing", file=sys.stderr)
sys.exit(1)
total_columns = int(defn['columns'])
default_width = int(defn['defaultWidth']) if defn.get('defaultWidth') else 10
# --- 2. Build font palette ---
font_map = {} # name -> 0-based index
font_entries = [] # list of dicts
def add_font(name, font_def):
face = font_def.get('face', 'Arial') if font_def else 'Arial'
size = int(font_def.get('size', 10)) if font_def else 10
bold = 'true' if font_def and font_def.get('bold') is True else 'false'
italic = 'true' if font_def and font_def.get('italic') is True else 'false'
underline = 'true' if font_def and font_def.get('underline') is True else 'false'
strikeout = 'true' if font_def and font_def.get('strikeout') is True else 'false'
idx = len(font_entries)
font_map[name] = idx
font_entries.append({
'Face': face,
'Size': size,
'Bold': bold,
'Italic': italic,
'Underline': underline,
'Strikeout': strikeout,
})
# Add user-defined fonts
has_default = False
if defn.get('fonts'):
for fname, fdef in defn['fonts'].items():
if fname == 'default':
has_default = True
add_font(fname, fdef)
# Ensure default font exists
if not has_default:
add_font('default', {'face': 'Arial', 'size': 10})
# --- 3. Determine line palette ---
has_thin_borders = False
has_thick_borders = False
if defn.get('styles'):
for sname, sval in defn['styles'].items():
if sval.get('border') and sval['border'] != 'none':
if sval.get('borderWidth') == 'thick':
has_thick_borders = True
else:
has_thin_borders = True
thin_line_index = -1
thick_line_index = -1
line_count = 0
if has_thin_borders:
thin_line_index = line_count
line_count += 1
if has_thick_borders:
thick_line_index = line_count
line_count += 1
# --- 4. Parse column width specs ---
def parse_column_spec(spec):
cols = []
for part in spec.split(','):
part = part.strip()
m = re.match(r'^(\d+)-(\d+)$', part)
if m:
from_col = int(m.group(1))
to_col = int(m.group(2))
for i in range(from_col, to_col + 1):
cols.append(i)
else:
cols.append(int(part))
return cols
# --- 4a. Auto-calculate defaultWidth from page format ---
page_targets = {
'A4-landscape': 780,
'A4-portrait': 540,
}
page_name = None
target_width = None
if defn.get('page'):
page_name = str(defn['page'])
if re.match(r'^\d+$', page_name):
target_width = int(page_name)
elif page_name in page_targets:
target_width = page_targets[page_name]
else:
print(f"WARNING: Unknown page format '{page_name}'. Known: {', '.join(page_targets.keys())}, or a number.", file=sys.stderr)
if target_width:
total_units = 0.0
absolute_sum = 0
specified_cols = {}
if defn.get('columnWidths'):
for prop_name, prop_value in defn['columnWidths'].items():
val = str(prop_value)
cols = parse_column_spec(prop_name)
for c in cols:
specified_cols[int(c)] = True
m = re.match(r'^([0-9.]+)x$', val)
if m:
total_units += float(m.group(1))
else:
absolute_sum += int(val)
for c in range(1, total_columns + 1):
if c not in specified_cols:
total_units += 1.0
if total_units > 0:
default_width = round((target_width - absolute_sum) / total_units)
# Build column width map: 1-based col -> width
col_width_map = {}
if defn.get('columnWidths'):
for prop_name, prop_value in defn['columnWidths'].items():
val = str(prop_value)
m = re.match(r'^([0-9.]+)x$', val)
if m:
width = round(float(m.group(1)) * default_width)
else:
width = int(val)
columns = parse_column_spec(prop_name)
for c in columns:
col_width_map[c] = width
# --- 5. Style resolver ---
def resolve_style(style_name, fill_type):
font_idx = font_map.get('default', 0)
lb = -1; tb = -1; rb = -1; bb = -1
ha = ''; va = ''; nf = ''
wrap = False
if style_name and defn.get('styles'):
style = defn['styles'].get(style_name)
if style:
# Font
if style.get('font') and style['font'] in font_map:
font_idx = font_map[style['font']]
# Borders
if style.get('border') and style['border'] != 'none':
line_idx = thick_line_index if style.get('borderWidth') == 'thick' else thin_line_index
for side in style['border'].split(','):
side = side.strip()
if side == 'all':
lb = line_idx; tb = line_idx; rb = line_idx; bb = line_idx
elif side == 'left':
lb = line_idx
elif side == 'top':
tb = line_idx
elif side == 'right':
rb = line_idx
elif side == 'bottom':
bb = line_idx
# Alignment
if style.get('align'):
align_map = {'left': 'Left', 'center': 'Center', 'right': 'Right'}
ha = align_map.get(style['align'], '')
if style.get('valign'):
valign_map = {'top': 'Top', 'center': 'Center'}
va = valign_map.get(style['valign'], '')
# Wrap
if style.get('wrap') is True:
wrap = True
# Number format
if style.get('format'):
nf = style['format']
return {
'FontIdx': font_idx,
'LB': lb, 'TB': tb, 'RB': rb, 'BB': bb,
'HA': ha, 'VA': va,
'Wrap': wrap,
'FillType': fill_type,
'NumberFormat': nf,
}
# --- 6. Format palette builder ---
format_registry = {} # key -> props
format_order = [] # ordered keys for index assignment
def get_format_key(font_idx=-1, lb=-1, tb=-1, rb=-1, bb=-1, ha='', va='',
wrap=False, fill_type='', number_format='', width=-1, height=-1):
return f'f={font_idx}|lb={lb}|tb={tb}|rb={rb}|bb={bb}|ha={ha}|va={va}|wr={wrap}|ft={fill_type}|nf={number_format}|w={width}|h={height}'
def register_format(key, props):
if key not in format_registry:
format_registry[key] = props
format_order.append(key)
# Return 1-based index
return format_order.index(key) + 1
# 6a. Default width format
default_format_key = get_format_key(width=default_width)
default_format_index = register_format(default_format_key, {'Width': default_width})
# 6b. Column width formats
col_format_map = {} # 1-based col -> format index
for col in sorted(col_width_map):
w = col_width_map[col]
key = get_format_key(width=w)
idx = register_format(key, {'Width': w})
col_format_map[int(col)] = idx
# 6c. Helper: determine fillType from cell content
def get_fill_type(cell):
if cell.get('param'):
return 'Parameter'
if cell.get('template'):
return 'Template'
if cell.get('text'):
return 'Text'
return ''
# Helper: register a cell format and return its index
def register_cell_format(style_name, fill_type):
resolved = resolve_style(style_name, fill_type)
key = get_format_key(
font_idx=resolved['FontIdx'],
lb=resolved['LB'], tb=resolved['TB'], rb=resolved['RB'], bb=resolved['BB'],
ha=resolved['HA'], va=resolved['VA'],
wrap=resolved['Wrap'], fill_type=resolved['FillType'],
number_format=resolved['NumberFormat'])
props = {
'FontIdx': resolved['FontIdx'],
'LB': resolved['LB'], 'TB': resolved['TB'],
'RB': resolved['RB'], 'BB': resolved['BB'],
'HA': resolved['HA'], 'VA': resolved['VA'],
'Wrap': resolved['Wrap'],
'FillType': resolved['FillType'],
'NumberFormat': resolved['NumberFormat'],
}
return register_format(key, props)
# Pre-register all formats from areas
for area in defn['areas']:
for row in area.get('rows', []):
# Skip list-of-values shorthand rows (treated as empty rows like PS1)
if isinstance(row, list):
continue
# Skip empty row placeholder
if row.get('empty'):
continue
# Row height format
if row.get('height'):
h_key = get_format_key(height=int(row['height']))
register_format(h_key, {'Height': int(row['height'])})
# rowStyle gap-fill format
if row.get('rowStyle'):
register_cell_format(row['rowStyle'], '')
# Explicit cell formats
if row.get('cells'):
for cell in row['cells']:
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
ft = get_fill_type(cell)
register_cell_format(cell_style, ft)
# --- 7. Generate XML ---
lines = []
# 7a. Header
lines.append('<?xml version="1.0" encoding="UTF-8"?>')
lines.append('<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">')
# 7b. Language settings
lines.append('\t<languageSettings>')
lines.append('\t\t<currentLanguage>ru</currentLanguage>')
lines.append('\t\t<defaultLanguage>ru</defaultLanguage>')
lines.append('\t\t<languageInfo>')
lines.append('\t\t\t<id>ru</id>')
lines.append('\t\t\t<code>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</code>')
lines.append('\t\t\t<description>\u0420\u0443\u0441\u0441\u043a\u0438\u0439</description>')
lines.append('\t\t</languageInfo>')
lines.append('\t</languageSettings>')
# 7c. Columns
lines.append('\t<columns>')
lines.append(f'\t\t<size>{total_columns}</size>')
# Emit columnsItem for columns with non-default widths
for col in sorted(col_format_map.keys()):
fmt_idx = col_format_map[col]
col_idx = col - 1 # Convert to 0-based
lines.append('\t\t<columnsItem>')
lines.append(f'\t\t\t<index>{col_idx}</index>')
lines.append('\t\t\t<column>')
lines.append(f'\t\t\t\t<formatIndex>{fmt_idx}</formatIndex>')
lines.append('\t\t\t</column>')
lines.append('\t\t</columnsItem>')
lines.append('\t</columns>')
# 7d. Rows -- main generation loop
global_row = 0
merges = []
named_items = []
active_rowspans = [] # list of {ColStart, ColEnd, StartLocalRow, EndLocalRow}
for area in defn['areas']:
area_start_row = global_row
area_name = area.get('name', '')
active_rowspans = []
local_row = 0
for row in area.get('rows', []):
# List-of-values shorthand: treat as row with no properties (like PS1)
if isinstance(row, list):
row = {}
# Empty row placeholder: emit N empty rows
if row.get('empty'):
count = int(row['empty'])
for ei in range(count):
lines.append('\t<rowsItem>')
lines.append(f'\t\t<index>{global_row}</index>')
lines.append('\t\t<row>')
lines.append('\t\t\t<empty>true</empty>')
lines.append('\t\t</row>')
lines.append('\t</rowsItem>')
global_row += 1
local_row += 1
continue
# Build set of columns occupied by rowspans from previous rows
rowspan_occupied = {}
for rs in active_rowspans:
if local_row > rs['StartLocalRow'] and local_row <= rs['EndLocalRow']:
for c in range(rs['ColStart'], rs['ColEnd'] + 1):
rowspan_occupied[c] = True
row_has_content = False
row_cells = []
# Determine row height format
row_format_idx = 0
if row.get('height'):
h_key = get_format_key(height=int(row['height']))
if h_key in format_registry:
row_format_idx = format_order.index(h_key) + 1
if row.get('cells') and len(row['cells']) > 0:
row_has_content = True
# Build set of occupied columns (1-based)
occupied_cols = dict(rowspan_occupied)
for cell in row['cells']:
col_start = int(cell['col'])
col_span = int(cell.get('span', 1))
for c in range(col_start, col_start + col_span):
occupied_cols[c] = True
# Generate explicit cells
for cell in row['cells']:
col_start = int(cell['col'])
col_span = int(cell.get('span', 1))
rowspan = int(cell.get('rowspan', 1))
cell_style = cell.get('style') or row.get('rowStyle') or 'default'
ft = get_fill_type(cell)
fmt_idx = register_cell_format(cell_style, ft)
cell_info = {
'Col': col_start - 1, # 0-based
'FormatIdx': fmt_idx,
'Param': cell.get('param'),
'Detail': cell.get('detail'),
'Text': cell.get('text'),
'Template': cell.get('template'),
}
row_cells.append(cell_info)
# Track rowspan for subsequent rows
if rowspan > 1:
active_rowspans.append({
'ColStart': col_start,
'ColEnd': col_start + col_span - 1,
'StartLocalRow': local_row,
'EndLocalRow': local_row + rowspan - 1,
})
# Collect merge
if col_span > 1 or rowspan > 1:
merge = {'R': global_row, 'C': col_start - 1, 'W': col_span - 1}
if rowspan > 1:
merge['H'] = rowspan - 1
merges.append(merge)
# Generate gap-fill cells for rowStyle
if row.get('rowStyle'):
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
for c in range(1, total_columns + 1):
if c not in occupied_cols:
row_cells.append({
'Col': c - 1,
'FormatIdx': gap_fmt_idx,
'Param': None,
'Detail': None,
'Text': None,
'Template': None,
})
# Sort cells by column
row_cells.sort(key=lambda x: x['Col'])
elif row.get('rowStyle'):
# Row with only rowStyle, no explicit cells
row_has_content = True
gap_fmt_idx = register_cell_format(row['rowStyle'], '')
for c in range(1, total_columns + 1):
if c in rowspan_occupied:
continue
row_cells.append({
'Col': c - 1,
'FormatIdx': gap_fmt_idx,
'Param': None,
'Detail': None,
'Text': None,
'Template': None,
})
# Emit rowsItem
lines.append('\t<rowsItem>')
lines.append(f'\t\t<index>{global_row}</index>')
lines.append('\t\t<row>')
if row_format_idx > 0:
lines.append(f'\t\t\t<formatIndex>{row_format_idx}</formatIndex>')
if not row_has_content:
lines.append('\t\t\t<empty>true</empty>')
else:
for cell_info in row_cells:
lines.append('\t\t\t<c>')
lines.append(f'\t\t\t\t<i>{cell_info["Col"]}</i>')
lines.append('\t\t\t\t<c>')
lines.append(f'\t\t\t\t\t<f>{cell_info["FormatIdx"]}</f>')
if cell_info['Param']:
lines.append(f'\t\t\t\t\t<parameter>{cell_info["Param"]}</parameter>')
if cell_info['Detail']:
lines.append(f'\t\t\t\t\t<detailParameter>{cell_info["Detail"]}</detailParameter>')
if cell_info['Text']:
lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Text"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>')
if cell_info['Template']:
lines.append('\t\t\t\t\t<tl>')
lines.append('\t\t\t\t\t\t<v8:item>')
lines.append('\t\t\t\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t\t\t\t<v8:content>{esc_xml(cell_info["Template"])}</v8:content>')
lines.append('\t\t\t\t\t\t</v8:item>')
lines.append('\t\t\t\t\t</tl>')
lines.append('\t\t\t\t</c>')
lines.append('\t\t\t</c>')
lines.append('\t\t</row>')
lines.append('\t</rowsItem>')
local_row += 1
global_row += 1
area_end_row = global_row - 1
named_items.append({
'Name': area_name,
'BeginRow': area_start_row,
'EndRow': area_end_row,
})
total_row_count = global_row
# 7e. Scalar metadata
lines.append(f'\t<templateMode>true</templateMode>')
lines.append(f'\t<defaultFormatIndex>{default_format_index}</defaultFormatIndex>')
lines.append(f'\t<height>{total_row_count}</height>')
lines.append(f'\t<vgRows>{total_row_count}</vgRows>')
# 7f. Merges
for m in merges:
lines.append('\t<merge>')
lines.append(f'\t\t<r>{m["R"]}</r>')
lines.append(f'\t\t<c>{m["C"]}</c>')
if m.get('H'):
lines.append(f'\t\t<h>{m["H"]}</h>')
lines.append(f'\t\t<w>{m["W"]}</w>')
lines.append('\t</merge>')
# 7g. Named items
for ni in named_items:
lines.append('\t<namedItem xsi:type="NamedItemCells">')
lines.append(f'\t\t<name>{ni["Name"]}</name>')
lines.append('\t\t<area>')
lines.append('\t\t\t<type>Rows</type>')
lines.append(f'\t\t\t<beginRow>{ni["BeginRow"]}</beginRow>')
lines.append(f'\t\t\t<endRow>{ni["EndRow"]}</endRow>')
lines.append('\t\t\t<beginColumn>-1</beginColumn>')
lines.append('\t\t\t<endColumn>-1</endColumn>')
lines.append('\t\t</area>')
lines.append('\t</namedItem>')
# 7h. Line palette
if has_thin_borders:
lines.append('\t<line width="1" gap="false">')
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
lines.append('\t</line>')
if has_thick_borders:
lines.append('\t<line width="2" gap="false">')
lines.append('\t\t<v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">Solid</v8ui:style>')
lines.append('\t</line>')
# 7i. Font palette
for fe in font_entries:
lines.append(f'\t<font faceName="{fe["Face"]}" height="{fe["Size"]}" bold="{fe["Bold"]}" italic="{fe["Italic"]}" underline="{fe["Underline"]}" strikeout="{fe["Strikeout"]}" kind="Absolute" scale="100"/>')
# 7j. Format palette
for key in format_order:
fmt = format_registry[key]
lines.append('\t<format>')
if fmt.get('FontIdx') is not None and fmt.get('FontIdx', -1) >= 0:
lines.append(f'\t\t<font>{fmt["FontIdx"]}</font>')
if fmt.get('LB') is not None and fmt.get('LB', -1) >= 0:
lines.append(f'\t\t<leftBorder>{fmt["LB"]}</leftBorder>')
if fmt.get('TB') is not None and fmt.get('TB', -1) >= 0:
lines.append(f'\t\t<topBorder>{fmt["TB"]}</topBorder>')
if fmt.get('RB') is not None and fmt.get('RB', -1) >= 0:
lines.append(f'\t\t<rightBorder>{fmt["RB"]}</rightBorder>')
if fmt.get('BB') is not None and fmt.get('BB', -1) >= 0:
lines.append(f'\t\t<bottomBorder>{fmt["BB"]}</bottomBorder>')
if fmt.get('Width'):
lines.append(f'\t\t<width>{fmt["Width"]}</width>')
if fmt.get('Height'):
lines.append(f'\t\t<height>{fmt["Height"]}</height>')
if fmt.get('HA'):
lines.append(f'\t\t<horizontalAlignment>{fmt["HA"]}</horizontalAlignment>')
if fmt.get('VA'):
lines.append(f'\t\t<verticalAlignment>{fmt["VA"]}</verticalAlignment>')
if fmt.get('Wrap') is True:
lines.append('\t\t<textPlacement>Wrap</textPlacement>')
if fmt.get('FillType'):
lines.append(f'\t\t<fillType>{fmt["FillType"]}</fillType>')
if fmt.get('NumberFormat'):
lines.append('\t\t<format>')
lines.append('\t\t\t<v8:item>')
lines.append('\t\t\t\t<v8:lang>ru</v8:lang>')
lines.append(f'\t\t\t\t<v8:content>{esc_xml(fmt["NumberFormat"])}</v8:content>')
lines.append('\t\t\t</v8:item>')
lines.append('\t\t</format>')
lines.append('\t</format>')
# 7k. Close document
lines.append('</document>')
# --- 8. Write output ---
out_path = args.OutputPath
if not os.path.isabs(out_path):
out_path = os.path.join(os.getcwd(), out_path)
out_dir = os.path.dirname(out_path)
if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir, exist_ok=True)
content = '\n'.join(lines) + '\n'
write_utf8_bom(out_path, content)
# --- 9. Summary ---
print(f"[OK] Compiled: {args.OutputPath}")
if defn.get('page'):
print(f" Page: {page_name} -> target {target_width}, defaultWidth={default_width}")
print(f" Areas: {len(named_items)}, Rows: {total_row_count}, Columns: {total_columns}")
print(f" Fonts: {len(font_entries)}, Lines: {line_count}, Formats: {len(format_registry)}")
print(f" Merges: {len(merges)}")
if __name__ == '__main__':
main()