diff --git a/.claude/skills/skd-compile/SKILL.md b/.claude/skills/skd-compile/SKILL.md
index 2da95218..afa3beaa 100644
--- a/.claude/skills/skd-compile/SKILL.md
+++ b/.claude/skills/skd-compile/SKILL.md
@@ -31,7 +31,7 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
## JSON DSL — краткий справочник
-Полная спецификация: `docs/skd-dsl-spec.md`.
+Справочник ниже. Все примеры компилируемы как есть.
### Корневая структура
@@ -41,6 +41,8 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
"calculatedFields": [...],
"totalFields": [...],
"parameters": [...],
+ "templates": [...],
+ "groupTemplates": [...],
"dataSetLinks": [...],
"settingsVariants": [...]
}
@@ -58,7 +60,7 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
Запрос поддерживает `@file` — ссылку на внешний .sql файл вместо inline-текста: `"query": "@queries/sales.sql"`. Путь разрешается относительно JSON-файла, затем CWD.
-### Поля — shorthand
+### Поля — shorthand и объектная форма
```
"Наименование" — просто имя
@@ -67,6 +69,12 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
"Служебное: string #noFilter #noOrder" — + ограничения
```
+Объектная форма — когда нужен title или другие свойства:
+```json
+{ "field": "ОстатокНаНачалоПериода", "title": "Остаток на начало периода" }
+```
+`dataPath` автоматически берётся из `field`, если не указан явно.
+
Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией.
**Синонимы типов** (русские и альтернативные): `число` = decimal, `строка` = string, `булево` = boolean, `дата` = date, `датаВремя` = dateTime, `СтандартныйПериод` = StandardPeriod, `СправочникСсылка.X` = CatalogRef.X, `ДокументСсылка.X` = DocumentRef.X, `int`/`number` = decimal, `bool` = boolean. Регистронезависимые.
@@ -143,6 +151,7 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
```json
"settingsVariants": [{
"name": "Основной",
+ "title": "Продажи по организациям",
"settings": {
"selection": ["Номенклатура", "Количество", "Auto"],
"filter": ["Организация = _ @off @user"],
@@ -188,6 +197,53 @@ powershell.exe -NoProfile -File .claude/skills/skd-compile/scripts/skd-compile.p
]
```
+### Шаблоны вывода — компактный DSL
+
+Вместо raw XML (`template`) — табличное описание через `rows` + именованный стиль `style`:
+
+```json
+"templates": [
+ {
+ "name": "Макет1",
+ "style": "header",
+ "widths": [36, 33, 16, 17],
+ "minHeight": 24.75,
+ "rows": [
+ ["Виды кассы", "Валюта", "Остаток на начало\nпериода", "Остаток на\nконец периода"],
+ ["|", "|", "|", "|"],
+ ["К1", "К2", "К3", "К4"]
+ ]
+ },
+ {
+ "name": "Макет2",
+ "style": "data",
+ "widths": [36, 33, 16, 17],
+ "rows": [["{ВидКассы}", "{Валюта}", "{Остаток}", "{ОстатокКонец}"]],
+ "parameters": [
+ { "name": "ВидКассы", "expression": "Представление(Счет)" },
+ { "name": "Остаток", "expression": "ОстатокНаНачалоПериода" }
+ ]
+ }
+]
+```
+
+Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `null` — пустая.
+
+Встроенные стили: `header` (фон, центр, перенос), `data` (фон группы), `subheader` (без фона, центр), `total` (без фона). Все — Arial 10, рамки Solid 1px, цвета через стили платформы.
+
+Пользовательские стили: файл `skd-styles.json` рядом с JSON или в корне проекта. Все допустимые ключи и формат цветов — в `examples/skd-styles.json`.
+
+Raw XML (`"template": "<...>"`) остаётся как fallback. Детект: если есть `rows` — DSL, иначе — raw.
+
+### Привязки макетов к группировкам
+
+```json
+"groupTemplates": [
+ { "groupField": "Счет", "templateType": "GroupHeader", "template": "Макет1" },
+ { "groupField": "Счет", "templateType": "Header", "template": "Макет2" }
+]
+```
+
## Примеры
### Минимальный
diff --git a/.claude/skills/skd-compile/examples/skd-styles.json b/.claude/skills/skd-compile/examples/skd-styles.json
new file mode 100644
index 00000000..0ec04688
--- /dev/null
+++ b/.claude/skills/skd-compile/examples/skd-styles.json
@@ -0,0 +1,30 @@
+{
+ "header": {
+ "font": "Arial",
+ "fontSize": 10,
+ "bold": false,
+ "italic": false,
+ "hAlign": "Center",
+ "vAlign": "Center",
+ "wrap": true,
+ "bgColor": "style:ReportHeaderBackColor",
+ "textColor": null,
+ "borderColor": "style:ReportLineColor",
+ "borders": true
+ },
+ "data": {
+ "bgColor": "style:ReportGroup1BackColor",
+ "borderColor": "style:ReportLineColor"
+ },
+ "total": {
+ "bold": true,
+ "borderColor": "style:ReportLineColor"
+ },
+ "myProjectStyle": {
+ "bgColor": "#FFE0E0",
+ "textColor": "#990000",
+ "fontSize": 12,
+ "bold": true,
+ "hAlign": "Right"
+ }
+}
diff --git a/.claude/skills/skd-compile/scripts/skd-compile.ps1 b/.claude/skills/skd-compile/scripts/skd-compile.ps1
index bb02e1ae..918389e7 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.1 — Compile 1C DCS from JSON
+# skd-compile v1.2 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -529,7 +529,7 @@ function Emit-Field {
$f = Parse-FieldShorthand $fieldDef
} else {
$f = @{
- dataPath = "$($fieldDef.dataPath)"
+ dataPath = if ($fieldDef.dataPath) { "$($fieldDef.dataPath)" } elseif ($fieldDef.field) { "$($fieldDef.field)" } else { "" }
field = if ($fieldDef.field) { "$($fieldDef.field)" } else { "$($fieldDef.dataPath)" }
title = if ($fieldDef.title) { "$($fieldDef.title)" } else { "" }
type = if ($fieldDef.type) { Resolve-TypeStr "$($fieldDef.type)" } else { "" }
@@ -931,25 +931,286 @@ function Emit-ParamValue {
}
}
+# === AreaTemplate DSL ===
+
+# Built-in style presets
+$script:areaStylePresets = @{
+ data = @{
+ font = 'Arial'; fontSize = 10; bold = $false; italic = $false
+ hAlign = $null; vAlign = $null; wrap = $false
+ bgColor = 'style:ReportGroup1BackColor'; textColor = $null
+ borderColor = 'style:ReportLineColor'; borders = $true
+ }
+ header = @{
+ font = 'Arial'; fontSize = 10; bold = $false; italic = $false
+ hAlign = 'Center'; vAlign = $null; wrap = $true
+ bgColor = 'style:ReportHeaderBackColor'; textColor = $null
+ borderColor = 'style:ReportLineColor'; borders = $true
+ }
+ subheader = @{
+ font = 'Arial'; fontSize = 10; bold = $false; italic = $false
+ hAlign = 'Center'; vAlign = $null; wrap = $true
+ bgColor = $null; textColor = $null
+ borderColor = 'style:ReportLineColor'; borders = $true
+ }
+ total = @{
+ font = 'Arial'; fontSize = 10; bold = $false; italic = $false
+ hAlign = $null; vAlign = $null; wrap = $false
+ bgColor = $null; textColor = $null
+ borderColor = 'style:ReportLineColor'; borders = $true
+ }
+}
+
+# Load user presets from skd-styles.json (same dir as definition or cwd)
+$script:userStylesLoaded = $false
+foreach ($stylesDir in @($script:queryBaseDir, (Get-Location).Path)) {
+ $stylesFile = Join-Path $stylesDir "skd-styles.json"
+ if (Test-Path $stylesFile) {
+ $userStyles = Get-Content -Raw -Encoding UTF8 $stylesFile | ConvertFrom-Json
+ foreach ($prop in $userStyles.PSObject.Properties) {
+ $preset = @{}
+ # Start from 'data' defaults
+ foreach ($k in $script:areaStylePresets['data'].Keys) {
+ $preset[$k] = $script:areaStylePresets['data'][$k]
+ }
+ # If overriding existing preset, start from it instead
+ if ($script:areaStylePresets.ContainsKey($prop.Name)) {
+ foreach ($k in $script:areaStylePresets[$prop.Name].Keys) {
+ $preset[$k] = $script:areaStylePresets[$prop.Name][$k]
+ }
+ }
+ # Apply user overrides
+ foreach ($up in $prop.Value.PSObject.Properties) {
+ $preset[$up.Name] = $up.Value
+ }
+ $script:areaStylePresets[$prop.Name] = $preset
+ }
+ $script:userStylesLoaded = $true
+ break
+ }
+}
+
+function Emit-ColorValue {
+ param([string]$color, [string]$indent)
+ if ($color.StartsWith('style:')) {
+ $styleName = $color.Substring(6)
+ X "$indentd8p1:$styleName"
+ } else {
+ X "$indent$(Esc-Xml $color)"
+ }
+}
+
+function Emit-CellAppearance {
+ param($style, [double]$width = 0, [bool]$vMerge = $false, [double]$minHeight = 0)
+ $ind = "`t`t`t`t`t"
+ X "`t`t`t`t"
+ # Background color
+ if ($style.bgColor) {
+ X "$ind"
+ X "$ind`tЦветФона"
+ Emit-ColorValue $style.bgColor "$ind`t"
+ X "$ind"
+ }
+ # Text color
+ if ($style.textColor) {
+ X "$ind"
+ X "$ind`tЦветТекста"
+ Emit-ColorValue $style.textColor "$ind`t"
+ X "$ind"
+ }
+ # Border color + border style (4 sides)
+ if ($style.borders) {
+ if ($style.borderColor) {
+ X "$ind"
+ X "$ind`tЦветГраницы"
+ Emit-ColorValue $style.borderColor "$ind`t"
+ X "$ind"
+ }
+ X "$ind"
+ X "$ind`tСтильГраницы"
+ X "$ind`t"
+ X "$ind`t`tNone"
+ X "$ind`t"
+ foreach ($side in @('Слева','Сверху','Справа','Снизу')) {
+ X "$ind`t"
+ X "$ind`t`tСтильГраницы.$side"
+ X "$ind`t`t"
+ X "$ind`t`t`tSolid"
+ X "$ind`t`t"
+ X "$ind`t"
+ }
+ X "$ind"
+ }
+ # Font
+ $boldStr = if ($style.bold) { "true" } else { "false" }
+ $italicStr = if ($style.italic) { "true" } else { "false" }
+ X "$ind"
+ X "$ind`tШрифт"
+ X "$ind`t"
+ X "$ind"
+ # Horizontal alignment
+ if ($style.hAlign) {
+ X "$ind"
+ X "$ind`tГоризонтальноеПоложение"
+ X "$ind`t$(Esc-Xml $style.hAlign)"
+ X "$ind"
+ }
+ # Vertical alignment
+ if ($style.vAlign) {
+ X "$ind"
+ X "$ind`tВертикальноеПоложение"
+ X "$ind`t$(Esc-Xml $style.vAlign)"
+ X "$ind"
+ }
+ # Text placement (wrap)
+ if ($style.wrap) {
+ X "$ind"
+ X "$ind`tРазмещение"
+ X "$ind`tWrap"
+ X "$ind"
+ }
+ # Width
+ if ($width -gt 0) {
+ X "$ind"
+ X "$ind`tМинимальнаяШирина"
+ X "$ind`t$width"
+ X "$ind"
+ X "$ind"
+ X "$ind`tМаксимальнаяШирина"
+ X "$ind`t$width"
+ X "$ind"
+ }
+ # Min height
+ if ($minHeight -gt 0) {
+ X "$ind"
+ X "$ind`tМинимальнаяВысота"
+ X "$ind`t$minHeight"
+ X "$ind"
+ }
+ # Vertical merge
+ if ($vMerge) {
+ X "$ind"
+ X "$ind`tОбъединятьПоВертикали"
+ X "$ind`ttrue"
+ X "$ind"
+ }
+ X "`t`t`t`t"
+}
+
+function Emit-AreaTemplateDSL {
+ param($t)
+ $styleName = if ($t.style) { "$($t.style)" } else { "data" }
+ if (-not $script:areaStylePresets.ContainsKey($styleName)) {
+ Write-Warning "Unknown area style preset '$styleName', falling back to 'data'"
+ $styleName = "data"
+ }
+ $style = $script:areaStylePresets[$styleName]
+
+ $rows = @($t.rows)
+ $widths = if ($t.widths) { @($t.widths) } else { @() }
+ $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
+ $vMerge = @{}
+ for ($r = $rows.Count - 1; $r -ge 1; $r--) {
+ $vMerge[$r] = @{}
+ for ($c = 0; $c -lt $colCount; $c++) {
+ $cellVal = $rows[$r][$c]
+ if ($cellVal -is [string] -and $cellVal -eq '|') {
+ $vMerge[$r][$c] = $true
+ }
+ }
+ }
+ if (-not $vMerge.ContainsKey(0)) { $vMerge[0] = @{} }
+
+ X "`t"
+ X "`t`t$(Esc-Xml "$($t.name)")"
+ X "`t`t"
+
+ for ($r = 0; $r -lt $rows.Count; $r++) {
+ X "`t`t`t"
+ 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
+ # 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++) {
+ if ($vMerge[$nr][$c] -eq $true) { $startsVMerge = $true } else { break }
+ }
+
+ X "`t`t`t`t"
+ if ($isMerged) {
+ # Merged cell — only appearance with vMerge flag + width
+ Emit-CellAppearance $style $w $true
+ } else {
+ # Cell value
+ if ($null -ne $cellVal -and $cellVal -ne '') {
+ $cellStr = "$cellVal"
+ if ($cellStr -match '^\{(.+)\}$') {
+ # Parameter reference
+ X "`t`t`t`t`t"
+ X "`t`t`t`t`t`t$(Esc-Xml $Matches[1])"
+ X "`t`t`t`t`t"
+ } else {
+ # Static text
+ X "`t`t`t`t`t"
+ X "`t`t`t`t`t`t"
+ X "`t`t`t`t`t`t`t"
+ X "`t`t`t`t`t`t`t`tru"
+ X "`t`t`t`t`t`t`t`t$(Esc-Xml $cellStr)"
+ X "`t`t`t`t`t`t`t"
+ X "`t`t`t`t`t`t"
+ X "`t`t`t`t`t"
+ }
+ }
+ # Appearance
+ $h = if ($r -eq 0) { $minHeight } else { 0 }
+ Emit-CellAppearance $style $w $startsVMerge $h
+ }
+ X "`t`t`t`t"
+ }
+ X "`t`t`t"
+ }
+
+ X "`t`t"
+ # Parameters (reuse existing logic)
+ if ($t.parameters) {
+ foreach ($tp in $t.parameters) {
+ X "`t`t"
+ X "`t`t`t$(Esc-Xml "$($tp.name)")"
+ X "`t`t`t$(Esc-Xml "$($tp.expression)")"
+ X "`t`t"
+ }
+ }
+ X "`t"
+}
+
# === Templates ===
function Emit-Templates {
if (-not $def.templates) { return }
foreach ($t in $def.templates) {
- X "`t"
- X "`t`t$(Esc-Xml "$($t.name)")"
- if ($t.template) {
- # Raw XML content
- X "`t`t$($t.template)"
- }
- if ($t.parameters) {
- foreach ($tp in $t.parameters) {
- X "`t`t"
- X "`t`t`t$(Esc-Xml "$($tp.name)")"
- X "`t`t`t$(Esc-Xml "$($tp.expression)")"
- X "`t`t"
+ if ($t.rows) {
+ # Compact DSL mode
+ Emit-AreaTemplateDSL $t
+ } else {
+ # Raw XML mode
+ X "`t"
+ X "`t`t$(Esc-Xml "$($t.name)")"
+ if ($t.template) {
+ X "`t`t$($t.template)"
}
+ if ($t.parameters) {
+ foreach ($tp in $t.parameters) {
+ X "`t`t"
+ X "`t`t`t$(Esc-Xml "$($tp.name)")"
+ X "`t`t`t$(Esc-Xml "$($tp.expression)")"
+ X "`t`t"
+ }
+ }
+ X "`t"
}
- X "`t"
}
}
@@ -1565,7 +1826,7 @@ function Emit-SettingsVariants {
X "`t"
X "`t`t$(Esc-Xml "$($v.name)")"
- $pres = if ($v.presentation) { "$($v.presentation)" } else { "$($v.name)" }
+ $pres = if ($v.presentation) { "$($v.presentation)" } elseif ($v.title) { "$($v.title)" } else { "$($v.name)" }
X "`t`t"
X "`t`t`t"
X "`t`t`t`tru"
diff --git a/.claude/skills/skd-compile/scripts/skd-compile.py b/.claude/skills/skd-compile/scripts/skd-compile.py
index 551f152e..3f41f3ae 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.1 — Compile 1C DCS from JSON
+# skd-compile v1.2 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -417,7 +417,7 @@ def emit_field(lines, field_def, indent):
f = parse_field_shorthand(field_def)
else:
f = {
- 'dataPath': str(field_def.get('dataPath', '')),
+ 'dataPath': str(field_def.get('dataPath', '')) or str(field_def.get('field', '')),
'field': str(field_def.get('field', '')) or str(field_def.get('dataPath', '')),
'title': str(field_def.get('title', '')) if field_def.get('title') else '',
'type': resolve_type_str(str(field_def['type'])) if field_def.get('type') else '',
@@ -772,23 +772,238 @@ def emit_parameters(lines, defn):
emit_single_param(lines, None, end_parsed)
+# === AreaTemplate DSL ===
+
+AREA_STYLE_PRESETS = {
+ 'data': {
+ 'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False,
+ 'hAlign': None, 'vAlign': None, 'wrap': False,
+ 'bgColor': 'style:ReportGroup1BackColor', 'textColor': None,
+ 'borderColor': 'style:ReportLineColor', 'borders': True,
+ },
+ 'header': {
+ 'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False,
+ 'hAlign': 'Center', 'vAlign': None, 'wrap': True,
+ 'bgColor': 'style:ReportHeaderBackColor', 'textColor': None,
+ 'borderColor': 'style:ReportLineColor', 'borders': True,
+ },
+ 'subheader': {
+ 'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False,
+ 'hAlign': 'Center', 'vAlign': None, 'wrap': True,
+ 'bgColor': None, 'textColor': None,
+ 'borderColor': 'style:ReportLineColor', 'borders': True,
+ },
+ 'total': {
+ 'font': 'Arial', 'fontSize': 10, 'bold': False, 'italic': False,
+ 'hAlign': None, 'vAlign': None, 'wrap': False,
+ 'bgColor': None, 'textColor': None,
+ 'borderColor': 'style:ReportLineColor', 'borders': True,
+ },
+}
+
+
+def load_user_styles(base_dir):
+ for d in [base_dir, os.getcwd()]:
+ p = os.path.join(d, 'skd-styles.json')
+ if os.path.isfile(p):
+ with open(p, 'r', encoding='utf-8-sig') as f:
+ user_styles = json.load(f)
+ for name, overrides in user_styles.items():
+ base = dict(AREA_STYLE_PRESETS.get(name, AREA_STYLE_PRESETS['data']))
+ base.update(overrides)
+ AREA_STYLE_PRESETS[name] = base
+ return
+
+
+def _emit_color_value(lines, color, indent):
+ if color.startswith('style:'):
+ style_name = color[6:]
+ lines.append(f'{indent}d8p1:{style_name}')
+ else:
+ lines.append(f'{indent}{esc_xml(color)}')
+
+
+def _emit_cell_appearance(lines, style, width=0, v_merge=False, min_height=0):
+ ind = '\t\t\t\t\t'
+ lines.append('\t\t\t\t')
+ # Background color
+ if style.get('bgColor'):
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u0426\u0432\u0435\u0442\u0424\u043e\u043d\u0430')
+ _emit_color_value(lines, style['bgColor'], f'{ind}\t')
+ lines.append(f'{ind}')
+ # Text color
+ if style.get('textColor'):
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u0426\u0432\u0435\u0442\u0422\u0435\u043a\u0441\u0442\u0430')
+ _emit_color_value(lines, style['textColor'], f'{ind}\t')
+ lines.append(f'{ind}')
+ # Borders
+ if style.get('borders'):
+ if style.get('borderColor'):
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u0426\u0432\u0435\u0442\u0413\u0440\u0430\u043d\u0438\u0446\u044b')
+ _emit_color_value(lines, style['borderColor'], f'{ind}\t')
+ lines.append(f'{ind}')
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u0421\u0442\u0438\u043b\u044c\u0413\u0440\u0430\u043d\u0438\u0446\u044b')
+ lines.append(f'{ind}\t')
+ lines.append(f'{ind}\t\tNone')
+ lines.append(f'{ind}\t')
+ for side in ['\u0421\u043b\u0435\u0432\u0430', '\u0421\u0432\u0435\u0440\u0445\u0443', '\u0421\u043f\u0440\u0430\u0432\u0430', '\u0421\u043d\u0438\u0437\u0443']:
+ lines.append(f'{ind}\t')
+ lines.append(f'{ind}\t\t\u0421\u0442\u0438\u043b\u044c\u0413\u0440\u0430\u043d\u0438\u0446\u044b.{side}')
+ lines.append(f'{ind}\t\t')
+ lines.append(f'{ind}\t\t\tSolid')
+ lines.append(f'{ind}\t\t')
+ lines.append(f'{ind}\t')
+ lines.append(f'{ind}')
+ # Font
+ bold_str = 'true' if style.get('bold') else 'false'
+ italic_str = 'true' if style.get('italic') else 'false'
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u0428\u0440\u0438\u0444\u0442')
+ lines.append(f'{ind}\t')
+ lines.append(f'{ind}')
+ # Horizontal alignment
+ if style.get('hAlign'):
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u0413\u043e\u0440\u0438\u0437\u043e\u043d\u0442\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435')
+ lines.append(f'{ind}\t{esc_xml(style["hAlign"])}')
+ lines.append(f'{ind}')
+ # Vertical alignment
+ if style.get('vAlign'):
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u0412\u0435\u0440\u0442\u0438\u043a\u0430\u043b\u044c\u043d\u043e\u0435\u041f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435')
+ lines.append(f'{ind}\t{esc_xml(style["vAlign"])}')
+ lines.append(f'{ind}')
+ # Wrap
+ if style.get('wrap'):
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u0420\u0430\u0437\u043c\u0435\u0449\u0435\u043d\u0438\u0435')
+ lines.append(f'{ind}\tWrap')
+ lines.append(f'{ind}')
+ # Width
+ 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}')
+ 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}')
+ # Min height
+ if min_height and min_height > 0:
+ lines.append(f'{ind}')
+ lines.append(f'{ind}\t\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u0430\u044f\u0412\u044b\u0441\u043e\u0442\u0430')
+ lines.append(f'{ind}\t{min_height}')
+ lines.append(f'{ind}')
+ # Vertical merge
+ if v_merge:
+ lines.append(f'{ind}')
+ 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}')
+ lines.append('\t\t\t\t')
+
+
+def _emit_area_template_dsl(lines, t):
+ style_name = str(t.get('style', '')) or 'data'
+ if style_name not in AREA_STYLE_PRESETS:
+ print(f"Warning: Unknown area style preset '{style_name}', falling back to 'data'", file=sys.stderr)
+ style_name = 'data'
+ style = AREA_STYLE_PRESETS[style_name]
+
+ rows = list(t['rows'])
+ widths = list(t.get('widths', []))
+ min_height = float(t.get('minHeight', 0))
+ col_count = len(widths) if widths else len(rows[0])
+
+ # Build merge map
+ v_merge = {}
+ for r in range(len(rows) - 1, 0, -1):
+ v_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 == '|':
+ v_merge[r][c] = True
+ if 0 not in v_merge:
+ v_merge[0] = {}
+
+ lines.append('\t')
+ lines.append(f'\t\t{esc_xml(str(t["name"]))}')
+ lines.append('\t\t')
+
+ for r in range(len(rows)):
+ lines.append('\t\t\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)
+ # Check if this cell starts a vertical merge
+ starts_v_merge = False
+ for nr in range(r + 1, len(rows)):
+ if v_merge.get(nr, {}).get(c, False):
+ starts_v_merge = True
+ else:
+ break
+
+ lines.append('\t\t\t\t')
+ if is_merged:
+ _emit_cell_appearance(lines, style, w, True)
+ else:
+ if cell_val is not None and str(cell_val) != '':
+ cell_str = str(cell_val)
+ m = re.match(r'^\{(.+)\}$', cell_str)
+ if m:
+ lines.append('\t\t\t\t\t')
+ lines.append(f'\t\t\t\t\t\t{esc_xml(m.group(1))}')
+ lines.append('\t\t\t\t\t')
+ else:
+ lines.append('\t\t\t\t\t')
+ lines.append('\t\t\t\t\t\t')
+ lines.append('\t\t\t\t\t\t\t')
+ lines.append('\t\t\t\t\t\t\t\tru')
+ lines.append(f'\t\t\t\t\t\t\t\t{esc_xml(cell_str)}')
+ lines.append('\t\t\t\t\t\t\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)
+ lines.append('\t\t\t\t')
+ lines.append('\t\t\t')
+
+ lines.append('\t\t')
+ if t.get('parameters'):
+ for tp in t['parameters']:
+ lines.append('\t\t')
+ lines.append(f'\t\t\t{esc_xml(str(tp["name"]))}')
+ lines.append(f'\t\t\t{esc_xml(str(tp["expression"]))}')
+ lines.append('\t\t')
+ lines.append('\t')
+
+
# === Templates ===
def emit_templates(lines, defn):
if not defn.get('templates'):
return
for t in defn['templates']:
- lines.append('\t')
- lines.append(f'\t\t{esc_xml(str(t["name"]))}')
- if t.get('template'):
- lines.append(f'\t\t{t["template"]}')
- if t.get('parameters'):
- for tp in t['parameters']:
- lines.append('\t\t')
- lines.append(f'\t\t\t{esc_xml(str(tp["name"]))}')
- lines.append(f'\t\t\t{esc_xml(str(tp["expression"]))}')
- lines.append('\t\t')
- lines.append('\t')
+ if t.get('rows'):
+ _emit_area_template_dsl(lines, t)
+ else:
+ lines.append('\t')
+ lines.append(f'\t\t{esc_xml(str(t["name"]))}')
+ if t.get('template'):
+ lines.append(f'\t\t{t["template"]}')
+ if t.get('parameters'):
+ for tp in t['parameters']:
+ lines.append('\t\t')
+ lines.append(f'\t\t\t{esc_xml(str(tp["name"]))}')
+ lines.append(f'\t\t\t{esc_xml(str(tp["expression"]))}')
+ lines.append('\t\t')
+ lines.append('\t')
# === GroupTemplates ===
@@ -1288,7 +1503,7 @@ def emit_settings_variants(lines, defn):
lines.append('\t')
lines.append(f'\t\t{esc_xml(str(v["name"]))}')
- pres = str(v.get('presentation', '')) or str(v['name'])
+ pres = str(v.get('presentation', '')) or str(v.get('title', '')) or str(v['name'])
lines.append('\t\t')
lines.append('\t\t\t')
lines.append('\t\t\t\tru')
@@ -1375,6 +1590,9 @@ def main():
global query_base_dir
query_base_dir = os.path.dirname(def_file) if args.DefinitionFile else os.getcwd()
+ # Load user style presets
+ load_user_styles(query_base_dir)
+
# --- 2. Resolve defaults ---
# DataSources
diff --git a/docs/skd-dsl-spec.md b/docs/skd-dsl-spec.md
index 4e419667..a3dcb43f 100644
--- a/docs/skd-dsl-spec.md
+++ b/docs/skd-dsl-spec.md
@@ -712,9 +712,99 @@ XML-маппинг — по `` на каждый элемент:
## 10. Макеты и привязки (templates, groupTemplates)
-Редко используются. Поддерживаются в объектной форме, близкой к XML.
+### templates — компактный DSL (рекомендуемый)
-### templates
+Табличное описание шаблона вывода. Содержимое задаётся через `rows`, оформление — через именованный пресет `style`.
+
+```json
+"templates": [
+ {
+ "name": "Макет1",
+ "style": "header",
+ "widths": [36, 33, 16, 17],
+ "minHeight": 24.75,
+ "rows": [
+ ["Виды кассы", "Валюта", "Остаток на начало\nпериода", "Остаток на\nконец\nпериода"],
+ ["|", "|", "|", "|"],
+ ["К1", "К2", "К3", "К4"]
+ ]
+ },
+ {
+ "name": "Макет2",
+ "style": "data",
+ "widths": [36, 33, 16, 17],
+ "rows": [["{ВидКассы}", "{Валюта}", "{ОстатокНачало}", "{ОстатокКонец}"]],
+ "parameters": [
+ { "name": "ВидКассы", "expression": "Представление(СчетМеждународногоУчета)" },
+ { "name": "ОстатокНачало", "expression": "ОстатокНаНачалоПериода" }
+ ]
+ },
+ {
+ "name": "Макет3",
+ "style": "total",
+ "widths": [36, 33, 16, 17],
+ "rows": [["Итого", "Х", "{ОстатокНачало}", "{ОстатокКонец}"]],
+ "parameters": [
+ { "name": "ОстатокНачало", "expression": "ОстатокНаНачалоПериода" }
+ ]
+ }
+]
+```
+
+#### Свойства шаблона
+
+| Свойство | Описание |
+|----------|----------|
+| `name` | Имя макета (ссылаются groupTemplate) |
+| `rows` | Массив строк; каждая строка — массив ячеек |
+| `style` | Именованный пресет оформления (по умолчанию `"data"`) |
+| `widths` | Массив ширин колонок (применяется ко всем строкам) |
+| `minHeight` | Минимальная высота первой строки (для шапок) |
+| `parameters` | Параметры макета — выражения для подстановки |
+
+#### Синтаксис ячеек
+
+| Значение | Описание |
+|----------|----------|
+| `"текст"` | Статический текст (`v8:LocalStringType`) |
+| `"{Имя}"` | Параметр шаблона (`dcscor:Parameter`), задаётся через `parameters` |
+| `"\|"` | Вертикальное объединение с ячейкой выше |
+| `null` | Пустая ячейка (без содержимого) |
+
+#### Встроенные пресеты стилей
+
+| Пресет | Фон | Шрифт | Выравнивание | Перенос | Рамки |
+|--------|-----|-------|-------------|---------|-------|
+| `header` | ReportHeaderBackColor | Arial 10 | Center | да | Solid 1px |
+| `data` | ReportGroup1BackColor | Arial 10 | — | нет | Solid 1px |
+| `subheader` | — | Arial 10 | Center | да | Solid 1px |
+| `total` | — | Arial 10 | — | нет | Solid 1px |
+
+#### Пользовательские пресеты (skd-styles.json)
+
+Файл `skd-styles.json` в директории определения или в корне проекта. Переопределяет встроенные пресеты или добавляет новые:
+
+```json
+{
+ "header": {
+ "bgColor": "style:ReportHeaderBackColor",
+ "borderColor": "style:ReportLineColor",
+ "bold": true
+ },
+ "myStyle": {
+ "font": "Arial", "fontSize": 12,
+ "bgColor": "#FFE0E0"
+ }
+}
+```
+
+Допустимые ключи: `font`, `fontSize`, `bold`, `italic`, `hAlign`, `vAlign`, `wrap`, `bgColor`, `textColor`, `borderColor`, `borders`. Недостающие ключи берутся из пресета `data`.
+
+Формат цветов: `"style:ИмяСтиля"` (ссылка на стиль платформы) или `"#RRGGBB"` (прямой цвет).
+
+### templates — raw XML (fallback)
+
+Для нестандартных случаев — raw XML вставляется как есть:
```json
"templates": [
@@ -728,6 +818,8 @@ XML-маппинг — по `` на каждый элемент:
]
```
+Детект: если есть `rows` — используется компактный DSL, иначе — raw XML из `template`.
+
### groupTemplates
```json
diff --git a/docs/skd-guide.md b/docs/skd-guide.md
index 7b559490..39a2c021 100644
--- a/docs/skd-guide.md
+++ b/docs/skd-guide.md
@@ -100,6 +100,40 @@
- **structure shorthand**: `"Поле1 > Поле2 > details"` — `>` разделяет уровни группировки
- **conditionalAppearance**: условное оформление с автоопределением типов значений (Color, Boolean, LocalStringType)
+### Шаблоны вывода — компактный DSL
+
+Для отчётов с фиксированным оформлением (ФСД, ведомости) — табличное описание вместо raw XML:
+
+```json
+"templates": [
+ {
+ "name": "Макет1", "style": "header",
+ "widths": [36, 33, 16, 17], "minHeight": 24.75,
+ "rows": [
+ ["Виды кассы", "Валюта", "Остаток на начало\nпериода", "Остаток на конец периода"],
+ ["|", "|", "|", "|"],
+ ["К1", "К2", "К3", "К4"]
+ ]
+ },
+ {
+ "name": "Макет2", "style": "data",
+ "widths": [36, 33, 16, 17],
+ "rows": [["{ВидКассы}", "{Валюта}", "{Остаток}", "{ОстатокКонец}"]],
+ "parameters": [
+ { "name": "ВидКассы", "expression": "Представление(Счет)" }
+ ]
+ }
+],
+"groupTemplates": [
+ { "groupField": "Счет", "templateType": "GroupHeader", "template": "Макет1" },
+ { "groupField": "Счет", "templateType": "Header", "template": "Макет2" }
+]
+```
+
+Синтаксис ячеек: `"текст"` — статика, `"{Имя}"` — параметр, `"|"` — объединение с ячейкой выше, `null` — пустая.
+
+Встроенные стили: `header` (фон, центр, перенос), `data` (фон группы), `subheader` (без фона, центр), `total` (без фона). Все — Arial 10, рамки Solid 1px, цвета через стили платформы. Пользовательские стили — через `skd-styles.json` в директории проекта.
+
### Объектная форма
Все секции поддерживают полную объектную форму для сложных случаев (title, appearance, role с выражениями, userSettingID, userSettingPresentation, conditionalAppearance, группы фильтров And/Or/Not и т.д.). Подробности — в [спецификации SKD DSL](skd-dsl-spec.md).