diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index 930d4eab..2b0c5850 100644 --- a/.claude/skills/mxl-compile/SKILL.md +++ b/.claude/skills/mxl-compile/SKILL.md @@ -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` diff --git a/.claude/skills/mxl-compile/reference/dsl-spec.md b/.claude/skills/mxl-compile/reference/dsl-spec.md index 2368a0b5..da7cb268 100644 --- a/.claude/skills/mxl-compile/reference/dsl-spec.md +++ b/.claude/skills/mxl-compile/reference/dsl-spec.md @@ -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": "Итог красным" } +} +``` + +| Поле | По умолч. | Описание | +|------|-----------|----------| +| `left`, `center`, `right` | — | Текст слота: строка, объект «язык → текст» или `{ "formatted": … }` | +| `font` | — | Имя шрифта из `fonts` — на весь колонтитул | +| `verticalAlignment` | — | Положение текста по вертикали: `Top`, `Center`, `Bottom` | +| `show` | `true` | Выводить ли колонтитул | +| `startPage` | `1` | Страница, с которой колонтитул печатается | + +Текст может быть многострочным (`\n`) и содержать поля `[&НомерСтраницы]`, `[&СтраницВсего]`, +`[&Дата]`, `[&Время]` — платформа подставляет их при печати. `{ "formatted": … }` — форматированная +строка: разметка живёт прямо в тексте (`жирный`, ``, ``). + +## Параметры печати + +Плоский объект; имя ключа совпадает с именем свойства в выгрузке: + +```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 обычно шире фактической доработки. diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 86a10285..aabab98e 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -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 у верхнего и нижнего. Слот устроен +# как ячейка — ссылка на формат плюс текст, — а формат несёт то, что в диалоге задаётся на весь +# колонтитул: шрифт, вертикальное выравнивание, признак вывода ( 1/-1) и страницу, +# с которой колонтитул печатается (). +$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 } + # Форматированная строка платформы — тот же текст, только разметка живёт внутри содержимого + # (жирный, …), поэтому возим её как есть и различаем именем тега. + $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$fmtIdx" + if ($pairs.Count -eq 0) { + X "`t`t<$textTag/>" + } else { + X "`t`t<$textTag>" + foreach ($pr in $pairs) { + X "`t`t`t" + X "`t`t`t`t$($pr.Lang)" + # Перенос строки внутри колонтитула платформа хранит голым LF, тогда как весь файл + # идёт CRLF. Пишем содержимое одной строкой вывода, чтобы перенос не переписался. + X "`t`t`t`t$(Esc-XmlText $pr.Text)" + X "`t`t`t" + } + X "`t`t" + } + X "`t" +} + +# Параметры печати. Имя ключа совпадает с именем тега в выгрузке, как у свойств стиля. +# Порядок снят с корпуса (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")" + } + if ($emitted.Count -eq 0) { return } + X "`t" + foreach ($line in $emitted) { X $line } + X "`t" +} + # Формат по умолчанию — последняя запись палитры (см. выше). $defaultFormatIndex = Register-Format @{ width = $defaultWidth } +# Формат колонтитула заводим здесь же: он идёт в палитре следом за умолчанием, и он +# обязан попасть в реестр ДО отсева неиспользуемых шрифтов — иначе шрифт колонтитула, +# на который больше никто не ссылается, будет выброшен. +$headerFmt = Get-HeaderFormatIndex $def.header +$footerFmt = Get-HeaderFormatIndex $def.footer + # Шрифт, на который не ссылается ни один формат, платформа в палитру не кладёт: у макета # без оформления элемента нет вовсе. Мы же всегда заводили 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$totalRowCount" Emit-Groups 'vg' $rowGroups Emit-Groups 'hg' $colGroups +Emit-PrintSettings $def.printSettings + # 7f. Merges foreach ($m in $merges) { X "`t" diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 78fe6b5b..4eb60656 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.py +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.py @@ -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 у верхнего и нижнего. Слот устроен как + # ячейка — ссылка на формат плюс текст, — а формат несёт то, что в диалоге задаётся на весь + # колонтитул: шрифт, вертикальное выравнивание, признак вывода ( 1/-1) и страницу, + # с которой колонтитул печатается (). + 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 + # Форматированная строка платформы — тот же текст, только разметка живёт внутри + # содержимого (жирный, …), поэтому возим её как есть и различаем + # именем тега. + 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{fmt_idx}') + 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') + lines.append(f'\t\t\t\t{lang}') + # Перенос строки внутри колонтитула платформа хранит голым LF, тогда как весь + # файл идёт CRLF. Содержимое кладём одним элементом списка, чтобы перенос + # не переписался при склейке строк документа. + lines.append(f'\t\t\t\t{esc_xml_text(content)}') + lines.append('\t\t\t') + lines.append(f'\t\t') + lines.append(f'\t') + + # Параметры печати. Имя ключа совпадает с именем тега в выгрузке, как у свойств стиля. + # Порядок снят с корпуса (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))}') + if not emitted: + return + lines.append('\t') + lines.extend(emitted) + lines.append('\t') + # Формат по умолчанию — последняя запись палитры (см. выше). default_format_index = register_format({'width': default_width}) + # Формат колонтитула заводим здесь же: он идёт в палитре следом за умолчанием, и он + # обязан попасть в реестр ДО отсева неиспользуемых шрифтов — иначе шрифт колонтитула, + # на который больше никто не ссылается, будет выброшен. + header_fmt = header_format_index(defn.get('header')) + footer_fmt = header_format_index(defn.get('footer')) + # Шрифт, на который не ссылается ни один формат, платформа в палитру не кладёт: у макета # без оформления элемента нет вовсе. Мы же всегда заводили 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') diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index 4750371d..0462d66e 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 @@ -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. Колонтитулы и параметры печати --- +# Слот колонтитула устроен как ячейка: ссылка на формат плюс текст. Признак вывода и стартовую +# страницу читаем только у записи, где есть : палитра дедуплицирована, и колонтитул без +# своих настроек ссылается на чужую запись, где — ширина колонки, а не номер страницы. +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. Группировки строк и колонок --- # Платформа хранит их плоским списком диапазонов, родитель раньше детей. Число уровней # () не читаем: оно выводится из вложенности — совпало на всех 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 # Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index b99f9ebc..e4861e3b 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py @@ -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. Колонтитулы и параметры печати --- + # Слот колонтитула устроен как ячейка: ссылка на формат плюс текст. Признак вывода и стартовую + # страницу читаем только у записи, где есть : палитра дедуплицирована, и колонтитул без + # своих настроек ссылается на чужую запись, где — ширина колонки, а не номер страницы. + 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. Группировки строк и колонок --- # Платформа хранит их плоским списком диапазонов, родитель раньше детей. Число уровней # () не читаем: оно выводится из вложенности — совпало на всех 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 # Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится diff --git a/docs/1c-spreadsheet-spec.md b/docs/1c-spreadsheet-spec.md index 7512d469..94d86f28 100644 --- a/docs/1c-spreadsheet-spec.md +++ b/docs/1c-spreadsheet-spec.md @@ -601,7 +601,40 @@ current-config не объявляет, поэтому вынести объяв обратные, ячейки продолжают ссылаться на записи со старыми ширинами. Из итогового XML это значение не выводится — воспроизводить его не нужно. -## Группировки строк и колонок +## Колонтитулы и параметры печати + +```xml + + 2 + ruСлева + +… + + 100015 + +``` + +Шесть элементов идут после строк и перед ``: `leftHeader`, `centerHeader`, +`rightHeader`, `leftFooter`, `centerFooter`, `rightFooter`. Пишутся не комплектом: `leftHeader` +есть у 1 013 макетов, остальные — у 677–790. + +| Наблюдение | Данные корпуса | +|---|---| +| текст слота | `` — обычная строка, `` — форматированная (разметка внутри содержимого: `…`, ``, ``) | +| формат слота | `` = признак вывода (`1` / `-1`), `` = страница, с которой печатать, плюс шрифт и `verticalAlignment` | +| формат общий на колонтитул | у 674 макетов из 677; разный у трёх | +| формат делится с другими владельцами | 637 против 153 «только колонтитулы» — палитра дедуплицирована, поэтому `` в чужой записи означает ширину колонки, а не страницу | + +**Ловушка:** перенос строки внутри текста колонтитула платформа хранит голым 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 2 @@ -616,7 +649,7 @@ current-config не объявляет, поэтому вынести объяв ``` -Строки группируются часто (1 797 макетов, 170 967 группировок), колонки — почти никогда +Строки группируются часто (1 797 макетов, 170 967 групп), колонки — почти никогда (1 макет, 1 группировка). Порядок документных тегов: `height` → `vgLevels` → `vgRows` → `vg` → `hg`; `vgLevels` пишется только при наличии группировок. diff --git a/docs/mxl-dsl-spec.md b/docs/mxl-dsl-spec.md index 103e5449..c7812f12 100644 --- a/docs/mxl-dsl-spec.md +++ b/docs/mxl-dsl-spec.md @@ -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": "Итог красным" } +} +``` + +| Поле | По умолч. | Описание | +|------|-----------|----------| +| `left`, `center`, `right` | — | Текст слота: строка, объект «язык → текст» или `{ "formatted": … }` | +| `font` | — | Имя шрифта из `fonts` — на весь колонтитул | +| `verticalAlignment` | — | Положение текста по вертикали: `Top`, `Center`, `Bottom` | +| `show` | `true` | Выводить ли колонтитул | +| `startPage` | `1` | Страница, с которой колонтитул печатается | + +Текст может быть многострочным (`\n`) и содержать поля `[&НомерСтраницы]`, `[&СтраницВсего]`, +`[&Дата]`, `[&Время]` — платформа подставляет их при печати. `{ "formatted": … }` — форматированная +строка: разметка живёт прямо в тексте (`жирный`, ``, ``). + +## Параметры печати + +Плоский объект; имя ключа совпадает с именем свойства в выгрузке: + +```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 обычно шире фактической доработки. diff --git a/tests/skills/cases/mxl-compile/error-print-settings-key.json b/tests/skills/cases/mxl-compile/error-print-settings-key.json new file mode 100644 index 00000000..6789b61e --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-print-settings-key.json @@ -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\"" +} diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Колонтитулы.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Колонтитулы.xml new file mode 100644 index 00000000..704c7fd3 --- /dev/null +++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Колонтитулы.xml @@ -0,0 +1,119 @@ + + + + ru + ru + + ru + Русский + Русский + + + en + Английский + Английский + + + + 1 + + + 0 + + + + 0 + + + ru + Текст + + + + + + + + 2 + + + ru + Слева + + + + + 2 + + + ru + Стр. [&НомерСтраницы] из [&СтраницВсего]. Дата [&Дата][&Время] + + + + + 2 + + + ru + Справа + + + + + 3 + + + ru + Нижний слева + + + + + 3 + + + ru + <b>Форматируемая</> <fontsize 12><colorstyle -16>строка</></> + + + + + 3 + + + ru + Нижний справа + + + + true + 1 + 1 + 1 + + 1000 + 1000 + 1000 + 1000 + 1200 + 1000 + 15 + + + + 72 + + + 0 + -1 + 2 + Center + + + 0 + 1 + 21 + Top + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/headers-footers.json b/tests/skills/cases/mxl-compile/headers-footers.json new file mode 100644 index 00000000..c9c231f6 --- /dev/null +++ b/tests/skills/cases/mxl-compile/headers-footers.json @@ -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": "Итог красным" } + }, + "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" } +} diff --git a/tests/skills/cases/mxl-compile/platform-headers.json b/tests/skills/cases/mxl-compile/platform-headers.json new file mode 100644 index 00000000..bd4d1d90 --- /dev/null +++ b/tests/skills/cases/mxl-compile/platform-headers.json @@ -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" +} diff --git a/tests/skills/cases/mxl-compile/snapshots/headers-footers/Template.xml b/tests/skills/cases/mxl-compile/snapshots/headers-footers/Template.xml new file mode 100644 index 00000000..bf563589 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/headers-footers/Template.xml @@ -0,0 +1,104 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 2 + + + 0 + + + + 0 + + + ru + Отчёт + + + + + + + + 2 + + + ru + Слева +вторая строка + + + + + 2 + + + ru + Стр. [&НомерСтраницы] из [&СтраницВсего] + + + + + 2 + + + ru + Справа + + + en + Right + + + + + 3 + + + ru + <b>Итог</> <colorstyle -16>красным</> + + + + true + 1 + 1 + 1 + + Landscape + 500 + 500 + 500 + 500 + 1000 + 1000 + true + false + 7 + + + + 10 + + + 0 + 1 + 2 + Bottom + + + 0 + -1 + 1 + Top + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/roundtrip-headers.json b/tests/skills/cases/mxl-decompile/roundtrip-headers.json new file mode 100644 index 00000000..141a36cf --- /dev/null +++ b/tests/skills/cases/mxl-decompile/roundtrip-headers.json @@ -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": "Итог" } }, + "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:" } +} diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-headers/Template.xml b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-headers/Template.xml new file mode 100644 index 00000000..9281f89a --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-headers/Template.xml @@ -0,0 +1,83 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 2 + + + 0 + + + + 0 + + + ru + Отчёт + + + + + + + + 2 + + + ru + Слева +вторая строка + + + + + 2 + + + ru + Стр. [&НомерСтраницы] + + + + + 3 + + + ru + <b>Итог</> + + + + true + 1 + 1 + 1 + + Landscape + true + 7 + + + + 10 + + + 0 + 1 + 2 + Bottom + + + 0 + -1 + 1 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-headers/back.json b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-headers/back.json new file mode 100644 index 00000000..08c25598 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-headers/back.json @@ -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": "Итог" } }, + "printSettings": { "pageOrientation": "Landscape", "fitToPage": true, "firstPageNumber": 7 }, + "areas": [{ "rows": [["Отчёт"]] }] +} \ No newline at end of file