mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-12 08:54:57 +03:00
Auto-build: copilot (python) from 7fa279c
This commit is contained in:
@@ -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('&','&').Replace('<','<').Replace('>','>').Replace('"','"')
|
||||
}
|
||||
|
||||
# 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('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
|
||||
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user