# mxl-compile v1.3 — 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 # --- Support guard (Ext/ParentConfigurations.bin) --- # See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" / # read-only configs unless allowed. Trigger = bin present; reaction from # .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never # throws — guard errors degrade to allow. function Get-RootUuid([string]$xmlPath) { if (-not (Test-Path $xmlPath)) { return $null } try { [xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8 $el = $mx.DocumentElement.FirstChild while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling } if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } } } catch {} return $null } function Find-V8Project([string]$startDir) { $d = $startDir for ($i = 0; $i -lt 20 -and $d; $i++) { $pj = Join-Path $d ".v8-project.json" if (Test-Path $pj) { return $pj } $parent = [System.IO.Path]::GetDirectoryName($d) if ($parent -eq $d) { break } $d = $parent } return $null } function Get-EditMode([string]$cfgDir) { try { $pj = Find-V8Project (Get-Location).Path if (-not $pj) { $pj = Find-V8Project $cfgDir } if (-not $pj) { return 'deny' } $proj = Get-Content -Raw $pj | ConvertFrom-Json $cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/') if ($proj.databases) { foreach ($db in $proj.databases) { if ($db.configSrc) { $src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/') if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) { if ($db.editingAllowedCheck) { return $db.editingAllowedCheck } } } } } if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck } return 'deny' } catch { return 'deny' } } function Assert-EditAllowed([string]$targetPath, [string]$require) { try { $rp = $targetPath try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {} $elemUuid = Get-RootUuid $rp $cfgDir = $null; $binPath = $null $d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) } for ($i = 0; $i -lt 12 -and $d; $i++) { if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" } if (-not $cfgDir) { $cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin" if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand } } if ($elemUuid -and $cfgDir) { break } $parent = [System.IO.Path]::GetDirectoryName($d) if ($parent -eq $d) { break } $d = $parent } # New object (no element file): fall back to config root uuid. if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") } if (-not $binPath -or -not (Test-Path $binPath)) { return } $bytes = [System.IO.File]::ReadAllBytes($binPath) if ($bytes.Length -le 32) { return } $start = 0 if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } $text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start) $hm = [regex]::Match($text, '^\{6,(\d+),(\d+),') if (-not $hm.Success) { return } $G = [int]$hm.Groups[1].Value $K = [int]$hm.Groups[2].Value if ($K -eq 0) { return } $best = $null if ($elemUuid) { $u = [regex]::Escape($elemUuid.ToLower()) foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) { $f1 = [int]$m.Groups[1].Value if ($null -eq $best -or $f1 -lt $best) { $best = $f1 } } } $blocked = $false; $code = ""; $reason = "" if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" } elseif ($require -eq 'removed') { if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" } } else { if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" } } if (-not $blocked) { return } $mode = Get-EditMode $cfgDir if ($mode -eq 'off') { return } # Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the # latter throws and would be swallowed by this function's own catch. if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return } $head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления." $cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются." $offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json." if ($code -eq "capability-off") { $state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя." $fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора." } elseif ($code -eq "not-removed") { $state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора." $fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно." } else { $state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)." $fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят." } [Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote") exit 1 } catch { return } } # --- 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 '' X '' # 7b. Language settings X "`t" X "`t`tru" X "`t`tru" X "`t`t" X "`t`t`tru" X "`t`t`tРусский" X "`t`t`tРусский" X "`t`t" X "`t" # 7c. Columns X "`t" X "`t`t$totalColumns" # 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" X "`t`t`t$colIdx" X "`t`t`t" X "`t`t`t`t$fmtIdx" X "`t`t`t" X "`t`t" } X "`t" # 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" X "`t`t$globalRow" X "`t`t" X "`t`t`ttrue" X "`t`t" X "`t" $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" X "`t`t$globalRow" X "`t`t" if ($rowFormatIdx -gt 0) { X "`t`t`t$rowFormatIdx" } if (-not $rowHasContent) { X "`t`t`ttrue" } else { foreach ($cellInfo in $rowCells) { X "`t`t`t" X "`t`t`t`t$($cellInfo.Col)" X "`t`t`t`t" X "`t`t`t`t`t$($cellInfo.FormatIdx)" if ($cellInfo.Param) { X "`t`t`t`t`t$($cellInfo.Param)" if ($cellInfo.Detail) { X "`t`t`t`t`t$($cellInfo.Detail)" } } if ($cellInfo.Text) { X "`t`t`t`t`t" X "`t`t`t`t`t`t" X "`t`t`t`t`t`t`tru" X "`t`t`t`t`t`t`t$(Esc-Xml $cellInfo.Text)" X "`t`t`t`t`t`t" X "`t`t`t`t`t" } if ($cellInfo.Template) { X "`t`t`t`t`t" X "`t`t`t`t`t`t" X "`t`t`t`t`t`t`tru" X "`t`t`t`t`t`t`t$(Esc-Xml $cellInfo.Template)" X "`t`t`t`t`t`t" X "`t`t`t`t`t" } X "`t`t`t`t" X "`t`t`t" } } X "`t`t" X "`t" $localRow++ $globalRow++ } $areaEndRow = $globalRow - 1 $namedItems += @{ Name = $areaName BeginRow = $areaStartRow EndRow = $areaEndRow } } $totalRowCount = $globalRow # 7e. Scalar metadata X "`ttrue" X "`t$defaultFormatIndex" X "`t$totalRowCount" X "`t$totalRowCount" # 7f. Merges foreach ($m in $merges) { X "`t" X "`t`t$($m.R)" X "`t`t$($m.C)" if ($m.H) { X "`t`t$($m.H)" } X "`t`t$($m.W)" X "`t" } # 7g. Named items foreach ($ni in $namedItems) { X "`t" X "`t`t$($ni.Name)" X "`t`t" X "`t`t`tRows" X "`t`t`t$($ni.BeginRow)" X "`t`t`t$($ni.EndRow)" X "`t`t`t-1" X "`t`t`t-1" X "`t`t" X "`t" } # 7h. Line palette if ($hasThinBorders) { X "`t" X "`t`tSolid" X "`t" } if ($hasThickBorders) { X "`t" X "`t`tSolid" X "`t" } # 7i. Font palette foreach ($fe in $fontEntries) { X "`t" } # 7j. Format palette foreach ($key in $formatRegistry.Keys) { $fmt = $formatRegistry[$key] X "`t" if ($fmt.FontIdx -ne $null -and $fmt.FontIdx -ge 0) { X "`t`t$($fmt.FontIdx)" } if ($fmt.LB -ne $null -and $fmt.LB -ge 0) { X "`t`t$($fmt.LB)" } if ($fmt.TB -ne $null -and $fmt.TB -ge 0) { X "`t`t$($fmt.TB)" } if ($fmt.RB -ne $null -and $fmt.RB -ge 0) { X "`t`t$($fmt.RB)" } if ($fmt.BB -ne $null -and $fmt.BB -ge 0) { X "`t`t$($fmt.BB)" } if ($fmt.Width) { X "`t`t$($fmt.Width)" } if ($fmt.Height) { X "`t`t$($fmt.Height)" } if ($fmt.HA) { X "`t`t$($fmt.HA)" } if ($fmt.VA) { X "`t`t$($fmt.VA)" } if ($fmt.Wrap -eq $true) { X "`t`tWrap" } if ($fmt.FillType) { X "`t`t$($fmt.FillType)" } if ($fmt.NumberFormat) { X "`t`t" X "`t`t`t" X "`t`t`t`tru" X "`t`t`t`t$(Esc-Xml $fmt.NumberFormat)" X "`t`t`t" X "`t`t" } X "`t" } # 7k. Close document X '' # --- 8. Write output --- $enc = New-Object System.Text.UTF8Encoding($true) $resolvedPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath } Assert-EditAllowed $resolvedPath 'editable' [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)"