feat(mxl-compile,mxl-decompile): колонтитулы и параметры печати

Ключи header и footer описывают колонтитул: три слота (left, center,
right), общий шрифт и вертикальное выравнивание, признак вывода и
страница, с которой печатать. Ключ printSettings — плоский набор
параметров печати с именами тегов платформы; порядок компилятор
выставляет сам, незнакомый ключ отвергает.

Слот устроен как ячейка: ссылка на формат плюс текст. Текст бывает двух
видов, и это не текст против шаблона, а обычная строка против
форматированной: у форматированной разметка живёт прямо в содержимом
(<b>жирный</>, <fontsize 12>, <colorstyle -16>), поэтому она возится как
есть. Признак вывода и стартовая страница лежат в формате как <height>
и <width>, но читаются только у записи с <height>: палитра
дедуплицирована, и колонтитул без своих настроек ссылается на чужую
запись, где <width> — ширина колонки (637 таких ссылок против 153 своих).

Перенос строки внутри текста колонтитула платформа хранит голым LF при
CRLF во всём остальном файле — единственное такое место в документе,
поэтому содержимое пишется одной строкой вывода.

Формат колонтитула регистрируется до отсева неиспользуемых шрифтов:
иначе шрифт, на который ссылается только колонтитул, выбрасывался бы
из палитры.

Стенд Колонтитулы собирается байт в байт обоими портами и добавлен
в платформенные фикстуры.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-15 17:07:54 +03:00
co-authored by Claude Opus 5
parent 1e0b82d419
commit 702a3b6a8d
16 changed files with 881 additions and 8 deletions
+3
View File
@@ -64,6 +64,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J
namedAreas: [{ name, rows, cols }],
rowGroups: [{ rows, name, collapsed, titleLocation }],
columnGroups: [{ cols, name, collapsed, titleLocation }],
header: { left, center, right, font, verticalAlignment, show, startPage }, footer: { … },
printSettings: { pageOrientation, topMargin, …, fitToPage, firstPageNumber },
columnSets: { name: { columns, columnWidths, columnStyles } }
}
```
@@ -83,6 +85,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J
- `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки)
- Содержимое ячейки задаётся одним из ключей: `param` — параметр заполнения, `text` — статический текст, `template` — текст со вставками `[Параметр]`
- `rowGroups`/`columnGroups` — группы строк и колонок (сворачиваются кнопкой на полях): плоский список диапазонов, вложенность выражена вхождением одного в другой
- `header`/`footer` — колонтитулы: три слота (`left`/`center`/`right`), общий шрифт и вертикальное выравнивание; текст может нести поля `[&НомерСтраницы]`, `[&Дата]`
- `note` — всплывающая подсказка у ячейки: строка, объект языков или `{ text, style, autoSize, box }`
- `valueType` делает ячейку полем ввода (`"Number(15,3,nonneg)"`, `"String(10)"`, `"CatalogRef.Валюты"`, составной через ` + `); текста в такой ячейке быть не может, значение задаётся ключом `value`
@@ -89,6 +89,8 @@
| `columnSets` | нет | — | Дополнительные колоночные раскладки (см. `styles.md`) |
| `rowGroups` | нет | — | Группы строк (см. ниже) |
| `columnGroups` | нет | — | Группы колонок (см. ниже) |
| `header` / `footer` | нет | — | Верхний и нижний колонтитулы (см. ниже) |
| `printSettings` | нет | — | Параметры печати (см. ниже) |
## Области (`areas[]`)
@@ -296,6 +298,56 @@
Координаты ячейки в примечании не задаются. `autoSize` и `box` независимы: при автоподгоне
размера положение окошка всё равно хранится.
## Колонтитулы
У верхнего и нижнего колонтитула по три слота — `left`, `center`, `right` — и общие настройки:
```json
"header": {
"font": "мелкий",
"verticalAlignment": "Bottom",
"startPage": 2,
"left": "Слева\nвторая строка",
"center": "Стр. [&НомерСтраницы] из [&СтраницВсего]",
"right": { "ru": "Справа", "en": "Right" }
},
"footer": {
"show": false,
"center": { "formatted": "<b>Итог</> <colorstyle -16>красным</>" }
}
```
| Поле | По умолч. | Описание |
|------|-----------|----------|
| `left`, `center`, `right` | — | Текст слота: строка, объект «язык → текст» или `{ "formatted": … }` |
| `font` | — | Имя шрифта из `fonts` — на весь колонтитул |
| `verticalAlignment` | — | Положение текста по вертикали: `Top`, `Center`, `Bottom` |
| `show` | `true` | Выводить ли колонтитул |
| `startPage` | `1` | Страница, с которой колонтитул печатается |
Текст может быть многострочным (`\n`) и содержать поля `[&НомерСтраницы]`, `[&СтраницВсего]`,
`[&Дата]`, `[&Время]` — платформа подставляет их при печати. `{ "formatted": … }` — форматированная
строка: разметка живёт прямо в тексте (`<b>жирный</>`, `<fontsize 12>`, `<colorstyle -16>`).
## Параметры печати
Плоский объект; имя ключа совпадает с именем свойства в выгрузке:
```json
"printSettings": {
"pageOrientation": "Landscape",
"topMargin": 500, "leftMargin": 500, "bottomMargin": 500, "rightMargin": 500,
"headerSize": 1000, "footerSize": 1000,
"fitToPage": true, "firstPageNumber": 7
}
```
Допустимые ключи: `pageOrientation`, `scale`, `collate`, `copies`, `perPage`, `topMargin`,
`leftMargin`, `bottomMargin`, `rightMargin`, `headerSize`, `footerSize`, `fitToPage`,
`blackAndWhite`, `printerName`, `paper`, `paperSource`, `pageWidth`, `pageHeight`, `duplexType`,
`pagePlacementAlternation`, `firstPageNumber`. Незнакомый ключ — ошибка. Порядок в объекте не
важен: компилятор пишет свойства в том порядке, что и платформа.
## Группы строк и колонок
Диапазон строк или колонок, который сворачивается кнопкой на полях. Задаются плоским списком;
@@ -339,7 +391,7 @@ XML не возвращается.
- объединения, не привязанные к ячейке (по всей высоте или ширине документа);
- рисунки и картинки, в том числе штрихкоды;
- колонтитулы, параметры печати, область печати.
- область печати.
Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому
diff после round-trip обычно шире фактической доработки.
@@ -1,4 +1,4 @@
# mxl-compile v1.44 — Compile 1C spreadsheet from JSON
# mxl-compile v1.45 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -1485,9 +1485,108 @@ foreach ($area in $def.areas) {
}
}
# 7d-ter. Колонтитулы. Шесть слотов: left/center/right у верхнего и нижнего. Слот устроен
# как ячейка — ссылка на формат плюс текст, — а формат несёт то, что в диалоге задаётся на весь
# колонтитул: шрифт, вертикальное выравнивание, признак вывода (<height> 1/-1) и страницу,
# с которой колонтитул печатается (<width>).
$script:headerSlots = @('left', 'center', 'right')
function Get-HeaderFormatIndex {
param($hf)
if ($null -eq $hf) { return 0 }
$props = @{}
if ($hf.font -and $fontMap.Contains("$($hf.font)")) { $props['font'] = $fontMap["$($hf.font)"] }
# Признак вывода и стартовая страница осмысленны только вместе: платформа пишет их парой.
$props['height'] = if ($hf.show -eq $false -or "$($hf.show)" -eq 'false') { -1 } else { 1 }
$props['width'] = if ($hf.startPage) { [int]$hf.startPage } else { 1 }
if ($hf.verticalAlignment) { $props['verticalAlignment'] = "$($hf.verticalAlignment)" }
return Register-Format $props
}
function Emit-HeaderSlot {
param([string]$tag, $value, [int]$fmtIdx)
if ($null -eq $value) { return }
# Форматированная строка платформы — тот же текст, только разметка живёт внутри содержимого
# (<b>жирный</>, <fontsize 12>…), поэтому возим её как есть и различаем именем тега.
$formatted = $false
$text = $value
if ($value -is [System.Management.Automation.PSCustomObject] -and $null -ne $value.formatted) {
$formatted = $true
$text = $value.formatted
}
$pairs = @()
if ($text -is [System.Collections.IDictionary]) {
foreach ($k in $text.Keys) { $pairs += @{ Lang = "$k"; Text = "$($text[$k])" } }
} elseif ($text -is [System.Management.Automation.PSCustomObject]) {
foreach ($pr in $text.PSObject.Properties) { $pairs += @{ Lang = $pr.Name; Text = "$($pr.Value)" } }
} else {
foreach ($l in $textLanguages) { $pairs += @{ Lang = $l; Text = "$text" } }
}
$textTag = if ($formatted) { 'tfl' } else { 'tl' }
X "`t<$tag>"
X "`t`t<f>$fmtIdx</f>"
if ($pairs.Count -eq 0) {
X "`t`t<$textTag/>"
} else {
X "`t`t<$textTag>"
foreach ($pr in $pairs) {
X "`t`t`t<v8:item>"
X "`t`t`t`t<v8:lang>$($pr.Lang)</v8:lang>"
# Перенос строки внутри колонтитула платформа хранит голым LF, тогда как весь файл
# идёт CRLF. Пишем содержимое одной строкой вывода, чтобы перенос не переписался.
X "`t`t`t`t<v8:content>$(Esc-XmlText $pr.Text)</v8:content>"
X "`t`t`t</v8:item>"
}
X "`t`t</$textTag>"
}
X "`t</$tag>"
}
# Параметры печати. Имя ключа совпадает с именем тега в выгрузке, как у свойств стиля.
# Порядок снят с корпуса (2350 макетов): платформа пишет теги строго в этой последовательности.
function Get-PrintSettingsOrder {
return @(
'pageOrientation', 'scale', 'collate', 'copies', 'perPage',
'topMargin', 'leftMargin', 'bottomMargin', 'rightMargin', 'headerSize', 'footerSize',
'fitToPage', 'blackAndWhite', 'printerName', 'paper', 'paperSource',
'pageWidth', 'pageHeight', 'duplexType', 'pagePlacementAlternation', 'firstPageNumber'
)
}
function Emit-PrintSettings {
param($ps)
if ($null -eq $ps) { return }
$order = Get-PrintSettingsOrder
$known = @{}
foreach ($k in $order) { $known[$k] = $true }
foreach ($pr in $ps.PSObject.Properties) {
if (-not $known.ContainsKey($pr.Name)) {
[Console]::Error.WriteLine("printSettings: unknown key `"$($pr.Name)`". Allowed: $($order -join ', ')")
exit 1
}
}
$emitted = @()
foreach ($tag in $order) {
$v = $ps.$tag
if ($null -eq $v) { continue }
if ($v -is [bool]) { $v = if ($v) { 'true' } else { 'false' } }
$emitted += "`t`t<$tag>$(Esc-XmlText "$v")</$tag>"
}
if ($emitted.Count -eq 0) { return }
X "`t<printSettings>"
foreach ($line in $emitted) { X $line }
X "`t</printSettings>"
}
# Формат по умолчанию — последняя запись палитры (см. выше).
$defaultFormatIndex = Register-Format @{ width = $defaultWidth }
# Формат колонтитула заводим здесь же: он идёт в палитре следом за умолчанием, и он
# обязан попасть в реестр ДО отсева неиспользуемых шрифтов — иначе шрифт колонтитула,
# на который больше никто не ссылается, будет выброшен.
$headerFmt = Get-HeaderFormatIndex $def.header
$footerFmt = Get-HeaderFormatIndex $def.footer
# Шрифт, на который не ссылается ни один формат, платформа в палитру не кладёт: у макета
# без оформления элемента <font> нет вовсе. Мы же всегда заводили Arial 10 по умолчанию.
# Отбрасываем неиспользуемые и перенумеровываем ссылки — индексы шрифтов позиционные.
@@ -2037,6 +2136,14 @@ if ($def.namedAreas) {
}
}
# 7d-ter. Колонтитулы: шесть слотов идут после строк и перед скалярными свойствами документа.
foreach ($slot in $script:headerSlots) {
if ($def.header) { Emit-HeaderSlot "$($slot)Header" $def.header.$slot $headerFmt }
}
foreach ($slot in $script:headerSlots) {
if ($def.footer) { Emit-HeaderSlot "$($slot)Footer" $def.footer.$slot $footerFmt }
}
# 7e-bis. Группировки строк и колонок. Платформа хранит их плоским списком диапазонов;
# вложенность выражена вхождением одного диапазона в другой, а частичных пересечений не
# бывает вовсе (корпус: 40 620 886 пар непересекающихся, 599 958 вложенных, 0 частичных).
@@ -2156,6 +2263,8 @@ X "`t<vgRows>$totalRowCount</vgRows>"
Emit-Groups 'vg' $rowGroups
Emit-Groups 'hg' $colGroups
Emit-PrintSettings $def.printSettings
# 7f. Merges
foreach ($m in $merges) {
X "`t<merge>"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.44 — Compile 1C spreadsheet from JSON
# mxl-compile v1.45 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import hashlib
@@ -1462,9 +1462,92 @@ def main():
if note:
register_note_format(note)
# Колонтитулы. Шесть слотов: left/center/right у верхнего и нижнего. Слот устроен как
# ячейка — ссылка на формат плюс текст, — а формат несёт то, что в диалоге задаётся на весь
# колонтитул: шрифт, вертикальное выравнивание, признак вывода (<height> 1/-1) и страницу,
# с которой колонтитул печатается (<width>).
def header_format_index(hf):
if not hf:
return 0
props = {}
if hf.get('font') and str(hf['font']) in font_map:
props['font'] = font_map[str(hf['font'])]
# Признак вывода и стартовая страница осмысленны только вместе: платформа пишет их парой.
props['height'] = -1 if (hf.get('show') is False or str(hf.get('show')).lower() == 'false') else 1
props['width'] = int(hf['startPage']) if hf.get('startPage') else 1
if hf.get('verticalAlignment'):
props['verticalAlignment'] = str(hf['verticalAlignment'])
return register_format(props)
def emit_header_slot(lines, tag, value, fmt_idx):
if value is None:
return
# Форматированная строка платформы — тот же текст, только разметка живёт внутри
# содержимого (<b>жирный</>, <fontsize 12>…), поэтому возим её как есть и различаем
# именем тега.
formatted = isinstance(value, dict) and value.get('formatted') is not None
text = value['formatted'] if formatted else value
if isinstance(text, dict):
pairs = [(str(k), str(v)) for k, v in text.items()]
else:
pairs = [(lang, str(text)) for lang in text_languages]
text_tag = 'tfl' if formatted else 'tl'
lines.append(f'\t<{tag}>')
lines.append(f'\t\t<f>{fmt_idx}</f>')
if not pairs:
lines.append(f'\t\t<{text_tag}/>')
else:
lines.append(f'\t\t<{text_tag}>')
for lang, content in pairs:
lines.append('\t\t\t<v8:item>')
lines.append(f'\t\t\t\t<v8:lang>{lang}</v8:lang>')
# Перенос строки внутри колонтитула платформа хранит голым LF, тогда как весь
# файл идёт CRLF. Содержимое кладём одним элементом списка, чтобы перенос
# не переписался при склейке строк документа.
lines.append(f'\t\t\t\t<v8:content>{esc_xml_text(content)}</v8:content>')
lines.append('\t\t\t</v8:item>')
lines.append(f'\t\t</{text_tag}>')
lines.append(f'\t</{tag}>')
# Параметры печати. Имя ключа совпадает с именем тега в выгрузке, как у свойств стиля.
# Порядок снят с корпуса (2350 макетов): платформа пишет теги строго в этой последовательности.
def print_settings_order():
return ('pageOrientation', 'scale', 'collate', 'copies', 'perPage',
'topMargin', 'leftMargin', 'bottomMargin', 'rightMargin', 'headerSize', 'footerSize',
'fitToPage', 'blackAndWhite', 'printerName', 'paper', 'paperSource',
'pageWidth', 'pageHeight', 'duplexType', 'pagePlacementAlternation', 'firstPageNumber')
def emit_print_settings(lines, ps):
if not ps:
return
order = print_settings_order()
for key in ps:
if key not in order:
print(f'printSettings: unknown key "{key}". Allowed: {", ".join(order)}', file=sys.stderr)
sys.exit(1)
emitted = []
for tag in order:
v = ps.get(tag)
if v is None:
continue
if isinstance(v, bool):
v = 'true' if v else 'false'
emitted.append(f'\t\t<{tag}>{esc_xml_text(str(v))}</{tag}>')
if not emitted:
return
lines.append('\t<printSettings>')
lines.extend(emitted)
lines.append('\t</printSettings>')
# Формат по умолчанию — последняя запись палитры (см. выше).
default_format_index = register_format({'width': default_width})
# Формат колонтитула заводим здесь же: он идёт в палитре следом за умолчанием, и он
# обязан попасть в реестр ДО отсева неиспользуемых шрифтов — иначе шрифт колонтитула,
# на который больше никто не ссылается, будет выброшен.
header_fmt = header_format_index(defn.get('header'))
footer_fmt = header_format_index(defn.get('footer'))
# Шрифт, на который не ссылается ни один формат, платформа в палитру не кладёт: у макета
# без оформления элемента <font> нет вовсе. Мы же всегда заводили Arial 10 по умолчанию.
# Отбрасываем неиспользуемые и перенумеровываем ссылки — индексы шрифтов позиционные.
@@ -1953,6 +2036,14 @@ def main():
'EndCol': cols[1] - 1 if cols else -1,
})
# 7d-ter. Колонтитулы: шесть слотов идут после строк и перед скалярными свойствами документа.
for slot in ('left', 'center', 'right'):
if defn.get('header'):
emit_header_slot(lines, f'{slot}Header', defn['header'].get(slot), header_fmt)
for slot in ('left', 'center', 'right'):
if defn.get('footer'):
emit_header_slot(lines, f'{slot}Footer', defn['footer'].get(slot), footer_fmt)
# 7e-bis. Группировки строк и колонок. Платформа хранит их плоским списком диапазонов;
# вложенность выражена вхождением одного диапазона в другой, а частичных пересечений не
# бывает вовсе (корпус: 40 620 886 пар непересекающихся, 599 958 вложенных, 0 частичных).
@@ -2063,6 +2154,8 @@ def main():
emit_groups(lines, 'vg', row_groups)
emit_groups(lines, 'hg', col_groups)
emit_print_settings(lines, defn.get('printSettings'))
# 7f. Merges
for m in merges:
lines.append('\t<merge>')
@@ -1,4 +1,4 @@
# mxl-decompile v1.24 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.25 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -265,6 +265,68 @@ function Get-Format {
return $rawFormats[$idx - 1]
}
# --- 4a. Колонтитулы и параметры печати ---
# Слот колонтитула устроен как ячейка: ссылка на формат плюс текст. Признак вывода и стартовую
# страницу читаем только у записи, где есть <height>: палитра дедуплицирована, и колонтитул без
# своих настроек ссылается на чужую запись, где <width> — ширина колонки, а не номер страницы.
function Read-HeaderPart {
param([string]$suffix)
$out = [ordered]@{}
$fmtIdx = $null
foreach ($slot in @('left', 'center', 'right')) {
$el = $root.SelectSingleNode("d:$slot$suffix", $ns)
if (-not $el) { continue }
if ($null -eq $fmtIdx) {
$fNode = $el.SelectSingleNode("d:f", $ns)
$fmtIdx = if ($fNode) { [int]$fNode.InnerText } else { 0 }
}
foreach ($textTag in @('tl', 'tfl')) {
$t = $el.SelectSingleNode("d:$textTag", $ns)
if (-not $t) { continue }
$byLang = [ordered]@{}
foreach ($it in $t.SelectNodes("v8:item", $ns)) {
$l = $it.SelectSingleNode("v8:lang", $ns)
$cnt = $it.SelectSingleNode("v8:content", $ns)
$byLang[$(if ($l) { $l.InnerText } else { '' })] = $(if ($cnt) { $cnt.InnerText } else { '' })
}
$value = Get-DslText $byLang
$out[$slot] = if ($textTag -ceq 'tfl') { [ordered]@{ formatted = $value } } else { $value }
break
}
}
if ($out.Count -eq 0 -and $null -eq $fmtIdx) { return $null }
$fmt = if ($fmtIdx) { Get-Format $fmtIdx } else { $null }
if ($fmt) {
$props = $fmt.Props
$head = [ordered]@{}
if ($fmt.FontIdx -ge 0) { $head["font"] = $fontNames[$fmt.FontIdx] }
if ($props.Contains('height')) {
if ("$($props['height'])" -ceq '-1') { $head["show"] = $false }
$w = if ($props.Contains('width')) { [int]$props['width'] } else { 1 }
if ($w -ne 1) { $head["startPage"] = $w }
}
if ($props.Contains('verticalAlignment')) { $head["verticalAlignment"] = $props['verticalAlignment'] }
foreach ($k in $out.Keys) { $head[$k] = $out[$k] }
$out = $head
}
return $out
}
$printSettings = $null
$psNode = $root.SelectSingleNode("d:printSettings", $ns)
if ($psNode) {
$printSettings = [ordered]@{}
foreach ($c in $psNode.ChildNodes) {
if ($c.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
$v = $c.InnerText.Trim()
$i = 0
if ($v -ceq 'true') { $printSettings[$c.get_LocalName()] = $true }
elseif ($v -ceq 'false') { $printSettings[$c.get_LocalName()] = $false }
elseif ([int]::TryParse($v, [ref]$i)) { $printSettings[$c.get_LocalName()] = $i }
else { $printSettings[$c.get_LocalName()] = $v }
}
}
# --- 4b. Группировки строк и колонок ---
# Платформа хранит их плоским списком диапазонов, родитель раньше детей. Число уровней
# (<vgLevels>) не читаем: оно выводится из вложенности — совпало на всех 1797 макетах.
@@ -1477,6 +1539,14 @@ foreach ($g in ($rowGroups + $colGroups)) {
if ($rowGroups.Count -gt 0) { $result["rowGroups"] = [array]$rowGroups }
if ($colGroups.Count -gt 0) { $result["columnGroups"] = [array]$colGroups }
# Читаем здесь, а не в разделе 4a: тексту колонтитула нужен свод языков макета, который
# становится известен только после разбора строк.
$headerPart = Read-HeaderPart 'Header'
$footerPart = Read-HeaderPart 'Footer'
if ($headerPart) { $result["header"] = $headerPart }
if ($footerPart) { $result["footer"] = $footerPart }
if ($printSettings) { $result["printSettings"] = $printSettings }
$result["areas"] = [array]$dslAreas
# Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-decompile v1.24 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.25 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -431,6 +431,67 @@ def main():
return None
return raw_formats[idx - 1]
# --- 4a. Колонтитулы и параметры печати ---
# Слот колонтитула устроен как ячейка: ссылка на формат плюс текст. Признак вывода и стартовую
# страницу читаем только у записи, где есть <height>: палитра дедуплицирована, и колонтитул без
# своих настроек ссылается на чужую запись, где <width> — ширина колонки, а не номер страницы.
def read_header_part(suffix):
slots = OrderedDict()
fmt_idx = None
for slot in ('left', 'center', 'right'):
el = find(root, f'd:{slot}{suffix}')
if el is None:
continue
if fmt_idx is None:
fmt_idx = int_of(find(el, 'd:f'))
for text_tag in ('tl', 'tfl'):
t = find(el, 'd:' + text_tag)
if t is None:
continue
by_lang = OrderedDict()
for it in findall(t, 'v8:item'):
lang = text_of(find(it, 'v8:lang')) or ''
by_lang[lang] = text_of(find(it, 'v8:content')) or ''
value = get_dsl_text(by_lang)
slots[slot] = OrderedDict([('formatted', value)]) if text_tag == 'tfl' else value
break
if not slots and fmt_idx is None:
return None
fmt = get_format(fmt_idx) if fmt_idx else None
if not fmt:
return slots
head = OrderedDict()
if fmt["FontIdx"] >= 0:
head["font"] = font_names.get(fmt["FontIdx"], "default")
props = fmt["Props"]
if 'height' in props:
if str(props['height']) == '-1':
head["show"] = False
width = int(props.get('width', 1))
if width != 1:
head["startPage"] = width
if 'verticalAlignment' in props:
head["verticalAlignment"] = props['verticalAlignment']
head.update(slots)
return head
print_settings = None
ps_node = find(root, 'd:printSettings')
if ps_node is not None:
print_settings = OrderedDict()
for c in ps_node:
name = etree.QName(c).localname
v = (c.text or '').strip()
if v == 'true':
print_settings[name] = True
elif v == 'false':
print_settings[name] = False
else:
try:
print_settings[name] = int(v)
except ValueError:
print_settings[name] = v
# --- 4b. Группировки строк и колонок ---
# Платформа хранит их плоским списком диапазонов, родитель раньше детей. Число уровней
# (<vgLevels>) не читаем: оно выводится из вложенности — совпало на всех 1797 макетах.
@@ -1472,6 +1533,17 @@ def main():
if col_groups:
result["columnGroups"] = col_groups
# Читаем здесь, а не в разделе 4a: тексту колонтитула нужен свод языков макета, который
# становится известен только после разбора строк.
header_part = read_header_part('Header')
footer_part = read_header_part('Footer')
if header_part:
result["header"] = header_part
if footer_part:
result["footer"] = footer_part
if print_settings:
result["printSettings"] = print_settings
result["areas"] = dsl_areas
# Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится
+35 -2
View File
@@ -601,7 +601,40 @@ current-config не объявляет, поэтому вынести объяв
обратные, ячейки продолжают ссылаться на записи со старыми ширинами. Из итогового XML это
значение не выводится — воспроизводить его не нужно.
## Группировки строк и колонок
## Колонтитулы и параметры печати
```xml
<leftHeader>
<f>2</f> <!-- формат: шрифт, выравнивание, признак вывода, стартовая страница -->
<tl><v8:item><v8:lang>ru</v8:lang><v8:content>Слева</v8:content></v8:item></tl>
</leftHeader>
<printSettings>
<topMargin>1000</topMargin><firstPageNumber>15</firstPageNumber>
</printSettings>
```
Шесть элементов идут после строк и перед `<templateMode>`: `leftHeader`, `centerHeader`,
`rightHeader`, `leftFooter`, `centerFooter`, `rightFooter`. Пишутся не комплектом: `leftHeader`
есть у 1 013 макетов, остальные — у 677–790.
| Наблюдение | Данные корпуса |
|---|---|
| текст слота | `<tl>` — обычная строка, `<tfl>` — форматированная (разметка внутри содержимого: `<b>…</>`, `<fontsize 12>`, `<colorstyle -16>`) |
| формат слота | `<height>` = признак вывода (`1` / `-1`), `<width>` = страница, с которой печатать, плюс шрифт и `verticalAlignment` |
| формат общий на колонтитул | у 674 макетов из 677; разный у трёх |
| формат делится с другими владельцами | 637 против 153 «только колонтитулы» — палитра дедуплицирована, поэтому `<width>` в чужой записи означает ширину колонки, а не страницу |
**Ловушка:** перенос строки внутри текста колонтитула платформа хранит голым LF, тогда как весь
остальной файл идёт CRLF. Это единственное место документа с другим переводом строки.
`printSettings` (2 350 макетов) — плоский набор в фиксированном порядке: `pageOrientation` ·
`scale` · `collate` · `copies` · `perPage` · `topMargin` · `leftMargin` · `bottomMargin` ·
`rightMargin` · `headerSize` · `footerSize` · `fitToPage` · `blackAndWhite` · `printerName` ·
`paper` · `paperSource` · `pageWidth` · `pageHeight` · `duplexType` · `pagePlacementAlternation` ·
`firstPageNumber`. Номер первой страницы принадлежит именно параметрам печати, а не колонтитулу.
## Группы строк и колонок
```xml
<vgLevels>2</vgLevels>
@@ -616,7 +649,7 @@ current-config не объявляет, поэтому вынести объяв
<hg></hg> <!-- то же для колонок -->
```
Строки группируются часто (1 797 макетов, 170 967 группировок), колонки — почти никогда
Строки группируются часто (1 797 макетов, 170 967 групп), колонки — почти никогда
(1 макет, 1 группировка). Порядок документных тегов: `height``vgLevels``vgRows``vg`
`hg`; `vgLevels` пишется только при наличии группировок.
+53 -1
View File
@@ -89,6 +89,8 @@
| `columnSets` | нет | — | Дополнительные колоночные раскладки (см. `mxl-dsl-styles.md`) |
| `rowGroups` | нет | — | Группы строк (см. ниже) |
| `columnGroups` | нет | — | Группы колонок (см. ниже) |
| `header` / `footer` | нет | — | Верхний и нижний колонтитулы (см. ниже) |
| `printSettings` | нет | — | Параметры печати (см. ниже) |
## Области (`areas[]`)
@@ -302,6 +304,56 @@
и компилятор проставляет это сам. `autoSize` и `box` независимы: при автоподгоне размера
положение окошка всё равно хранится.
## Колонтитулы
У верхнего и нижнего колонтитула по три слота — `left`, `center`, `right` — и общие настройки:
```json
"header": {
"font": "мелкий",
"verticalAlignment": "Bottom",
"startPage": 2,
"left": "Слева\nвторая строка",
"center": "Стр. [&НомерСтраницы] из [&СтраницВсего]",
"right": { "ru": "Справа", "en": "Right" }
},
"footer": {
"show": false,
"center": { "formatted": "<b>Итог</> <colorstyle -16>красным</>" }
}
```
| Поле | По умолч. | Описание |
|------|-----------|----------|
| `left`, `center`, `right` | — | Текст слота: строка, объект «язык → текст» или `{ "formatted": … }` |
| `font` | — | Имя шрифта из `fonts` — на весь колонтитул |
| `verticalAlignment` | — | Положение текста по вертикали: `Top`, `Center`, `Bottom` |
| `show` | `true` | Выводить ли колонтитул |
| `startPage` | `1` | Страница, с которой колонтитул печатается |
Текст может быть многострочным (`\n`) и содержать поля `[&НомерСтраницы]`, `[&СтраницВсего]`,
`[&Дата]`, `[&Время]` — платформа подставляет их при печати. `{ "formatted": … }` — форматированная
строка: разметка живёт прямо в тексте (`<b>жирный</>`, `<fontsize 12>`, `<colorstyle -16>`).
## Параметры печати
Плоский объект; имя ключа совпадает с именем свойства в выгрузке:
```json
"printSettings": {
"pageOrientation": "Landscape",
"topMargin": 500, "leftMargin": 500, "bottomMargin": 500, "rightMargin": 500,
"headerSize": 1000, "footerSize": 1000,
"fitToPage": true, "firstPageNumber": 7
}
```
Допустимые ключи: `pageOrientation`, `scale`, `collate`, `copies`, `perPage`, `topMargin`,
`leftMargin`, `bottomMargin`, `rightMargin`, `headerSize`, `footerSize`, `fitToPage`,
`blackAndWhite`, `printerName`, `paper`, `paperSource`, `pageWidth`, `pageHeight`, `duplexType`,
`pagePlacementAlternation`, `firstPageNumber`. Незнакомый ключ — ошибка. Порядок в объекте не
важен: компилятор пишет свойства в том порядке, что и платформа.
## Группы строк и колонок
Диапазон строк или колонок, который сворачивается кнопкой на полях. Задаются плоским списком;
@@ -347,7 +399,7 @@ XML не возвращается.
- объединения, не привязанные к ячейке (по всей высоте или ширине документа);
- рисунки и картинки, в том числе штрихкоды;
- колонтитулы, параметры печати, область печати.
- область печати.
Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому
diff после round-trip обычно шире фактической доработки.
@@ -0,0 +1,10 @@
{
"name": "Ошибка: неизвестный ключ параметров печати",
"input": {
"columns": 1,
"areas": [{ "rows": [["a"]] }],
"printSettings": { "topMargin": 500, "marginTop": 500 }
},
"params": { "outputPath": "Template.xml" },
"expectError": "printSettings: unknown key \"marginTop\""
}
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<languageSettings>
<currentLanguage>ru</currentLanguage>
<defaultLanguage>ru</defaultLanguage>
<languageInfo>
<id>ru</id>
<code>Русский</code>
<description>Русский</description>
</languageInfo>
<languageInfo>
<id>en</id>
<code>Английский</code>
<description>Английский</description>
</languageInfo>
</languageSettings>
<columns>
<size>1</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Текст</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<leftHeader>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Слева</v8:content>
</v8:item>
</tl>
</leftHeader>
<centerHeader>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Стр. [&amp;НомерСтраницы] из [&amp;СтраницВсего]. Дата [&amp;Дата][&amp;Время]</v8:content>
</v8:item>
</tl>
</centerHeader>
<rightHeader>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Справа</v8:content>
</v8:item>
</tl>
</rightHeader>
<leftFooter>
<f>3</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Нижний слева</v8:content>
</v8:item>
</tl>
</leftFooter>
<centerFooter>
<f>3</f>
<tfl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>&lt;b&gt;Форматируемая&lt;/&gt; &lt;fontsize 12&gt;&lt;colorstyle -16&gt;строка&lt;/&gt;&lt;/&gt;</v8:content>
</v8:item>
</tfl>
</centerFooter>
<rightFooter>
<f>3</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Нижний справа</v8:content>
</v8:item>
</tl>
</rightFooter>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>1</height>
<vgRows>1</vgRows>
<printSettings>
<topMargin>1000</topMargin>
<leftMargin>1000</leftMargin>
<bottomMargin>1000</bottomMargin>
<rightMargin>1000</rightMargin>
<headerSize>1200</headerSize>
<footerSize>1000</footerSize>
<firstPageNumber>15</firstPageNumber>
</printSettings>
<font faceName="Arial" height="8" bold="false" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
<format>
<width>72</width>
</format>
<format>
<font>0</font>
<height>-1</height>
<width>2</width>
<verticalAlignment>Center</verticalAlignment>
</format>
<format>
<font>0</font>
<height>1</height>
<width>21</width>
<verticalAlignment>Top</verticalAlignment>
</format>
</document>
@@ -0,0 +1,29 @@
{
"name": "Колонтитулы и параметры печати",
"input": {
"columns": 2,
"fonts": { "мелкий": { "face": "Arial", "size": 8 } },
"areas": [{ "rows": [["Отчёт"]] }],
"header": {
"font": "мелкий",
"verticalAlignment": "Bottom",
"startPage": 2,
"left": "Слева\nвторая строка",
"center": "Стр. [&НомерСтраницы] из [&СтраницВсего]",
"right": { "ru": "Справа", "en": "Right" }
},
"footer": {
"font": "мелкий",
"verticalAlignment": "Top",
"show": false,
"center": { "formatted": "<b>Итог</> <colorstyle -16>красным</>" }
},
"printSettings": {
"pageOrientation": "Landscape",
"topMargin": 500, "leftMargin": 500, "bottomMargin": 500, "rightMargin": 500,
"headerSize": 1000, "footerSize": 1000,
"fitToPage": true, "blackAndWhite": false, "firstPageNumber": 7
}
},
"params": { "outputPath": "Template.xml" }
}
@@ -0,0 +1,13 @@
{
"name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: Колонтитулы",
"setup": "fixture:platform-roundtrip",
"preRun": [
{ "script": "mxl-decompile/scripts/mxl-decompile",
"args": { "-TemplatePath": "Колонтитулы.xml", "-OutputPath": "back.json" },
"cwd": "{workDir}" }
],
"params": { "outputPath": "out.xml" },
"noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual",
"expect": { "filesEqual": { "actual": "out.xml", "expected": "Колонтитулы.xml" } },
"inputFrom": "back.json"
}
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<languageSettings>
<currentLanguage>ru</currentLanguage>
<defaultLanguage>ru</defaultLanguage>
<languageInfo>
<id>ru</id>
<code>Русский</code>
<description>Русский</description>
</languageInfo>
</languageSettings>
<columns>
<size>2</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Отчёт</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<leftHeader>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Слева
вторая строка</v8:content>
</v8:item>
</tl>
</leftHeader>
<centerHeader>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Стр. [&amp;НомерСтраницы] из [&amp;СтраницВсего]</v8:content>
</v8:item>
</tl>
</centerHeader>
<rightHeader>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Справа</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Right</v8:content>
</v8:item>
</tl>
</rightHeader>
<centerFooter>
<f>3</f>
<tfl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>&lt;b&gt;Итог&lt;/&gt; &lt;colorstyle -16&gt;красным&lt;/&gt;</v8:content>
</v8:item>
</tfl>
</centerFooter>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>1</height>
<vgRows>1</vgRows>
<printSettings>
<pageOrientation>Landscape</pageOrientation>
<topMargin>500</topMargin>
<leftMargin>500</leftMargin>
<bottomMargin>500</bottomMargin>
<rightMargin>500</rightMargin>
<headerSize>1000</headerSize>
<footerSize>1000</footerSize>
<fitToPage>true</fitToPage>
<blackAndWhite>false</blackAndWhite>
<firstPageNumber>7</firstPageNumber>
</printSettings>
<font faceName="Arial" height="8" bold="false" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
<format>
<width>10</width>
</format>
<format>
<font>0</font>
<height>1</height>
<width>2</width>
<verticalAlignment>Bottom</verticalAlignment>
</format>
<format>
<font>0</font>
<height>-1</height>
<width>1</width>
<verticalAlignment>Top</verticalAlignment>
</format>
</document>
@@ -0,0 +1,21 @@
{
"name": "Roundtrip — колонтитулы и параметры печати",
"preRun": [
{ "script": "mxl-compile/scripts/mxl-compile",
"input": {
"columns": 2,
"fonts": { "мелкий": { "face": "Arial", "size": 8 } },
"areas": [{ "rows": [["Отчёт"]] }],
"header": { "font": "мелкий", "verticalAlignment": "Bottom", "startPage": 2,
"left": "Слева\nвторая строка", "center": "Стр. [&НомерСтраницы]" },
"footer": { "font": "мелкий", "show": false,
"center": { "formatted": "<b>Итог</>" } },
"printSettings": { "pageOrientation": "Landscape", "fitToPage": true, "firstPageNumber": 7 }
},
"args": { "-JsonPath": "{inputFile}", "-OutputPath": "Template.xml" },
"cwd": "{workDir}" }
],
"params": { "templatePath": "Template.xml" },
"args_extra": ["-OutputPath", "{workDir}/back.json"],
"expect": { "files": ["back.json"], "stdoutContains": "[OK] Decompiled:" }
}
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<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">
<languageSettings>
<currentLanguage>ru</currentLanguage>
<defaultLanguage>ru</defaultLanguage>
<languageInfo>
<id>ru</id>
<code>Русский</code>
<description>Русский</description>
</languageInfo>
</languageSettings>
<columns>
<size>2</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Отчёт</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<leftHeader>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Слева
вторая строка</v8:content>
</v8:item>
</tl>
</leftHeader>
<centerHeader>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Стр. [&amp;НомерСтраницы]</v8:content>
</v8:item>
</tl>
</centerHeader>
<centerFooter>
<f>3</f>
<tfl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>&lt;b&gt;Итог&lt;/&gt;</v8:content>
</v8:item>
</tfl>
</centerFooter>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>1</height>
<vgRows>1</vgRows>
<printSettings>
<pageOrientation>Landscape</pageOrientation>
<fitToPage>true</fitToPage>
<firstPageNumber>7</firstPageNumber>
</printSettings>
<font faceName="Arial" height="8" bold="false" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
<format>
<width>10</width>
</format>
<format>
<font>0</font>
<height>1</height>
<width>2</width>
<verticalAlignment>Bottom</verticalAlignment>
</format>
<format>
<font>0</font>
<height>-1</height>
<width>1</width>
</format>
</document>
@@ -0,0 +1,10 @@
{
"columns": 2,
"defaultWidth": 10,
"fonts": { "default": { "face": "Arial", "size": 8 } },
"styles": {},
"header": { "font": "default", "startPage": 2, "verticalAlignment": "Bottom", "left": "Слева\nвторая строка", "center": "Стр. [&НомерСтраницы]" },
"footer": { "font": "default", "show": false, "center": { "formatted": "<b>Итог</>" } },
"printSettings": { "pageOrientation": "Landscape", "fitToPage": true, "firstPageNumber": 7 },
"areas": [{ "rows": [["Отчёт"]] }]
}