diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index d514bab0..930d4eab 100644 --- a/.claude/skills/mxl-compile/SKILL.md +++ b/.claude/skills/mxl-compile/SKILL.md @@ -62,6 +62,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J { col, span, rowspan, style, param, detail, text, template, valueType, controlType, value, note } ]}]}], namedAreas: [{ name, rows, cols }], + rowGroups: [{ rows, name, collapsed, titleLocation }], + columnGroups: [{ cols, name, collapsed, titleLocation }], columnSets: { name: { columns, columnWidths, columnStyles } } } ``` @@ -80,8 +82,9 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J - `col` — 1-based позиция колонки - `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки) - Содержимое ячейки задаётся одним из ключей: `param` — параметр заполнения, `text` — статический текст, `template` — текст со вставками `[Параметр]` +- `rowGroups`/`columnGroups` — группы строк и колонок (сворачиваются кнопкой на полях): плоский список диапазонов, вложенность выражена вхождением одного в другой - `note` — всплывающая подсказка у ячейки: строка, объект языков или `{ text, style, autoSize, box }` -- `valueType` делает ячейку полем ввода (`"Number(15,3,nonneg)"`, `"String(10)"`, `"CatalogRef.Валюты"`, составной через ` + `); текста в такой ячейке быть не может. `value` — значение в поле, `controlType: "checkbox"` — флажок вместо поля ввода +- `valueType` делает ячейку полем ввода (`"Number(15,3,nonneg)"`, `"String(10)"`, `"CatalogRef.Валюты"`, составной через ` + `); текста в такой ячейке быть не может, значение задаётся ключом `value` Двухуровневая шапка массивами: ```json diff --git a/.claude/skills/mxl-compile/reference/dsl-spec.md b/.claude/skills/mxl-compile/reference/dsl-spec.md index 1f695904..2368a0b5 100644 --- a/.claude/skills/mxl-compile/reference/dsl-spec.md +++ b/.claude/skills/mxl-compile/reference/dsl-spec.md @@ -87,6 +87,8 @@ | `areas` | да | — | Массив областей — диапазонов подряд идущих строк (порядок = порядок в документе); имя необязательно | | `namedAreas` | нет | — | Именованные области, заданные координатами (см. ниже) | | `columnSets` | нет | — | Дополнительные колоночные раскладки (см. `styles.md`) | +| `rowGroups` | нет | — | Группы строк (см. ниже) | +| `columnGroups` | нет | — | Группы колонок (см. ниже) | ## Области (`areas[]`) @@ -289,11 +291,37 @@ | `text` | — | Текст подсказки: строка или объект «язык → текст» | | `style` | стиль подсказки | Имя стиля из `styles`; без него — оформление, которое даёт Конфигуратор | | `autoSize` | `true` | Подгонять ли размер окошка под текст | -| `box` | канонический | Смещения окошка: `top`, `left` — положение, `bottom`, `right` — размер | +| `box` | — | Смещения окошка: `top`, `left` — положение, `bottom`, `right` — размер. Без ключа окошко встаёт стандартно | -Координаты ячейки в примечании не задаются — платформа привязывает конец окошка к самой ячейке, -и компилятор проставляет это сам. `autoSize` и `box` независимы: при автоподгоне размера -положение окошка всё равно хранится. +Координаты ячейки в примечании не задаются. `autoSize` и `box` независимы: при автоподгоне +размера положение окошка всё равно хранится. + +## Группы строк и колонок + +Диапазон строк или колонок, который сворачивается кнопкой на полях. Задаются плоским списком; +вложенность выражена вхождением одного диапазона в другой: + +```json +"rowGroups": [ + { "rows": "2-4" }, + { "rows": 3 }, + { "rows": 5, "name": "Итоговая", "collapsed": true, "titleLocation": "begin" } +], +"columnGroups": [ + { "cols": "2-3", "name": { "ru": "Показатели", "en": "Values" } }, + { "cols": 3 } +] +``` + +| Поле | Обяз. | По умолч. | Описание | +|------|:-----:|-----------|----------| +| `rows` / `cols` | да | — | Диапазон 1-based: число или `"N-M"` | +| `name` | нет | — | Имя группы: строка или объект «язык → текст» | +| `collapsed` | нет | `false` | Свёрнута ли группа при открытии | +| `titleLocation` | нет | `auto` | Расположение заголовка: `begin`, `end`, `auto` | + +Диапазоны либо вложены, либо не пересекаются: частичное пересечение — ошибка. Порядок в списке +не важен, число уровней вложенности считается само. ## `rowStyle` — оформление строки @@ -311,7 +339,6 @@ XML не возвращается. - объединения, не привязанные к ячейке (по всей высоте или ширине документа); - рисунки и картинки, в том числе штрихкоды; -- группировки строк и колонок; - колонтитулы, параметры печати, область печати. Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index b83da846..86a10285 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.43 — Compile 1C spreadsheet from JSON +# mxl-compile v1.44 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -2037,11 +2037,124 @@ if ($def.namedAreas) { } } +# 7e-bis. Группировки строк и колонок. Платформа хранит их плоским списком диапазонов; +# вложенность выражена вхождением одного диапазона в другой, а частичных пересечений не +# бывает вовсе (корпус: 40 620 886 пар непересекающихся, 599 958 вложенных, 0 частичных). +function Get-Groups { + param([string]$key, [string]$axis) + $out = @() + $idx = 0 + foreach ($g in $def.$key) { + $idx++ + $where = "$key[$idx]" + $spec = "$($g.$axis)".Trim() + if (-not $spec) { + [Console]::Error.WriteLine("${key}: '$axis' is required: $where") + exit 1 + } + if ($spec -match '^(\d+)\s*-\s*(\d+)$') { + $frm = [int]$Matches[1]; $to = [int]$Matches[2] + } elseif ($spec -match '^\d+$') { + $frm = [int]$spec; $to = $frm + } else { + [Console]::Error.WriteLine("${key}: invalid '$axis' value `"$spec`": $where") + exit 1 + } + if ($frm -lt 1) { $frm = 1 } + if ($to -lt $frm) { + [Console]::Error.WriteLine("${key}: '$axis' range is reversed `"$spec`": $where") + exit 1 + } + $header = '' + if ($null -ne $g.titleLocation -and "$($g.titleLocation)" -ne '') { + switch ("$($g.titleLocation)".Trim().ToLowerInvariant()) { + 'auto' { $header = '' } + 'begin' { $header = 'Begin' } + 'end' { $header = 'End' } + default { + [Console]::Error.WriteLine("${key}: unknown 'titleLocation' value `"$($g.titleLocation)`" ($where). Allowed: begin, end, auto") + exit 1 + } + } + } + $out += @{ + B = $frm - 1; E = $to - 1 + Name = $g.name + Collapsed = ($g.collapsed -eq $true -or "$($g.collapsed)" -eq 'true') + Header = $header + } + } + for ($i = 0; $i -lt $out.Count; $i++) { + for ($j = $i + 1; $j -lt $out.Count; $j++) { + $a = $out[$i]; $b = $out[$j] + if ($a.E -lt $b.B -or $b.E -lt $a.B) { continue } + if (($a.B -le $b.B -and $b.E -le $a.E) -or ($b.B -le $a.B -and $a.E -le $b.E)) { continue } + [Console]::Error.WriteLine("${key}: ranges must nest or be disjoint, got a partial overlap ($($a.B + 1)-$($a.E + 1) and $($b.B + 1)-$($b.E + 1))") + exit 1 + } + } + # Порядок платформы — родитель раньше детей, по возрастанию начала. + return @($out | Sort-Object @{ Expression = { $_.B } }, @{ Expression = { -$_.E } }) +} + +function Get-GroupLevels { + # Глубина вложенности. На корпусе она совпала с у всех 1797 макетов. + param($groups) + $depth = 0 + for ($i = 0; $i -lt $groups.Count; $i++) { + $g = $groups[$i]; $d = 1 + for ($j = 0; $j -lt $groups.Count; $j++) { + if ($j -eq $i) { continue } + $o = $groups[$j] + if ($o.B -le $g.B -and $g.E -le $o.E -and -not ($o.B -eq $g.B -and $o.E -eq $g.E)) { $d++ } + } + if ($d -gt $depth) { $depth = $d } + } + return $depth +} + +function Emit-Groups { + param([string]$tag, $groups) + foreach ($g in $groups) { + X "`t<$tag>" + X "`t`t$($g.B)" + if ($g.E -ne $g.B) { X "`t`t$($g.E)" } + if ($null -ne $g.Name) { + $pairs = @() + if ($g.Name -is [System.Collections.IDictionary]) { + foreach ($k in $g.Name.Keys) { $pairs += @{ Lang = "$k"; Text = "$($g.Name[$k])" } } + } elseif ($g.Name -is [System.Management.Automation.PSCustomObject]) { + foreach ($pr in $g.Name.PSObject.Properties) { $pairs += @{ Lang = $pr.Name; Text = "$($pr.Value)" } } + } else { + foreach ($l in $textLanguages) { $pairs += @{ Lang = $l; Text = "$($g.Name)" } } + } + X "`t`t" + foreach ($pr in $pairs) { + X "`t`t`t" + X "`t`t`t`t$($pr.Lang)" + X "`t`t`t`t$(Esc-XmlText $pr.Text)" + X "`t`t`t" + } + X "`t`t" + } + # Развёрнутая группировка тега не пишет: на корпусе только со значением false. + if ($g.Collapsed) { X "`t`tfalse" } + if ($g.Header) { X "`t`t$($g.Header)" } + X "`t" + } +} + +$rowGroups = Get-Groups 'rowGroups' 'rows' +$colGroups = Get-Groups 'columnGroups' 'cols' + # 7e. Scalar metadata X "`ttrue" X "`t$defaultFormatIndex" X "`t$totalRowCount" +if ($rowGroups.Count -gt 0) { X "`t$(Get-GroupLevels $rowGroups)" } X "`t$totalRowCount" +Emit-Groups 'vg' $rowGroups +Emit-Groups 'hg' $colGroups # 7f. Merges foreach ($m in $merges) { diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 45190143..78fe6b5b 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.43 — Compile 1C spreadsheet from JSON +# mxl-compile v1.44 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import hashlib @@ -1953,11 +1953,115 @@ def main(): 'EndCol': cols[1] - 1 if cols else -1, }) + # 7e-bis. Группировки строк и колонок. Платформа хранит их плоским списком диапазонов; + # вложенность выражена вхождением одного диапазона в другой, а частичных пересечений не + # бывает вовсе (корпус: 40 620 886 пар непересекающихся, 599 958 вложенных, 0 частичных). + def parse_groups(key, axis): + out = [] + for i, g in enumerate(defn.get(key) or [], start=1): + where = f'{key}[{i}]' + if not isinstance(g, dict): + print(f'{key}: element must be an object: {where}', file=sys.stderr) + sys.exit(1) + spec = g.get(axis) + if spec is None or str(spec).strip() == '': + print(f"{key}: '{axis}' is required: {where}", file=sys.stderr) + sys.exit(1) + spec = str(spec).strip() + m = re.match(r'^(\d+)\s*-\s*(\d+)$', spec) + if m: + frm, to = int(m.group(1)), int(m.group(2)) + elif re.match(r'^\d+$', spec): + frm = to = int(spec) + else: + print(f"{key}: invalid '{axis}' value \"{spec}\": {where}", file=sys.stderr) + sys.exit(1) + if frm < 1: + frm = 1 + if to < frm: + print(f"{key}: '{axis}' range is reversed \"{spec}\": {where}", file=sys.stderr) + sys.exit(1) + header = g.get('titleLocation') + header = str(header).strip().lower() if header is not None else '' + if header in ('', 'auto'): + header = '' + elif header == 'begin': + header = 'Begin' + elif header == 'end': + header = 'End' + else: + print(f"{key}: unknown 'titleLocation' value \"{g.get('titleLocation')}\"" + f' ({where}). Allowed: begin, end, auto', file=sys.stderr) + sys.exit(1) + out.append({ + 'B': frm - 1, 'E': to - 1, + 'Name': g.get('name'), + 'Collapsed': g.get('collapsed') is True or str(g.get('collapsed')).lower() == 'true', + 'Header': header, + }) + for i in range(len(out)): + for j in range(i + 1, len(out)): + a, b = out[i], out[j] + if a['E'] < b['B'] or b['E'] < a['B']: + continue + if (a['B'] <= b['B'] and b['E'] <= a['E']) or (b['B'] <= a['B'] and a['E'] <= b['E']): + continue + print(f'{key}: ranges must nest or be disjoint, got a partial overlap' + f' ({a["B"] + 1}-{a["E"] + 1} and {b["B"] + 1}-{b["E"] + 1})', file=sys.stderr) + sys.exit(1) + # Порядок платформы — родитель раньше детей, по возрастанию начала. + out.sort(key=lambda x: (x['B'], -x['E'])) + return out + + row_groups = parse_groups('rowGroups', 'rows') + col_groups = parse_groups('columnGroups', 'cols') + + def group_levels(groups): + """Глубина вложенности. На корпусе она совпала с у всех 1797 макетов.""" + depth = 0 + for i, g in enumerate(groups): + d = 1 + for j, o in enumerate(groups): + if j != i and o['B'] <= g['B'] and g['E'] <= o['E'] and (o['B'], o['E']) != (g['B'], g['E']): + d += 1 + depth = max(depth, d) + return depth + + def emit_groups(lines, tag, groups): + for g in groups: + lines.append(f'\t<{tag}>') + lines.append(f'\t\t{g["B"]}') + if g['E'] != g['B']: + lines.append(f'\t\t{g["E"]}') + if g['Name'] is not None: + value = g['Name'] + if isinstance(value, dict): + pairs = [(str(k), str(v)) for k, v in value.items()] + else: + pairs = [(lang, str(value)) for lang in text_languages] + lines.append('\t\t') + for lang, content in pairs: + lines.append('\t\t\t') + lines.append(f'\t\t\t\t{lang}') + lines.append(f'\t\t\t\t{esc_xml_text(content)}') + lines.append('\t\t\t') + lines.append('\t\t') + # Развёрнутая группировка тега не пишет: на корпусе только со значением false. + if g['Collapsed']: + lines.append('\t\tfalse') + if g['Header']: + lines.append(f'\t\t{g["Header"]}') + lines.append(f'\t') + # 7e. Scalar metadata lines.append(f'\ttrue') lines.append(f'\t{default_format_index}') lines.append(f'\t{total_row_count}') + if row_groups: + lines.append(f'\t{group_levels(row_groups)}') lines.append(f'\t{total_row_count}') + emit_groups(lines, 'vg', row_groups) + emit_groups(lines, 'hg', col_groups) # 7f. Merges for m in merges: diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index 9d5be4ba..4750371d 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.23 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.24 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -265,6 +265,41 @@ function Get-Format { return $rawFormats[$idx - 1] } +# --- 4b. Группировки строк и колонок --- +# Платформа хранит их плоским списком диапазонов, родитель раньше детей. Число уровней +# () не читаем: оно выводится из вложенности — совпало на всех 1797 макетах. +function Read-Groups { + param([string]$tag, [string]$axis) + $out = @() + foreach ($g in $root.SelectNodes("d:$tag", $ns)) { + $bNode = $g.SelectSingleNode("d:b", $ns) + $b = if ($bNode) { [int]$bNode.InnerText } else { 0 } + $eNode = $g.SelectSingleNode("d:e", $ns) + $e = if ($eNode) { [int]$eNode.InnerText } else { $b } + $item = [ordered]@{} + $item[$axis] = if ($e -eq $b) { $b + 1 } else { "$($b + 1)-$($e + 1)" } + $tEl = $g.SelectSingleNode("d:t", $ns) + if ($tEl) { + $byLang = [ordered]@{} + foreach ($it in $tEl.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 { '' }) + } + if ($byLang.Count -gt 0) { $item["name"] = $byLang } + } + $oNode = $g.SelectSingleNode("d:o", $ns) + if ($oNode -and $oNode.InnerText.Trim() -ceq 'false') { $item["collapsed"] = $true } + $gNode = $g.SelectSingleNode("d:g", $ns) + if ($gNode -and $gNode.InnerText) { $item["titleLocation"] = $gNode.InnerText.Trim().ToLowerInvariant() } + $out += $item + } + return @($out) +} + +$rowGroups = Read-Groups 'vg' 'rows' +$colGroups = Read-Groups 'hg' 'cols' + # --- 5. Extract columns and default width --- # Колоночная раскладка («индивидуальная ширина колонок» для группы строк) — элемент . @@ -1435,6 +1470,13 @@ if ($extraSets.Count -gt 0) { $result["columnSets"] = $setsOut } +# Имя группировки — та же мультиязычная строка, что текст ячейки, и сворачивается так же. +foreach ($g in ($rowGroups + $colGroups)) { + if ($g.Contains("name")) { $g["name"] = Get-DslText $g["name"] } +} +if ($rowGroups.Count -gt 0) { $result["rowGroups"] = [array]$rowGroups } +if ($colGroups.Count -gt 0) { $result["columnGroups"] = [array]$colGroups } + $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 e617b061..b99f9ebc 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.23 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.24 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -431,6 +431,36 @@ def main(): return None return raw_formats[idx - 1] + # --- 4b. Группировки строк и колонок --- + # Платформа хранит их плоским списком диапазонов, родитель раньше детей. Число уровней + # () не читаем: оно выводится из вложенности — совпало на всех 1797 макетах. + def read_groups(tag, axis): + out = [] + for g in findall(root, "d:" + tag): + b = int_of(find(g, "d:b")) + e_node = find(g, "d:e") + e = int_of(e_node, b) if e_node is not None else b + item = OrderedDict() + item[axis] = (b + 1) if e == b else f'{b + 1}-{e + 1}' + t_el = find(g, "d:t") + if t_el is not None: + by_lang = OrderedDict() + for it in findall(t_el, "v8:item"): + lang = text_of(find(it, "v8:lang")) or '' + by_lang[lang] = text_of(find(it, "v8:content")) or '' + if by_lang: + item["name"] = by_lang + if (text_of(find(g, "d:o")) or 'true').strip() == 'false': + item["collapsed"] = True + place = text_of(find(g, "d:g")) + if place: + item["titleLocation"] = place.strip().lower() + out.append(item) + return out + + row_groups = read_groups("vg", "rows") + col_groups = read_groups("hg", "cols") + # --- 5. Extract columns and default width --- # Колоночная раскладка («индивидуальная ширина колонок» для группы строк) — элемент . @@ -1433,6 +1463,15 @@ def main(): sets_out[cs["Id"]] = entry result["columnSets"] = sets_out + # Имя группировки — та же мультиязычная строка, что текст ячейки, и сворачивается так же. + for _g in row_groups + col_groups: + if "name" in _g: + _g["name"] = get_dsl_text(_g["name"]) + if row_groups: + result["rowGroups"] = row_groups + if col_groups: + result["columnGroups"] = col_groups + result["areas"] = dsl_areas # Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 index fae4a1f5..e7e18579 100644 --- a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 +++ b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 @@ -1,4 +1,4 @@ -# mxl-validate v1.5 — Validate 1C spreadsheet +# mxl-validate v1.6 — Validate 1C spreadsheet # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Alias('Path')] @@ -507,6 +507,38 @@ if ($valueFormatIdx.Count -gt 0) { Report-OK "Value cells: $($valueFormatIdx.Count) value formats" } +# --- Check 14: группировки строк и колонок --- +# Диапазоны группировок либо вложены, либо не пересекаются: на корпусе 40 620 886 пар +# непересекающихся и 599 958 вложенных, частичных пересечений нет ни одного. + +foreach ($tag in @('vg', 'hg')) { + $ranges = @() + foreach ($g in $root.SelectNodes("d:$tag", $nsMgr)) { + $bNode = $g.SelectSingleNode("d:b", $nsMgr) + if (-not $bNode) { + Report-Error "Group <$tag>: is missing" + continue + } + $b = [int]$bNode.InnerText + $eNode = $g.SelectSingleNode("d:e", $nsMgr) + $e = if ($eNode) { [int]$eNode.InnerText } else { $b } + if ($e -lt $b) { Report-Error "Group <$tag> $b..${e}: range is reversed" } + if ($tag -ceq 'vg' -and $docHeight -gt 0 -and $b -ge $docHeight) { + Report-Warn "Group starts at row $b, beyond document height ($docHeight)" + } + $ranges += ,@($b, $e) + } + for ($i = 0; $i -lt $ranges.Count; $i++) { + for ($j = $i + 1; $j -lt $ranges.Count; $j++) { + $a = $ranges[$i]; $c = $ranges[$j] + if ($a[1] -lt $c[0] -or $c[1] -lt $a[0]) { continue } + if (($a[0] -le $c[0] -and $c[1] -le $a[1]) -or ($c[0] -le $a[0] -and $a[1] -le $c[1])) { continue } + Report-Error "Groups <$tag> $($a[0])..$($a[1]) and $($c[0])..$($c[1]) overlap partially" + } + } + if ($ranges.Count -gt 0) { Report-OK "Groups <$tag>: $($ranges.Count)" } +} + # --- Summary --- $checks = $script:okCount + $errors + $warnings diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.py b/.claude/skills/mxl-validate/scripts/mxl-validate.py index a939b179..ac0c3342 100644 --- a/.claude/skills/mxl-validate/scripts/mxl-validate.py +++ b/.claude/skills/mxl-validate/scripts/mxl-validate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-validate v1.5 — Validate 1C spreadsheet document Template.xml +# mxl-validate v1.6 — Validate 1C spreadsheet document Template.xml # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Validates spreadsheet Template.xml: height, palette refs, column/row indices, areas, merges.""" import sys, os, argparse @@ -494,6 +494,35 @@ def main(): if value_format_idx: r.ok(f'Value cells: {len(value_format_idx)} value formats') + # --- Check 14: группировки строк и колонок --- + # Диапазоны группировок либо вложены, либо не пересекаются: на корпусе 40 620 886 пар + # непересекающихся и 599 958 вложенных, частичных пересечений нет ни одного. + for tag in ('vg', 'hg'): + ranges = [] + for g in root.findall(f'{{{NS_D}}}{tag}'): + b_node = g.find(f'{{{NS_D}}}b') + if b_node is None or not b_node.text: + r.error(f'Group <{tag}>: is missing') + continue + b = int(b_node.text) + e_node = g.find(f'{{{NS_D}}}e') + e = int(e_node.text) if e_node is not None and e_node.text else b + if e < b: + r.error(f'Group <{tag}> {b}..{e}: range is reversed') + if tag == 'vg' and doc_height > 0 and b >= doc_height: + r.warn(f'Group starts at row {b}, beyond document height ({doc_height})') + ranges.append((b, e)) + for i in range(len(ranges)): + for j in range(i + 1, len(ranges)): + a, c = ranges[i], ranges[j] + if a[1] < c[0] or c[1] < a[0]: + continue + if (a[0] <= c[0] and c[1] <= a[1]) or (c[0] <= a[0] and a[1] <= c[1]): + continue + r.error(f'Groups <{tag}> {a[0]}..{a[1]} and {c[0]}..{c[1]} overlap partially') + if ranges: + r.ok(f'Groups <{tag}>: {len(ranges)}') + # --- Finalize --- checks = r.ok_count + r.errors + r.warnings if r.errors == 0 and r.warnings == 0 and not detailed: diff --git a/docs/1c-spreadsheet-spec.md b/docs/1c-spreadsheet-spec.md index be18d1aa..7512d469 100644 --- a/docs/1c-spreadsheet-spec.md +++ b/docs/1c-spreadsheet-spec.md @@ -601,6 +601,38 @@ current-config не объявляет, поэтому вынести объяв обратные, ячейки продолжают ссылаться на записи со старыми ширинами. Из итогового XML это значение не выводится — воспроизводить его не нужно. +## Группировки строк и колонок + +```xml +2 +8 + + 1 + 2 + ruИтоговая + false + Begin + + +``` + +Строки группируются часто (1 797 макетов, 170 967 группировок), колонки — почти никогда +(1 макет, 1 группировка). Порядок документных тегов: `height` → `vgLevels` → `vgRows` → `vg` → +`hg`; `vgLevels` пишется только при наличии группировок. + +| Наблюдение | Данные корпуса | +|---|---| +| диапазоны либо вложены, либо не пересекаются | 40 620 886 пар непересекающихся, 599 958 вложенных, **0 частичных** | +| порядок записи — родитель раньше вложенных, по возрастанию начала | совпало у 1 798 макетов из 1 798 | +| `` = глубина вложенности | совпало у 1 797 из 1 797 | +| `` | только `false` (6 130) — развёрнутая группировка тег не пишет | +| `` | в корпусе только `Begin` (6); значение `End` даёт Конфигуратор при «Расположение заголовка: Конец» | + +Имя (``) — мультиязычная строка, и в 143 455 случаях это автоимя `R<номер строки + 1>`. +Вывести его нельзя: 16 666 имён «протухли» (`R91` у строки 103 — строки вставляли после +создания группировки), а 10 287 группировок имени не имеют вовсе. Языковые наборы — +`ru`+`en` (93 250) либо псевдоязык `#` (67 431), никогда вместе. + ## Примечание к ячейке ```xml diff --git a/docs/mxl-dsl-spec.md b/docs/mxl-dsl-spec.md index 69d4803b..103e5449 100644 --- a/docs/mxl-dsl-spec.md +++ b/docs/mxl-dsl-spec.md @@ -87,6 +87,8 @@ | `areas` | да | — | Массив областей — диапазонов подряд идущих строк (порядок = порядок в документе); имя необязательно | | `namedAreas` | нет | — | Именованные области, заданные координатами (см. ниже) | | `columnSets` | нет | — | Дополнительные колоночные раскладки (см. `mxl-dsl-styles.md`) | +| `rowGroups` | нет | — | Группы строк (см. ниже) | +| `columnGroups` | нет | — | Группы колонок (см. ниже) | ## Области (`areas[]`) @@ -300,6 +302,35 @@ и компилятор проставляет это сам. `autoSize` и `box` независимы: при автоподгоне размера положение окошка всё равно хранится. +## Группы строк и колонок + +Диапазон строк или колонок, который сворачивается кнопкой на полях. Задаются плоским списком; +вложенность выражена вхождением одного диапазона в другой: + +```json +"rowGroups": [ + { "rows": "2-4" }, + { "rows": 3 }, + { "rows": 5, "name": "Итоговая", "collapsed": true, "titleLocation": "begin" } +], +"columnGroups": [ + { "cols": "2-3", "name": { "ru": "Показатели", "en": "Values" } }, + { "cols": 3 } +] +``` + +| Поле | Обяз. | По умолч. | Описание | +|------|:-----:|-----------|----------| +| `rows` / `cols` | да | — | Диапазон 1-based: число или `"N-M"` | +| `name` | нет | — | Имя группы: строка или объект «язык → текст» | +| `collapsed` | нет | `false` | Свёрнута ли группа при открытии | +| `titleLocation` | нет | `auto` | Расположение заголовка: `begin`, `end`, `auto` | + +Диапазоны либо вложены, либо не пересекаются — частичное пересечение платформа не хранит, и +компилятор отвергает его с ненулевым кодом выхода. Число уровней вложенности считается само. +Порядок в списке не важен: компилятор пишет группы так же, как платформа, — родитель +раньше вложенных, по возрастанию начала. + ## `rowStyle` — оформление строки Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком. @@ -316,7 +347,6 @@ XML не возвращается. - объединения, не привязанные к ячейке (по всей высоте или ширине документа); - рисунки и картинки, в том числе штрихкоды; -- группировки строк и колонок; - колонтитулы, параметры печати, область печати. Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому diff --git a/tests/skills/cases/mxl-compile/error-groups-overlap.json b/tests/skills/cases/mxl-compile/error-groups-overlap.json new file mode 100644 index 00000000..fc9cbbea --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-groups-overlap.json @@ -0,0 +1,10 @@ +{ + "name": "Ошибка: группировки пересекаются частично", + "input": { + "columns": 2, + "areas": [{ "rows": [["a"], ["b"], ["c"], ["d"]] }], + "rowGroups": [{ "rows": "1-3" }, { "rows": "2-4" }] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "rowGroups: ranges must nest or be disjoint, got a partial overlap (1-3 and 2-4)" +} diff --git a/tests/skills/cases/mxl-compile/error-groups-title.json b/tests/skills/cases/mxl-compile/error-groups-title.json new file mode 100644 index 00000000..d18ba910 --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-groups-title.json @@ -0,0 +1,10 @@ +{ + "name": "Ошибка: неизвестное расположение заголовка группировки", + "input": { + "columns": 2, + "areas": [{ "rows": [["a"], ["b"]] }], + "rowGroups": [{ "rows": 2, "titleLocation": "middle" }] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "rowGroups: unknown 'titleLocation' value \"middle\" (rowGroups[1]). Allowed: begin, end, auto" +} 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..f74adb28 --- /dev/null +++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Группировки.xml @@ -0,0 +1,236 @@ + + + + ru + ru + + ru + Русский + Русский + + + en + Английский + Английский + + + + 3 + + + 0 + + + + 0 + + + ru + 11 + + + + + + + 0 + + + ru + 12 + + + + + + + 0 + + + ru + 13 + + + + + + + + 1 + + + + 0 + + + ru + 21 + + + + + + + 0 + + + ru + 22 + + + + + + + 0 + + + ru + 23 + + + + + + + + 2 + + + + 0 + + + ru + 31 + + + + + + + 0 + + + ru + 23 + + + + + + + 0 + + + ru + 33 + + + + + + + + 3 + + + + 0 + + + ru + 41 + + + + + + + + 4 + + + + 0 + + + ru + 51 + + + + + + + + 5 + 7 + + true + + + true + 1 + 8 + 2 + 8 + + 1 + 2 + + + 2 + + + 3 + + + 4 + + + 5 + + + ru + Произвеольное имя группы + + + + + 6 + + + ru + ПИмяНачало + + + Begin + + + 7 + + + ru + ПИмяКонец + + + End + + + 1 + 2 + + + 2 + + + ru + Горизонтальная + + + + + 72 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/platform-groups.json b/tests/skills/cases/mxl-compile/platform-groups.json new file mode 100644 index 00000000..8b7f749b --- /dev/null +++ b/tests/skills/cases/mxl-compile/platform-groups.json @@ -0,0 +1,15 @@ +{ + "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/row-column-groups.json b/tests/skills/cases/mxl-compile/row-column-groups.json new file mode 100644 index 00000000..b4d94452 --- /dev/null +++ b/tests/skills/cases/mxl-compile/row-column-groups.json @@ -0,0 +1,25 @@ +{ + "name": "Группировки строк и колонок", + "input": { + "columns": 3, + "areas": [ + { "rows": [ + ["11", "12", "13"], + ["21", "22", "23"], + ["31", "32", "33"], + ["41"], + ["51"] + ]} + ], + "rowGroups": [ + { "rows": "2-4" }, + { "rows": 3 }, + { "rows": 5, "name": "Итоговая", "collapsed": true, "titleLocation": "begin" } + ], + "columnGroups": [ + { "cols": "2-3", "name": { "ru": "Показатели", "en": "Values" } }, + { "cols": 3, "titleLocation": "end" } + ] + }, + "params": { "outputPath": "Template.xml" } +} diff --git a/tests/skills/cases/mxl-compile/snapshots/row-column-groups/Template.xml b/tests/skills/cases/mxl-compile/snapshots/row-column-groups/Template.xml new file mode 100644 index 00000000..31a76a48 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/row-column-groups/Template.xml @@ -0,0 +1,205 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 3 + + + 0 + + + + 0 + + + ru + 11 + + + + + + + 0 + + + ru + 12 + + + + + + + 0 + + + ru + 13 + + + + + + + + 1 + + + + 0 + + + ru + 21 + + + + + + + 0 + + + ru + 22 + + + + + + + 0 + + + ru + 23 + + + + + + + + 2 + + + + 0 + + + ru + 31 + + + + + + + 0 + + + ru + 32 + + + + + + + 0 + + + ru + 33 + + + + + + + + 3 + + + + 0 + + + ru + 41 + + + + + + + + 4 + + + + 0 + + + ru + 51 + + + + + + + true + 1 + 5 + 2 + 5 + + 1 + 3 + + + 2 + + + 4 + + + ru + Итоговая + + + false + Begin + + + 1 + 2 + + + ru + Показатели + + + en + Values + + + + + 2 + End + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/roundtrip-groups.json b/tests/skills/cases/mxl-decompile/roundtrip-groups.json new file mode 100644 index 00000000..bb8448a4 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/roundtrip-groups.json @@ -0,0 +1,28 @@ +{ + "name": "Roundtrip — группировки строк и колонок", + "preRun": [ + { + "script": "mxl-compile/scripts/mxl-compile", + "input": { + "columns": 3, + "areas": [ + { "rows": [["11", "12", "13"], ["21", "22", "23"], ["31"], ["41"], ["51"]] } + ], + "rowGroups": [ + { "rows": "2-4" }, + { "rows": 3 }, + { "rows": 5, "name": "Итоговая", "collapsed": true, "titleLocation": "begin" } + ], + "columnGroups": [ + { "cols": "2-3", "name": { "#": "R2" } }, + { "cols": 3, "titleLocation": "end" } + ] + }, + "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-groups/Template.xml b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-groups/Template.xml new file mode 100644 index 00000000..545cc42e --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-groups/Template.xml @@ -0,0 +1,179 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 3 + + + 0 + + + + 0 + + + ru + 11 + + + + + + + 0 + + + ru + 12 + + + + + + + 0 + + + ru + 13 + + + + + + + + 1 + + + + 0 + + + ru + 21 + + + + + + + 0 + + + ru + 22 + + + + + + + 0 + + + ru + 23 + + + + + + + + 2 + + + + 0 + + + ru + 31 + + + + + + + + 3 + + + + 0 + + + ru + 41 + + + + + + + + 4 + + + + 0 + + + ru + 51 + + + + + + + true + 1 + 5 + 2 + 5 + + 1 + 3 + + + 2 + + + 4 + + + ru + Итоговая + + + false + Begin + + + 1 + 2 + + + # + R2 + + + + + 2 + End + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-groups/back.json b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-groups/back.json new file mode 100644 index 00000000..db2f50cc --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-groups/back.json @@ -0,0 +1 @@ +{ "columns": 3, "defaultWidth": 10, "fonts": {}, "styles": {}, "rowGroups": [{ "rows": "2-4" }, { "rows": 3 }, { "rows": 5, "name": "Итоговая", "collapsed": true, "titleLocation": "begin" }], "columnGroups": [{ "cols": "2-3", "name": { "#": "R2" } }, { "cols": 3, "titleLocation": "end" }], "areas": [{ "rows": [["11", "12", "13"], ["21", "22", "23"], ["31"], ["41"], ["51"]] }] } \ No newline at end of file