diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md index 91f7ca49..c4de63dd 100644 --- a/.claude/skills/skd-compile/SKILL.md +++ b/.claude/skills/skd-compile/SKILL.md @@ -257,7 +257,16 @@ Folder в selection: `{"folder": "Поступление", "items": ["ПолеА ] ``` -Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `null` — пустая. +Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `">"` — объединение с ячейкой слева, `null` — пустая. + +Двухуровневая шапка с горизонтальным объединением: +```json +"rows": [ + ["Вид актива", "Остаток начало", "Поступление", ">", ">", ">", "Выбытие", ">", ">", "Остаток конец"], + ["|", "|", "из произв.", "из п/ф", "со сч.40", "прочее", "Реализ.", "отгруж.", "прочее", "|"], + ["К1", "К2", "К3", "К4", "К5", "К6", "К7", "К8", "К9", "К10"] +] +``` Встроенные стили: `header` (фон, центр, перенос), `data` (фон группы), `subheader` (без фона, центр), `total` (без фона). Все — Arial 10, рамки Solid 1px, цвета через стили платформы. diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1 index bd1eb8b1..c55e598b 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.ps1 +++ b/.claude/skills/skd-compile/scripts/skd-compile.ps1 @@ -1,4 +1,4 @@ -# skd-compile v1.6 — Compile 1C DCS from JSON +# skd-compile v1.7 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [string]$DefinitionFile, @@ -1068,7 +1068,7 @@ function Emit-ColorValue { } function Emit-CellAppearance { - param($style, [double]$width = 0, [bool]$vMerge = $false, [double]$minHeight = 0, $extraItems = @()) + param($style, [double]$width = 0, [bool]$vMerge = $false, [bool]$hMerge = $false, [double]$minHeight = 0, $extraItems = @()) $ind = "`t`t`t`t`t" X "`t`t`t`t" # Background color @@ -1161,6 +1161,13 @@ function Emit-CellAppearance { X "$ind`ttrue" X "$ind" } + # Horizontal merge + if ($hMerge) { + X "$ind" + X "$ind`tОбъединятьПоГоризонтали" + X "$ind`ttrue" + X "$ind" + } # Extra appearance items (e.g. drilldown Расшифровка) foreach ($ei in $extraItems) { X $ei } X "`t`t`t`t" @@ -1180,7 +1187,7 @@ function Emit-AreaTemplateDSL { $minHeight = if ($t.minHeight) { [double]$t.minHeight } else { 0 } $colCount = if ($widths.Count -gt 0) { $widths.Count } else { $rows[0].Count } - # Build merge map: vMerge[row][col] = $true if cell is merged with above + # Build vertical merge map: vMerge[row][col] = $true if cell is merged with above $vMerge = @{} for ($r = $rows.Count - 1; $r -ge 1; $r--) { $vMerge[$r] = @{} @@ -1193,6 +1200,18 @@ function Emit-AreaTemplateDSL { } if (-not $vMerge.ContainsKey(0)) { $vMerge[0] = @{} } + # Build horizontal merge map: hMerge[row][col] = $true if cell is merged with left + $hMerge = @{} + for ($r = 0; $r -lt $rows.Count; $r++) { + $hMerge[$r] = @{} + for ($c = 0; $c -lt $colCount; $c++) { + $cellVal = $rows[$r][$c] + if ($cellVal -is [string] -and $cellVal -eq '>') { + $hMerge[$r][$c] = $true + } + } + } + # Build drilldown map: param_name -> drilldown_value $drilldownMap = @{} if ($t.parameters) { @@ -1210,7 +1229,8 @@ function Emit-AreaTemplateDSL { for ($c = 0; $c -lt $colCount; $c++) { $cellVal = $rows[$r][$c] $w = if ($c -lt $widths.Count) { [double]$widths[$c] } else { 0 } - $isMerged = $vMerge[$r][$c] -eq $true + $isVMerged = $vMerge[$r][$c] -eq $true + $isHMerged = $hMerge[$r][$c] -eq $true # Check if this cell starts a vertical merge (next row has "|" in same column) $startsVMerge = $false for ($nr = $r + 1; $nr -lt $rows.Count; $nr++) { @@ -1218,13 +1238,19 @@ function Emit-AreaTemplateDSL { } X "`t`t`t`t" - if ($isMerged) { - # Merged cell — only appearance with vMerge flag + width + if ($isVMerged) { + # Vertically merged cell — only appearance with vMerge flag + width Emit-CellAppearance $style $w $true + } elseif ($isHMerged) { + # Horizontally merged cell — only appearance with hMerge flag + width + Emit-CellAppearance $style $w $false $true } else { # Cell value if ($null -ne $cellVal -and $cellVal -ne '') { $cellStr = "$cellVal" + # Unescape \| and \> + if ($cellStr -eq '\|') { $cellStr = '|' } + elseif ($cellStr -eq '\>') { $cellStr = '>' } if ($cellStr -match '^\{(.+)\}$') { # Parameter reference $paramName = $Matches[1] @@ -1255,7 +1281,7 @@ function Emit-AreaTemplateDSL { # Appearance $h = if ($r -eq 0) { $minHeight } else { 0 } if (-not $cellExtraItems) { $cellExtraItems = @() } - Emit-CellAppearance $style $w $startsVMerge $h $cellExtraItems + Emit-CellAppearance $style $w $startsVMerge $false $h $cellExtraItems $cellExtraItems = @() } X "`t`t`t`t" diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py index 7ed702e6..c8b6be4a 100644 --- a/.claude/skills/skd-compile/scripts/skd-compile.py +++ b/.claude/skills/skd-compile/scripts/skd-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# skd-compile v1.6 — Compile 1C DCS from JSON +# skd-compile v1.7 — Compile 1C DCS from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import json @@ -12,6 +12,10 @@ import uuid def esc_xml(s): return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"') +def fmt_dec(v): + """Format decimal: 30.0 → '30', 16.625 → '16.625' (match PS1 output).""" + return str(int(v)) if v == int(v) else str(v) + def resolve_query_value(val, base_dir): if not val.startswith("@"): @@ -892,7 +896,7 @@ def _emit_color_value(lines, color, indent): lines.append(f'{indent}{esc_xml(color)}') -def _emit_cell_appearance(lines, style, width=0, v_merge=False, min_height=0, extra_items=None): +def _emit_cell_appearance(lines, style, width=0, v_merge=False, h_merge=False, min_height=0, extra_items=None): ind = '\t\t\t\t\t' lines.append('\t\t\t\t') # Background color @@ -956,11 +960,11 @@ def _emit_cell_appearance(lines, style, width=0, v_merge=False, min_height=0, ex if width and width > 0: lines.append(f'{ind}') lines.append(f'{ind}\t\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430') - lines.append(f'{ind}\t{width}') + lines.append(f'{ind}\t{fmt_dec(width)}') lines.append(f'{ind}') lines.append(f'{ind}') lines.append(f'{ind}\t\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0428\u0438\u0440\u0438\u043d\u0430') - lines.append(f'{ind}\t{width}') + lines.append(f'{ind}\t{fmt_dec(width)}') lines.append(f'{ind}') # Min height if min_height and min_height > 0: @@ -974,6 +978,12 @@ def _emit_cell_appearance(lines, style, width=0, v_merge=False, min_height=0, ex lines.append(f'{ind}\t\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u0438') lines.append(f'{ind}\ttrue') lines.append(f'{ind}') + # Horizontal merge + if h_merge: + lines.append(f'{ind}') + lines.append(f'{ind}\t\u041e\u0431\u044a\u0435\u0434\u0438\u043d\u044f\u0442\u044c\u041f\u043e\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u0438') + lines.append(f'{ind}\ttrue') + lines.append(f'{ind}') # Extra appearance items (e.g. drilldown) if extra_items: for ei in extra_items: @@ -993,7 +1003,7 @@ def _emit_area_template_dsl(lines, t): min_height = float(t.get('minHeight', 0)) col_count = len(widths) if widths else len(rows[0]) - # Build merge map + # Build vertical merge map v_merge = {} for r in range(len(rows) - 1, 0, -1): v_merge[r] = {} @@ -1004,6 +1014,15 @@ def _emit_area_template_dsl(lines, t): if 0 not in v_merge: v_merge[0] = {} + # Build horizontal merge map + h_merge = {} + for r in range(len(rows)): + h_merge[r] = {} + for c in range(col_count): + cell_val = rows[r][c] if c < len(rows[r]) else None + if isinstance(cell_val, str) and cell_val == '>': + h_merge[r][c] = True + # Build drilldown map: param_name -> drilldown_value drilldown_map = {} if t.get('parameters'): @@ -1020,7 +1039,8 @@ def _emit_area_template_dsl(lines, t): for c in range(col_count): cell_val = rows[r][c] if c < len(rows[r]) else None w = float(widths[c]) if c < len(widths) else 0 - is_merged = v_merge.get(r, {}).get(c, False) + is_v_merged = v_merge.get(r, {}).get(c, False) + is_h_merged = h_merge.get(r, {}).get(c, False) # Check if this cell starts a vertical merge starts_v_merge = False for nr in range(r + 1, len(rows)): @@ -1030,12 +1050,19 @@ def _emit_area_template_dsl(lines, t): break lines.append('\t\t\t\t') - if is_merged: + if is_v_merged: _emit_cell_appearance(lines, style, w, True) + elif is_h_merged: + _emit_cell_appearance(lines, style, w, h_merge=True) else: cell_extra_items = [] if cell_val is not None and str(cell_val) != '': cell_str = str(cell_val) + # Unescape \| and \> + if cell_str == '\\|': + cell_str = '|' + elif cell_str == '\\>': + cell_str = '>' m = re.match(r'^\{(.+)\}$', cell_str) if m: param_name = m.group(1) @@ -1059,7 +1086,7 @@ def _emit_area_template_dsl(lines, t): lines.append('\t\t\t\t\t\t') lines.append('\t\t\t\t\t') h = min_height if r == 0 else 0 - _emit_cell_appearance(lines, style, w, starts_v_merge, h, cell_extra_items or None) + _emit_cell_appearance(lines, style, w, starts_v_merge, False, h, cell_extra_items or None) lines.append('\t\t\t\t') lines.append('\t\t\t') diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md index cf334480..f2b2aa37 100644 --- a/docs/skd-dsl-spec.md +++ b/docs/skd-dsl-spec.md @@ -831,7 +831,8 @@ XML-маппинг — по `` на каждый элемент: |----------|----------| | `"текст"` | Статический текст (`v8:LocalStringType`) | | `"{Имя}"` | Параметр шаблона (`dcscor:Parameter`), задаётся через `parameters` | -| `"\|"` | Вертикальное объединение с ячейкой выше | +| `"\|"` | Вертикальное объединение с ячейкой выше (`ОбъединятьПоВертикали`) | +| `">"` | Горизонтальное объединение с ячейкой слева (`ОбъединятьПоГоризонтали`) | | `null` | Пустая ячейка (без содержимого) | #### Встроенные пресеты стилей diff --git a/tests/skills/cases/skd-compile/horizontal-merge.json b/tests/skills/cases/skd-compile/horizontal-merge.json new file mode 100644 index 00000000..2fdcadf6 --- /dev/null +++ b/tests/skills/cases/skd-compile/horizontal-merge.json @@ -0,0 +1,47 @@ +{ + "name": "Горизонтальное объединение ячеек (>) в шаблонах", + "params": { "outputPath": "Template.xml" }, + "input": { + "dataSets": [{ + "name": "Основной", + "query": "ВЫБРАТЬ Т.Счет, Т.Остаток, Т.Пост1, Т.Пост2, Т.Пост3, Т.Выб1, Т.Выб2, Т.Итого ИЗ Регистр КАК Т", + "fields": ["Счет: string", "Остаток: decimal(15,2)", "Пост1: decimal(15,2)", "Пост2: decimal(15,2)", "Пост3: decimal(15,2)", "Выб1: decimal(15,2)", "Выб2: decimal(15,2)", "Итого: decimal(15,2)"] + }], + "templates": [ + { + "name": "Макет1", + "style": "header", + "widths": [30, 16, 16, 16, 16, 16, 16, 16], + "minHeight": 24.75, + "rows": [ + ["Счет", "Остаток", "Поступление", ">", ">", "Выбытие", ">", "Итого"], + ["|", "|", "из произв.", "из п/ф", "прочее", "Реализ.", "прочее", "|"], + ["К1", "К2", "К3", "К4", "К5", "К6", "К7", "К8"] + ] + }, + { + "name": "Макет2", + "style": "data", + "widths": [30, 16, 16, 16, 16, 16, 16, 16], + "rows": [["{Счет}", "{Остаток}", "{Пост1}", "{Пост2}", "{Пост3}", "{Выб1}", "{Выб2}", "{Итого}"]] + } + ], + "settingsVariants": [{ + "name": "Основной", + "settings": { + "selection": ["Auto"], + "structure": "details" + } + }] + }, + "validatePath": "Template.xml", + "expect": { + "files": ["Template.xml"], + "contains": [ + "ОбъединятьПоГоризонтали", + "ОбъединятьПоВертикали", + "Поступление", + "Выбытие" + ] + } +} diff --git a/tests/skills/cases/skd-compile/snapshots/horizontal-merge/Template.xml b/tests/skills/cases/skd-compile/snapshots/horizontal-merge/Template.xml new file mode 100644 index 00000000..a232d903 --- /dev/null +++ b/tests/skills/cases/skd-compile/snapshots/horizontal-merge/Template.xml @@ -0,0 +1,2297 @@ + + + + ИсточникДанных1 + Local + + + Основной + + Счет + Счет + + xs:string + + 0 + Variable + + + + + Остаток + Остаток + + xs:decimal + + 15 + 2 + Any + + + + + Пост1 + Пост1 + + xs:decimal + + 15 + 2 + Any + + + + + Пост2 + Пост2 + + xs:decimal + + 15 + 2 + Any + + + + + Пост3 + Пост3 + + xs:decimal + + 15 + 2 + Any + + + + + Выб1 + Выб1 + + xs:decimal + + 15 + 2 + Any + + + + + Выб2 + Выб2 + + xs:decimal + + 15 + 2 + Any + + + + + Итого + Итого + + xs:decimal + + 15 + 2 + Any + + + + ИсточникДанных1 + ВЫБРАТЬ Т.Счет, Т.Остаток, Т.Пост1, Т.Пост2, Т.Пост3, Т.Выб1, Т.Выб2, Т.Итого ИЗ Регистр КАК Т + + + + + Основной + + + ru + Основной + + + + + + + + + + + + + + + +