diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index ae7b7833..d514bab0 100644 --- a/.claude/skills/mxl-compile/SKILL.md +++ b/.claude/skills/mxl-compile/SKILL.md @@ -59,7 +59,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J styles: { name: { font, horizontalAlignment, verticalAlignment, textPlacement, backColor, textColor, border, borderColor, format, hidden } }, areas: [{ name, columnSet, rows: [{ height, hidden, rowStyle, cells: [ - { col, span, rowspan, style, param, detail, text, template, valueType, controlType, value } + { col, span, rowspan, style, param, detail, text, template, valueType, controlType, value, note } ]}]}], namedAreas: [{ name, rows, cols }], columnSets: { name: { columns, columnWidths, columnStyles } } @@ -80,6 +80,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J - `col` — 1-based позиция колонки - `rowspan` — объединение строк вниз (rowStyle учитывает занятые ячейки) - Содержимое ячейки задаётся одним из ключей: `param` — параметр заполнения, `text` — статический текст, `template` — текст со вставками `[Параметр]` +- `note` — всплывающая подсказка у ячейки: строка, объект языков или `{ text, style, autoSize, box }` - `valueType` делает ячейку полем ввода (`"Number(15,3,nonneg)"`, `"String(10)"`, `"CatalogRef.Валюты"`, составной через ` + `); текста в такой ячейке быть не может. `value` — значение в поле, `controlType: "checkbox"` — флажок вместо поля ввода Двухуровневая шапка массивами: diff --git a/.claude/skills/mxl-compile/reference/dsl-spec.md b/.claude/skills/mxl-compile/reference/dsl-spec.md index ccb073e4..1f695904 100644 --- a/.claude/skills/mxl-compile/reference/dsl-spec.md +++ b/.claude/skills/mxl-compile/reference/dsl-spec.md @@ -152,7 +152,8 @@ | `{ ... }` | Обычная ячейка **без** `col`; нужна для `style`, `detail`, `template` | Объект-элемент трактуется по его ключам: если среди них есть ключ ячейки (`span`, `rowspan`, -`style`, `param`, `detail`, `text`, `template`, `valueType`, `controlType`, `value`) — объект +`style`, `param`, `detail`, `text`, `template`, `valueType`, `controlType`, `value`, +`note`) — объект описывает свойства ячейки. Иначе он целиком считается её текстом, а его ключи — идентификаторами языков. @@ -186,6 +187,7 @@ | `valueType` | нет | — | Тип значения: ячейка становится полем ввода (см. ниже) | | `controlType` | нет | `input` | Элемент управления поля ввода: `input` или `checkbox`. Только вместе с `valueType` | | `value` | нет | — | Значение в поле ввода. Только вместе с `valueType` | +| `note` | нет | — | Примечание к ячейке (см. ниже) | ### Содержимое ячейки @@ -265,6 +267,34 @@ `controlType` нужен редко: умолчание платформы — поле ввода, и оно подходит всем типам, включая `Boolean`. Флажок задаётся явно. +## Примечание к ячейке + +Всплывающая подсказка, которую платформа показывает при наведении. Задаётся ключом `note` — +строкой, объектом «язык → текст» или полной формой: + +```json +{ "col": 1, "text": "Итого", "note": "Сумма без НДС" } +{ "col": 2, "note": { "ru": "на дату документа", "en": "as of the document date" } } +{ "col": 3, "note": { "text": "не более 20%", "style": "жёлтая-подсказка" } } +{ "col": 4, "note": { "text": "…", "autoSize": false, + "box": { "top": 58, "left": -175, "bottom": 362, "right": 478 } } } +``` + +Объект трактуется по ключам — так же, как текст ячейки в короткой форме строки: есть ключ +примечания (`text`, `style`, `box`, `autoSize`) → это описание примечания, иначе ключи +считаются идентификаторами языков. + +| Поле | По умолч. | Описание | +|------|-----------|----------| +| `text` | — | Текст подсказки: строка или объект «язык → текст» | +| `style` | стиль подсказки | Имя стиля из `styles`; без него — оформление, которое даёт Конфигуратор | +| `autoSize` | `true` | Подгонять ли размер окошка под текст | +| `box` | канонический | Смещения окошка: `top`, `left` — положение, `bottom`, `right` — размер | + +Координаты ячейки в примечании не задаются — платформа привязывает конец окошка к самой ячейке, +и компилятор проставляет это сам. `autoSize` и `box` независимы: при автоподгоне размера +положение окошка всё равно хранится. + ## `rowStyle` — оформление строки Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком. @@ -280,7 +310,7 @@ round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попа XML не возвращается. - объединения, не привязанные к ячейке (по всей высоте или ширине документа); -- рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам; +- рисунки и картинки, в том числе штрихкоды; - группировки строк и колонок; - колонтитулы, параметры печати, область печати. diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index cece0a02..b83da846 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.42 — Compile 1C spreadsheet from JSON +# mxl-compile v1.43 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -933,6 +933,60 @@ function Get-CellValue { return @{ Type = 'xs:string'; Text = '' } } +# Примечание к ячейке. Из четырнадцати тегов, которые пишет платформа, настоящей информации +# несут пять: текст, стиль, признак авторазмера и четыре смещения окошка. Остальное — константы +# (drawingType, pictureSize, id) либо выводится: якорь конца это координаты самой ячейки +# (1087 примечаний корпуса, без исключений), якорь начала — 1/1 (1085 из 1087). +$script:noteKeys = @('text', 'style', 'box', 'autoSize', 'anchor') +# Стиль подсказки, который даёт Конфигуратор: 926 примечаний корпуса из 1087. +$script:noteDefaultStyle = [ordered]@{ + verticalAlignment = 'Top' + textColor = 'style:ToolTipTextColor' + backColor = 'style:ToolTipBackColor' +} +# Размер окошка платформа подбирает по тексту, и вычислить его мы не можем. Пишем самый +# частый набор корпуса; при autoSize он всё равно пересчитывается при показе. +$script:noteDefaultBox = @{ Top = -21; Left = 21; Bottom = 51; Right = 408 } + +function Test-NoteObject { + param($el) + foreach ($p in $el.PSObject.Properties) { + if ($script:noteKeys -contains $p.Name) { return $true } + } + return $false +} + +function Get-CellNote { + param($cell, [string]$where) + $raw = $cell.note + if ($null -eq $raw) { return $null } + + # Как у текста ячейки: строка — текст, объект трактуется по ключам. Ключи примечания + # и идентификаторы языков не пересекаются. + $text = $raw + $style = $null + $autoSize = $true + $box = @{} + $script:noteDefaultBox + $anchor = @{ Row = 1; Col = 1 } + + if ($raw -is [System.Management.Automation.PSCustomObject] -and (Test-NoteObject $raw)) { + $text = $raw.text + if ($null -eq $text) { $text = '' } + if ($raw.style) { $style = "$($raw.style)" } + if ($null -ne $raw.autoSize) { $autoSize = ($raw.autoSize -eq $true -or "$($raw.autoSize)" -eq 'true') } + foreach ($side in @('Top', 'Left', 'Bottom', 'Right')) { + $v = $raw.box.$side + if ($null -ne $v) { $box[$side] = [int]$v } + } + if ($raw.anchor) { + if ($null -ne $raw.anchor.row) { $anchor.Row = [int]$raw.anchor.row } + if ($null -ne $raw.anchor.col) { $anchor.Col = [int]$raw.anchor.col } + } + } + + return @{ Text = $text; Style = $style; AutoSize = $autoSize; Box = $box; Anchor = $anchor } +} + # --- 6. Format palette builder --- $formatRegistry = [ordered]@{} # key -> hashtable with properties @@ -1153,6 +1207,59 @@ function Get-FillType { return "" } +# Формат примечания — обычная запись палитры: без своего стиля берём канонический стиль подсказки. +function Register-NoteFormat { + param($note) + if ($note.Style) { + $props = Resolve-Style -styleName $note.Style -fillType "" + } else { + $props = @{} + foreach ($k in $script:noteDefaultStyle.Keys) { $props[$k] = $script:noteDefaultStyle[$k] } + } + return Register-Format $props +} + +# Порядок тегов внутри снят с корпуса: у всех 1087 примечаний он один и тот же, +# и все четырнадцать тегов присутствуют всегда. +function Emit-CellNote { + param($note, [int]$fmtIdx, [int]$row, [int]$col) + X "`t`t`t`t`t" + X "`t`t`t`t`t`tComment" + X "`t`t`t`t`t`t0" + X "`t`t`t`t`t`t$fmtIdx" + $pairs = @() + if ($note.Text -is [System.Collections.IDictionary]) { + foreach ($k in $note.Text.Keys) { $pairs += @{ Lang = "$k"; Text = "$($note.Text[$k])" } } + } elseif ($note.Text -is [System.Management.Automation.PSCustomObject]) { + foreach ($p in $note.Text.PSObject.Properties) { $pairs += @{ Lang = $p.Name; Text = "$($p.Value)" } } + } else { + foreach ($l in $textLanguages) { $pairs += @{ Lang = $l; Text = "$($note.Text)" } } + } + if ($pairs.Count -eq 0) { + X "`t`t`t`t`t`t" + } else { + X "`t`t`t`t`t`t" + foreach ($p in $pairs) { + X "`t`t`t`t`t`t`t" + X "`t`t`t`t`t`t`t`t$($p.Lang)" + X "`t`t`t`t`t`t`t`t$(Esc-XmlText $p.Text)" + X "`t`t`t`t`t`t`t" + } + X "`t`t`t`t`t`t" + } + X "`t`t`t`t`t`t$($note.Anchor.Row)" + X "`t`t`t`t`t`t$($note.Box.Top)" + X "`t`t`t`t`t`t$row" + X "`t`t`t`t`t`t$($note.Box.Bottom)" + X "`t`t`t`t`t`t$($note.Anchor.Col)" + X "`t`t`t`t`t`t$($note.Box.Left)" + X "`t`t`t`t`t`t$col" + X "`t`t`t`t`t`t$($note.Box.Right)" + X "`t`t`t`t`t`t$(if ($note.AutoSize) { 'true' } else { 'false' })" + X "`t`t`t`t`t`tStretch" + X "`t`t`t`t`t" +} + # Helper: register a cell format and return its index function Register-CellFormat { param($styleName, [string]$fillType, [hashtable]$valueProps) @@ -1188,7 +1295,7 @@ function Set-CellProp { function Test-CellObject { param($el) $cellKeys = @('col', 'span', 'rowspan', 'style', 'param', 'detail', 'text', 'template', - 'valueType', 'controlType', 'value', 'control') + 'valueType', 'controlType', 'value', 'control', 'note') foreach ($p in $el.PSObject.Properties) { if ($cellKeys -contains $p.Name) { return $true } } @@ -1371,6 +1478,8 @@ foreach ($area in $def.areas) { $ft = Get-FillType $cell $vp = Get-CellValueProps $cell "area `"$($area.name)`"" Register-CellFormat -styleName $cellStyle -fillType $ft -valueProps $vp | Out-Null + $note = Get-CellNote $cell "area `"$($area.name)`"" + if ($note) { Register-NoteFormat $note | Out-Null } } } } @@ -1653,6 +1762,8 @@ foreach ($area in $def.areas) { $ft = Get-FillType $cell $vp = Get-CellValueProps $cell "area `"$areaName`", row $($localRow + 1)" $fmtIdx = Register-CellFormat -styleName $cellStyle -fillType $ft -valueProps $vp + $cellNote = Get-CellNote $cell "area `"$areaName`", row $($localRow + 1)" + $cellNoteFmt = if ($cellNote) { Register-NoteFormat $cellNote } else { 0 } $cellInfo = @{ Col = $colStart - 1 # 0-based @@ -1663,6 +1774,8 @@ foreach ($area in $def.areas) { Template = $cell.template Value = $(if ($vp.Count -gt 0) { Get-CellValue $cell "$($vp['valueType'])" "area `"$areaName`", row $($localRow + 1)" } else { $null }) Control = $(if ($vp.Count -gt 0) { $cell.control } else { $null }) + Note = $cellNote + NoteFmt = $cellNoteFmt } $rowCells += $cellInfo @@ -1793,6 +1906,12 @@ foreach ($area in $def.areas) { X "`t`t`t`t`t$($cellInfo.Detail)" } + # Якорь конца примечания — координаты самой ячейки, поэтому его не задают: + # он выводится здесь, при эмиссии. + if ($null -ne $cellInfo.Note) { + Emit-CellNote $cellInfo.Note $cellInfo.NoteFmt $globalRow $cellInfo.Col + } + X "`t`t`t`t" X "`t`t`t" } diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index fc3e993e..45190143 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.42 — Compile 1C spreadsheet from JSON +# mxl-compile v1.43 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import hashlib @@ -644,6 +644,58 @@ def emit_value_type_content(lines, indent, canon_type): lines.extend(quals[q]) +# Примечание к ячейке. Из четырнадцати тегов, которые пишет платформа, настоящей информации +# несут пять: текст, стиль, признак авторазмера и четыре смещения окошка. Остальное — константы +# (drawingType, pictureSize, id) либо выводится: якорь конца это координаты самой ячейки +# (1087 примечаний корпуса, без исключений), якорь начала — 1/1 (1085 из 1087). +NOTE_KEYS = ('text', 'style', 'box', 'autoSize', 'anchor') +# Стиль подсказки, который даёт Конфигуратор: 926 примечаний корпуса из 1087. +NOTE_DEFAULT_STYLE = { + 'verticalAlignment': 'Top', + 'textColor': 'style:ToolTipTextColor', + 'backColor': 'style:ToolTipBackColor', +} +# Размер окошка платформа подбирает по тексту, и вычислить его мы не можем. Пишем самый +# частый набор корпуса; при autoSize он всё равно пересчитывается при показе. +NOTE_DEFAULT_BOX = {'top': -21, 'left': 21, 'bottom': 51, 'right': 408} + + +def is_note_object(el): + return any(k in el for k in NOTE_KEYS) + + +def cell_note(cell, where): + """Как у текста ячейки: строка — текст, объект трактуется по ключам. Ключи примечания + и идентификаторы языков не пересекаются.""" + raw = cell.get('note') + if raw is None: + return None + text = raw + style = None + auto_size = True + box = dict(NOTE_DEFAULT_BOX) + anchor = {'row': 1, 'col': 1} + + if isinstance(raw, dict) and is_note_object(raw): + text = raw.get('text') + if text is None: + text = '' + if raw.get('style'): + style = str(raw.get('style')) + if raw.get('autoSize') is not None: + auto_size = raw.get('autoSize') is True or str(raw.get('autoSize')).lower() == 'true' + raw_box = raw.get('box') or {} + for side in ('top', 'left', 'bottom', 'right'): + if raw_box.get(side) is not None: + box[side] = int(raw_box.get(side)) + raw_anchor = raw.get('anchor') or {} + for k in ('row', 'col'): + if raw_anchor.get(k) is not None: + anchor[k] = int(raw_anchor.get(k)) + + return {'Text': text, 'Style': style, 'AutoSize': auto_size, 'Box': box, 'Anchor': anchor} + + def cell_value(cell, canon_type, where): """Значение ячейки-поля ввода — тег САМОЙ ячейки (), а не запись палитры: у двух ячеек с одинаковым оформлением значения разные, и в дедупликацию формата оно не входит. @@ -1174,6 +1226,46 @@ def main(): return 'Template' return '' + # Формат примечания — обычная запись палитры: без своего стиля берём канонический стиль подсказки. + def register_note_format(note): + props = resolve_style(note['Style'], '') if note['Style'] else dict(NOTE_DEFAULT_STYLE) + return register_format(props) + + def emit_cell_note(lines, note, fmt_idx, row, col): + """Порядок тегов внутри снят с корпуса: у всех 1087 примечаний он один и тот же, + и все четырнадцать тегов присутствуют всегда.""" + lines.append('\t\t\t\t\t') + lines.append('\t\t\t\t\t\tComment') + lines.append('\t\t\t\t\t\t0') + lines.append(f'\t\t\t\t\t\t{fmt_idx}') + value = note['Text'] + 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] + if not pairs: + lines.append('\t\t\t\t\t\t') + else: + lines.append('\t\t\t\t\t\t') + for lang, content in pairs: + lines.append('\t\t\t\t\t\t\t') + lines.append(f'\t\t\t\t\t\t\t\t{lang}') + lines.append(f'\t\t\t\t\t\t\t\t{esc_xml_text(content)}') + lines.append('\t\t\t\t\t\t\t') + lines.append('\t\t\t\t\t\t') + box = note['Box'] + lines.append(f'\t\t\t\t\t\t{note["Anchor"]["row"]}') + lines.append(f'\t\t\t\t\t\t{box["top"]}') + lines.append(f'\t\t\t\t\t\t{row}') + lines.append(f'\t\t\t\t\t\t{box["bottom"]}') + lines.append(f'\t\t\t\t\t\t{note["Anchor"]["col"]}') + lines.append(f'\t\t\t\t\t\t{box["left"]}') + lines.append(f'\t\t\t\t\t\t{col}') + lines.append(f'\t\t\t\t\t\t{box["right"]}') + lines.append('\t\t\t\t\t\t%s' % ('true' if note['AutoSize'] else 'false')) + lines.append('\t\t\t\t\t\tStretch') + lines.append('\t\t\t\t\t') + # Helper: register a cell format and return its index def register_cell_format(style_name, fill_type, value_props=None): resolved = resolve_style(style_name, fill_type) @@ -1366,6 +1458,9 @@ def main(): ft = get_fill_type(cell) vp = cell_value_props(cell, f'area "{area.get("name") or ""}"') register_cell_format(cell_style, ft, vp) + note = cell_note(cell, f'area "{area.get("name") or ""}"') + if note: + register_note_format(note) # Формат по умолчанию — последняя запись палитры (см. выше). default_format_index = register_format({'width': default_width}) @@ -1607,6 +1702,8 @@ def main(): ft = get_fill_type(cell) vp = cell_value_props(cell, f'area "{area_name}", row {local_row + 1}') fmt_idx = register_cell_format(cell_style, ft, vp) + note = cell_note(cell, f'area "{area_name}", row {local_row + 1}') + note_fmt = register_note_format(note) if note else 0 cell_info = { 'Col': col_start - 1, # 0-based @@ -1618,6 +1715,8 @@ def main(): 'Value': (cell_value(cell, vp.get('valueType', ''), f'area "{area_name}", row {local_row + 1}') if vp else None), 'Control': (cell.get('control') if vp else None), + 'Note': note, + 'NoteFmt': note_fmt, } row_cells.append(cell_info) @@ -1737,6 +1836,12 @@ def main(): if cell_info['Detail']: lines.append(f'\t\t\t\t\t{cell_info["Detail"]}') + # Якорь конца примечания — координаты самой ячейки, поэтому его не задают: + # он выводится здесь, при эмиссии. + if cell_info.get('Note') is not None: + emit_cell_note(lines, cell_info['Note'], cell_info['NoteFmt'], + global_row, cell_info['Col']) + lines.append('\t\t\t\t') lines.append('\t\t\t') diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index c82be858..9d5be4ba 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.22 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.23 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -417,6 +417,39 @@ foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) { $value = @{ Type = $vNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance"); Text = $vNode.InnerText } } + # Примечание к ячейке. Якоря не читаем как данные: конец — координаты самой + # ячейки, начало — 1/1 у 1085 примечаний корпуса из 1087; остальное авторское. + $note = $null + $noteNode = $cContent.SelectSingleNode("d:note", $ns) + if ($noteNode) { + $noteText = [ordered]@{} + $tEl = $noteNode.SelectSingleNode("d:text", $ns) + if ($tEl) { + foreach ($it in $tEl.SelectNodes("v8:item", $ns)) { + $l = $it.SelectSingleNode("v8:lang", $ns) + $cnt = $it.SelectSingleNode("v8:content", $ns) + $noteText[$(if ($l) { $l.InnerText } else { '' })] = $(if ($cnt) { $cnt.InnerText } else { '' }) + } + } + $ng = @{} + foreach ($ch in $noteNode.ChildNodes) { + if ($ch.NodeType -eq [System.Xml.XmlNodeType]::Element) { $ng[$ch.get_LocalName()] = $ch.InnerText.Trim() } + } + $note = @{ + FormatIdx = [int]($(if ($ng['formatIndex']) { $ng['formatIndex'] } else { 0 })) + Text = $noteText + AutoSize = ($(if ($ng.ContainsKey('autoSize')) { $ng['autoSize'] } else { 'true' }) -ceq 'true') + Box = [ordered]@{ + top = [int]($(if ($ng['beginRowOffset']) { $ng['beginRowOffset'] } else { 0 })) + left = [int]($(if ($ng['beginColumnOffset']) { $ng['beginColumnOffset'] } else { 0 })) + bottom = [int]($(if ($ng['endRowOffset']) { $ng['endRowOffset'] } else { 0 })) + right = [int]($(if ($ng['endColumnOffset']) { $ng['endColumnOffset'] } else { 0 })) + } + AnchorRow = [int]($(if ($ng['beginRow']) { $ng['beginRow'] } else { 1 })) + AnchorCol = [int]($(if ($ng['beginColumn']) { $ng['beginColumn'] } else { 1 })) + } + } + # Настройки элемента управления — сериализованный base64 у самой ячейки. # Структуру не разбираем, возим дословно. $control = $null @@ -460,6 +493,7 @@ foreach ($riNode in $root.SelectNodes("d:rowsItem", $ns)) { Detail = $detail Value = $value Control = $control + Note = $note Text = $text HasText = $hasText } @@ -646,6 +680,16 @@ foreach ($r in ($rowData.Keys | Sort-Object { [int]$_ } | ForEach-Object { $rowD $key = Get-StyleKey $fmt if (-not $styleKeys.Contains($key)) { $styleKeys[$key] = $fmt } $formatToStyleKey[$cell.FormatIdx] = $key + # Формат примечания живёт в той же палитре и тоже заслуживает имени: иначе + # оформление подсказки терялось бы при обратной сборке. + if ($cell.Note) { + $nfmt = Get-Format $cell.Note.FormatIdx + if ($nfmt) { + $nkey = Get-StyleKey $nfmt + if (-not $styleKeys.Contains($nkey)) { $styleKeys[$nkey] = $nfmt } + $formatToStyleKey[$cell.Note.FormatIdx] = $nkey + } + } } } @@ -1074,7 +1118,7 @@ foreach ($area in $blocks) { $hasValue = ($cf -and $cf.Props['containsValue'] -ceq 'true') # Расшифровка сама по себе делает ячейку содержательной: в корпусе 12 653 ячейки # несут только её. Без этого такая ячейка уходила в заполнители и терялась. - $hasContent = $cell.Param -or $cell.HasText -or $hasValue -or $cell.Detail + $hasContent = $cell.Param -or $cell.HasText -or $hasValue -or $cell.Detail -or $cell.Note $hasMerge = $mergeMap.ContainsKey("$globalRow,$($cell.Col)") if ($hasContent -or $hasMerge) { @@ -1193,6 +1237,20 @@ foreach ($area in $blocks) { # с параметром, две трети расшифровок терялись молча. if ($cell.Detail) { $dslCell["detail"] = $cell.Detail } + if ($cell.Note) { + $n = $cell.Note + $dslNote = [ordered]@{} + $dslNote["text"] = Get-DslText $n.Text + $styleName = Get-StyleName $n.FormatIdx + if ($styleName -ne "default") { $dslNote["style"] = $styleName } + if (-not $n.AutoSize) { $dslNote["autoSize"] = $false } + $dslNote["box"] = $n.Box + if ($n.AnchorRow -ne 1 -or $n.AnchorCol -ne 1) { + $dslNote["anchor"] = [ordered]@{ row = $n.AnchorRow; col = $n.AnchorCol } + } + $dslCell["note"] = $dslNote + } + $dslCells += $dslCell } @@ -1335,7 +1393,14 @@ foreach ($a in $dslAreas) { $rs = $r.rowStyle $usedStyles[$(if ($rs -is [System.Collections.IDictionary]) { $rs['style'] } else { $rs })] = $true } - if ($cellList) { foreach ($c in $cellList) { if ($c -isnot [string] -and $c.style) { $usedStyles[$c.style] = $true } } } + if ($cellList) { + foreach ($c in $cellList) { + if ($c -isnot [string] -and $c.style) { $usedStyles[$c.style] = $true } + # Четвёртый владелец формата — примечание: его стиль тоже держит ссылку, + # иначе он вырезается как неиспользуемый и ссылка остаётся висячей. + if ($c -isnot [string] -and $c.note -and $c.note['style']) { $usedStyles[$c.note['style']] = $true } + } + } } } # Стиль бывает не только у ячейки и строки: колонка — третий владелец формата. Берём стили diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 6ae66365..e617b061 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.22 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.23 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -580,6 +580,35 @@ def main(): if v_node is not None: value = (v_node.get(f'{{{XSI_NS}}}type') or '', (v_node.text or '')) + # Примечание к ячейке. Якоря не читаем как данные: конец — координаты самой + # ячейки, начало — 1/1 у 1085 примечаний корпуса из 1087; остальное авторское. + note = None + note_node = find(c_content, "d:note") + if note_node is not None: + g = {etree.QName(ch).localname: ch for ch in note_node} + def _txt(name, default=''): + el = g.get(name) + return (el.text or default).strip() if el is not None else default + note_text = OrderedDict() + t_el = g.get('text') + if t_el is not None: + for it in findall(t_el, "v8:item"): + lang = text_of(find(it, "v8:lang")) or '' + note_text[lang] = text_of(find(it, "v8:content")) or '' + note = { + "FormatIdx": int(_txt('formatIndex', '0') or 0), + "Text": note_text, + "AutoSize": _txt('autoSize', 'true') == 'true', + "Box": OrderedDict([ + ("top", int(_txt('beginRowOffset', '0') or 0)), + ("left", int(_txt('beginColumnOffset', '0') or 0)), + ("bottom", int(_txt('endRowOffset', '0') or 0)), + ("right", int(_txt('endColumnOffset', '0') or 0)), + ]), + "AnchorRow": int(_txt('beginRow', '1') or 1), + "AnchorCol": int(_txt('beginColumn', '1') or 1), + } + # Настройки элемента управления — сериализованный base64 у самой ячейки. # Структуру не разбираем, возим дословно. control = None @@ -622,6 +651,7 @@ def main(): "Detail": detail, "Value": value, "Control": control, + "Note": note, "Text": text, "HasText": has_text, }) @@ -800,6 +830,15 @@ def main(): if key not in style_keys: style_keys[key] = fmt format_to_style_key[cell["FormatIdx"]] = key + # Формат примечания живёт в той же палитре и тоже заслуживает имени: иначе + # оформление подсказки терялось бы при обратной сборке. + if cell.get("Note"): + nfmt = get_format(cell["Note"]["FormatIdx"]) + if nfmt: + nkey = get_style_key(nfmt) + if nkey not in style_keys: + style_keys[nkey] = nfmt + format_to_style_key[cell["Note"]["FormatIdx"]] = nkey def row_style_fmt(fmt): """Оформление строки без её собственных свойств: скрытие уезжает инлайном к height, @@ -1069,7 +1108,8 @@ def main(): has_value = bool(cf and cf["Props"].get("containsValue") == "true") # Расшифровка сама по себе делает ячейку содержательной: в корпусе 12 653 ячейки # несут только её. Без этого такая ячейка уходила в заполнители и терялась. - has_content = cell["Param"] or cell["HasText"] or has_value or cell["Detail"] + has_content = (cell["Param"] or cell["HasText"] or has_value + or cell["Detail"] or cell["Note"]) has_merge = f"{global_row},{cell['Col']}" in merge_map if has_content or has_merge: @@ -1183,6 +1223,20 @@ def main(): if cell["Detail"]: dsl_cell["detail"] = cell["Detail"] + if cell["Note"]: + n = cell["Note"] + dsl_note = OrderedDict() + dsl_note["text"] = get_dsl_text(n["Text"]) + style_name = get_style_name(n["FormatIdx"]) + if style_name != "default": + dsl_note["style"] = style_name + if not n["AutoSize"]: + dsl_note["autoSize"] = False + dsl_note["box"] = n["Box"] + if n["AnchorRow"] != 1 or n["AnchorCol"] != 1: + dsl_note["anchor"] = OrderedDict([("row", n["AnchorRow"]), ("col", n["AnchorCol"])]) + dsl_cell["note"] = dsl_note + dsl_cells.append(dsl_cell) if len(dsl_cells) > 0: @@ -1344,6 +1398,10 @@ def main(): for c in cell_list: if isinstance(c, dict) and "style" in c: used_styles.add(c["style"]) + # Четвёртый владелец формата — примечание: его стиль тоже держит ссылку, + # иначе он вырезается как неиспользуемый и ссылка остаётся висячей. + if isinstance(c, dict) and isinstance(c.get("note"), dict) and "style" in c["note"]: + used_styles.add(c["note"]["style"]) # Стиль бывает не только у ячейки и строки: колонка — третий владелец формата. Берём # стили ИЗ САМИХ РАСКЛАДОК, а не из result: columnSets попадает в результат ПОЗЖЕ этой # проверки, поэтому стиль, на который ссылается только дополнительная раскладка, diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 index fc64e5f0..fae4a1f5 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.4 — Validate 1C spreadsheet +# mxl-validate v1.5 — Validate 1C spreadsheet # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Alias('Path')] @@ -286,6 +286,14 @@ foreach ($ri in $rowNodes) { Report-Error "Row ${rowIndex}: cell format index $val > format palette size ($formatCount)" } } + # Примечание — четвёртый владелец формата, и его ссылка тоже бывает битой. + $noteNode = $cell.SelectSingleNode("d:note", $nsMgr) + if ($noteNode) { + $nf = $noteNode.SelectSingleNode("d:formatIndex", $nsMgr) + if ($nf -and [int]$nf.InnerText -gt $formatCount) { + Report-Error "Row ${rowIndex}: note format index $($nf.InnerText) > format palette size ($formatCount)" + } + } } } diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.py b/.claude/skills/mxl-validate/scripts/mxl-validate.py index 392f6f46..a939b179 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.4 — Validate 1C spreadsheet document Template.xml +# mxl-validate v1.5 — 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 @@ -303,6 +303,13 @@ def main(): max_cell_format_ref = val if val > format_count: r.error(f'Row {row_index}: cell format index {val} > format palette size ({format_count})') + # Примечание — четвёртый владелец формата, и его ссылка тоже бывает битой. + note = cell.find(f'{{{NS_D}}}note') + if note is not None: + nf = note.find(f'{{{NS_D}}}formatIndex') + if nf is not None and nf.text and int(nf.text) > format_count: + r.error(f'Row {row_index}: note format index {nf.text}' + f' > format palette size ({format_count})') row_index += 1 diff --git a/docs/1c-spreadsheet-spec.md b/docs/1c-spreadsheet-spec.md index a4f270cb..be18d1aa 100644 --- a/docs/1c-spreadsheet-spec.md +++ b/docs/1c-spreadsheet-spec.md @@ -601,6 +601,43 @@ current-config не объявляет, поэтому вынести объяв обратные, ячейки продолжают ссылаться на записи со старыми ширинами. Из итогового XML это значение не выводится — воспроизводить его не нужно. +## Примечание к ячейке + +```xml + + Comment + 0 + 1 + ruтест + 1 -21 + 0 51 + 1 21 + 0 408 + true + Stretch + +``` + +Конструкция редкая (188 макетов, 1 087 примечаний), но структура жёсткая: все четырнадцать тегов +присутствуют у всех 1 087, порядок один и тот же, опциональных нет. Информации при этом меньше, +чем тегов: + +| Тег | Наблюдение | +|---|---| +| `drawingType`, `pictureSize`, `id` | константы: `Comment`, `Stretch`, `0` | +| `beginRow`, `beginColumn` | всегда `1`/`1` — 1085 и 1086 из 1087; три исключения в одном макете | +| `endRow`, `endColumn` | **координаты самой ячейки** — 1087 из 1087 | +| четыре смещения | авторские: сдвиг окошка меняет `begin*Offset`, растяжение — `end*Offset` | +| `autoSize` | `true` у 1036 из 1087 | + +`autoSize` описывает не наличие геометрии, а пересчёт размера: при `true` окошко всё равно несёт +координаты, и они осмысленны — 306 различных пар `(endRowOffset, endColumnOffset)` против +12 различных пар положения. + +Формат примечания — обычная запись палитры, на корпусе их всего 7 различных: 926 — стиль +подсказки (`verticalAlignment: Top` + `style:ToolTipTextColor` + `style:ToolTipBackColor`), +105 — он же с заливкой `#FFFAD9`. + ## Ресурсы картинок ```xml diff --git a/docs/mxl-dsl-spec.md b/docs/mxl-dsl-spec.md index dbf1d73b..69d4803b 100644 --- a/docs/mxl-dsl-spec.md +++ b/docs/mxl-dsl-spec.md @@ -188,6 +188,7 @@ | `controlType` | нет | `input` | Элемент управления поля ввода: `input` или `checkbox`. Только вместе с `valueType` | | `value` | нет | — | Значение в поле ввода. Только вместе с `valueType` | | `control` | нет | — | Настройки элемента управления в записи платформы (base64). Раундтрип, не для ручного авторинга | +| `note` | нет | — | Примечание к ячейке (см. ниже) | ### Содержимое ячейки @@ -270,6 +271,35 @@ включая `Boolean`. Флажок задаётся явно. Значение `"none"` (тега элемента управления нет вовсе) — форма раундтрипа, для ручного авторинга не нужна. +## Примечание к ячейке + +Всплывающая подсказка, которую платформа показывает при наведении. Задаётся ключом `note` — +строкой, объектом «язык → текст» или полной формой: + +```json +{ "col": 1, "text": "Итого", "note": "Сумма без НДС" } +{ "col": 2, "note": { "ru": "на дату документа", "en": "as of the document date" } } +{ "col": 3, "note": { "text": "не более 20%", "style": "жёлтая-подсказка" } } +{ "col": 4, "note": { "text": "…", "autoSize": false, + "box": { "top": 58, "left": -175, "bottom": 362, "right": 478 } } } +``` + +Объект трактуется по ключам — так же, как текст ячейки в короткой форме строки: есть ключ +примечания (`text`, `style`, `box`, `autoSize`, `anchor`) → это описание примечания, иначе ключи +считаются идентификаторами языков. + +| Поле | По умолч. | Описание | +|------|-----------|----------| +| `text` | — | Текст подсказки: строка или объект «язык → текст» | +| `style` | стиль подсказки | Имя стиля из `styles`; без него — оформление, которое даёт Конфигуратор | +| `autoSize` | `true` | Подгонять ли размер окошка под текст | +| `box` | канонический | Смещения окошка: `top`, `left` — положение, `bottom`, `right` — размер | +| `anchor` | `{ row: 1, col: 1 }` | Якорь начала окошка. Раундтрип, не для ручного авторинга | + +Координаты ячейки в примечании не задаются — платформа привязывает конец окошка к самой ячейке, +и компилятор проставляет это сам. `autoSize` и `box` независимы: при автоподгоне размера +положение окошка всё равно хранится. + ## `rowStyle` — оформление строки Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком. @@ -285,7 +315,7 @@ round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попа XML не возвращается. - объединения, не привязанные к ячейке (по всей высоте или ширине документа); -- рисунки и картинки, в том числе штрихкоды, и примечания к ячейкам; +- рисунки и картинки, в том числе штрихкоды; - группировки строк и колонок; - колонтитулы, параметры печати, область печати. diff --git a/tests/skills/cases/mxl-compile/cell-notes.json b/tests/skills/cases/mxl-compile/cell-notes.json new file mode 100644 index 00000000..73fb8703 --- /dev/null +++ b/tests/skills/cases/mxl-compile/cell-notes.json @@ -0,0 +1,23 @@ +{ + "name": "Примечания к ячейкам: короткая и полная формы", + "input": { + "columns": 4, + "textLanguages": ["ru"], + "styles": { "жёлтая-подсказка": { "verticalAlignment": "Top", "backColor": "#FFFAD9" } }, + "areas": [ + { + "name": "Примечания", + "rows": [ + { "cells": [ + { "col": 1, "text": "Итого", "note": "Сумма без НДС" }, + { "col": 2, "text": "Курс", "note": { "ru": "на дату документа", "en": "as of the document date" } }, + { "col": 3, "param": "Скидка", "note": { "text": "не более 20%", "style": "жёлтая-подсказка" } }, + { "col": 4, "note": { "text": "сдвинуто и растянуто", "autoSize": false, + "box": { "top": 58, "left": -175, "bottom": 362, "right": 478 } } } + ]} + ] + } + ] + }, + "params": { "outputPath": "Template.xml" } +} 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..c2af6107 --- /dev/null +++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/СПримечанием.xml @@ -0,0 +1,154 @@ + + + + ru + ru + + ru + Русский + Русский + + + en + Английский + Английский + + + + 3 + + + 0 + + + + 0 + + + ru + текст + + + + Comment + 0 + 1 + + + ru + тест + + + en + test + + + 1 + -19 + 0 + 50 + 1 + 19 + 0 + 82 + true + Stretch + + + + + + + 1 + + + + 0 + + + ru + Изменен размер + + + + Comment + 0 + 1 + + + ru + Изменен размер + + + 1 + -19 + 1 + 362 + 1 + 19 + 0 + 478 + false + Stretch + + + + + 2 + + 0 + + + + + + + 2 + + + + 0 + + + ru + Сдивнут + + + + Comment + 0 + 1 + + + ru + Сдвинут + + + 1 + 58 + 2 + 50 + 1 + -175 + 0 + 144 + true + Stretch + + + + + + true + 2 + 3 + 3 + + Top + style:ToolTipTextColor + style:ToolTipBackColor + + + 72 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/platform-notes.json b/tests/skills/cases/mxl-compile/platform-notes.json new file mode 100644 index 00000000..a3189211 --- /dev/null +++ b/tests/skills/cases/mxl-compile/platform-notes.json @@ -0,0 +1,25 @@ +{ + "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/cell-notes/Template.xml b/tests/skills/cases/mxl-compile/snapshots/cell-notes/Template.xml new file mode 100644 index 00000000..72f2d687 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/cell-notes/Template.xml @@ -0,0 +1,170 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 4 + + + 0 + + + + 0 + + + ru + Итого + + + + Comment + 0 + 1 + + + ru + Сумма без НДС + + + 1 + -21 + 0 + 51 + 1 + 21 + 0 + 408 + true + Stretch + + + + + + 0 + + + ru + Курс + + + + Comment + 0 + 1 + + + ru + на дату документа + + + en + as of the document date + + + 1 + -21 + 0 + 51 + 1 + 21 + 1 + 408 + true + Stretch + + + + + + 2 + Скидка + + Comment + 0 + 3 + + + ru + не более 20% + + + 1 + -21 + 0 + 51 + 1 + 21 + 2 + 408 + true + Stretch + + + + + + 0 + + Comment + 0 + 1 + + + ru + сдвинуто и растянуто + + + 1 + 58 + 0 + 362 + 1 + -175 + 3 + 478 + false + Stretch + + + + + + true + 4 + 1 + 1 + + Примечания + + Rows + 0 + 0 + -1 + -1 + + + + Top + style:ToolTipTextColor + style:ToolTipBackColor + + + Parameter + + + Top + #FFFAD9 + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/roundtrip-cell-notes.json b/tests/skills/cases/mxl-decompile/roundtrip-cell-notes.json new file mode 100644 index 00000000..68df8f14 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/roundtrip-cell-notes.json @@ -0,0 +1,36 @@ +{ + "name": "Roundtrip — примечания к ячейкам", + "preRun": [ + { + "script": "mxl-compile/scripts/mxl-compile", + "input": { + "columns": 4, + "textLanguages": ["ru"], + "styles": { "жёлтая-подсказка": { "verticalAlignment": "Top", "backColor": "#FFFAD9" } }, + "areas": [ + { + "name": "Примечания", + "rows": [ + { "cells": [ + { "col": 1, "text": "Итого", "note": "Сумма без НДС" }, + { "col": 2, "text": "Курс", "note": { "ru": "на дату", "en": "as of date" } }, + { "col": 3, "param": "Скидка", "note": { "text": "не более 20%", "style": "жёлтая-подсказка" } }, + { "col": 4, "note": { "text": "сдвинуто", "autoSize": false, + "box": { "top": 58, "left": -175, "bottom": 362, "right": 478 }, + "anchor": { "row": 4, "col": 1 } } } + ]} + ] + } + ] + }, + "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-cell-notes/Template.xml b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-cell-notes/Template.xml new file mode 100644 index 00000000..90510e0c --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-cell-notes/Template.xml @@ -0,0 +1,170 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 4 + + + 0 + + + + 0 + + + ru + Итого + + + + Comment + 0 + 1 + + + ru + Сумма без НДС + + + 1 + -21 + 0 + 51 + 1 + 21 + 0 + 408 + true + Stretch + + + + + + 0 + + + ru + Курс + + + + Comment + 0 + 1 + + + ru + на дату + + + en + as of date + + + 1 + -21 + 0 + 51 + 1 + 21 + 1 + 408 + true + Stretch + + + + + + 2 + Скидка + + Comment + 0 + 3 + + + ru + не более 20% + + + 1 + -21 + 0 + 51 + 1 + 21 + 2 + 408 + true + Stretch + + + + + + 0 + + Comment + 0 + 1 + + + ru + сдвинуто + + + 4 + 58 + 0 + 362 + 1 + -175 + 3 + 478 + false + Stretch + + + + + + true + 4 + 1 + 1 + + Примечания + + Rows + 0 + 0 + -1 + -1 + + + + Top + style:ToolTipTextColor + style:ToolTipBackColor + + + Parameter + + + Top + #FFFAD9 + + + 10 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-cell-notes/back.json b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-cell-notes/back.json new file mode 100644 index 00000000..1309efc9 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-cell-notes/back.json @@ -0,0 +1,19 @@ +{ + "columns": 4, + "defaultWidth": 10, + "fonts": {}, + "styles": { "vtop-bg": { "verticalAlignment": "Top", "backColor": "#FFFAD9" } }, + "areas": [ + { + "name": "Примечания", + "rows": [ + [ + { "text": "Итого", "note": { "text": "Сумма без НДС", "box": { "top": -21, "left": 21, "bottom": 51, "right": 408 } } }, + { "text": "Курс", "note": { "text": { "ru": "на дату", "en": "as of date" }, "box": { "top": -21, "left": 21, "bottom": 51, "right": 408 } } }, + { "param": "Скидка", "note": { "text": "не более 20%", "style": "vtop-bg", "box": { "top": -21, "left": 21, "bottom": 51, "right": 408 } } }, + { "note": { "text": "сдвинуто", "autoSize": false, "box": { "top": 58, "left": -175, "bottom": 362, "right": 478 }, "anchor": { "row": 4, "col": 1 } } } + ] + ] + } + ] +} \ No newline at end of file