feat(mxl-compile,mxl-decompile,mxl-validate): группы строк и колонок

Документные ключи rowGroups и columnGroups описывают сворачиваемые
группы: диапазон, имя, признак свёрнутости и расположение заголовка.

Форма плоская, как в формате: вложенность выражена вхождением одного
диапазона в другой. Дерево было бы читаемее, но платформа хранит плоско,
и для правки чужого макета дерево дороже — чтобы добавить группу внутрь
существующей, пришлось бы искать родителя. Частичных пересечений
платформа не порождает (корпус: 40 620 886 пар непересекающихся,
599 958 вложенных, ни одного частичного), поэтому компилятор их
отвергает.

Число уровней вложенности не задаётся: оно совпало с <vgLevels> у всех
1797 макетов корпуса. Порядок записи компилятор приводит к
платформенному — родитель раньше вложенных, по возрастанию начала
(совпало у 1798 макетов из 1798).

Имя группы вывести нельзя, хотя 143 455 имён корпуса выглядят как
автоматические R<номер строки>: 16 666 из них протухли после вставки
строк, а 10 287 групп имени не имеют вовсе. Расположение заголовка взяло
ключ titleLocation — то же свойство и то же имя, что в DSL форм.

mxl-validate: проверка 14 — перевёрнутый диапазон, частичное пересечение,
начало группы за пределами документа.

Стенд Группировки собирается байт в байт обоими портами и добавлен
в платформенные фикстуры.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-15 15:50:18 +03:00
co-authored by Claude Opus 5
parent 4005f36cce
commit 1e0b82d419
19 changed files with 1173 additions and 13 deletions
@@ -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)"
}
@@ -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"
}
@@ -0,0 +1,236 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<languageSettings>
<currentLanguage>ru</currentLanguage>
<defaultLanguage>ru</defaultLanguage>
<languageInfo>
<id>ru</id>
<code>Русский</code>
<description>Русский</description>
</languageInfo>
<languageInfo>
<id>en</id>
<code>Английский</code>
<description>Английский</description>
</languageInfo>
</languageSettings>
<columns>
<size>3</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>11</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>12</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>13</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>1</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>21</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>22</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>23</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>2</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>31</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>23</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>33</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>3</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>41</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>4</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>51</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>5</index>
<indexTo>7</indexTo>
<row>
<empty>true</empty>
</row>
</rowsItem>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>8</height>
<vgLevels>2</vgLevels>
<vgRows>8</vgRows>
<vg>
<b>1</b>
<e>2</e>
</vg>
<vg>
<b>2</b>
</vg>
<vg>
<b>3</b>
</vg>
<vg>
<b>4</b>
</vg>
<vg>
<b>5</b>
<t>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Произвеольное имя группы</v8:content>
</v8:item>
</t>
</vg>
<vg>
<b>6</b>
<t>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>ПИмяНачало</v8:content>
</v8:item>
</t>
<g>Begin</g>
</vg>
<vg>
<b>7</b>
<t>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>ПИмяКонец</v8:content>
</v8:item>
</t>
<g>End</g>
</vg>
<hg>
<b>1</b>
<e>2</e>
</hg>
<hg>
<b>2</b>
<t>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Горизонтальная</v8:content>
</v8:item>
</t>
</hg>
<format>
<width>72</width>
</format>
</document>
@@ -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"
}
@@ -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" }
}
@@ -0,0 +1,205 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<languageSettings>
<currentLanguage>ru</currentLanguage>
<defaultLanguage>ru</defaultLanguage>
<languageInfo>
<id>ru</id>
<code>Русский</code>
<description>Русский</description>
</languageInfo>
</languageSettings>
<columns>
<size>3</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>11</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>12</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>13</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>1</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>21</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>22</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>23</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>2</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>31</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>32</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>33</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>3</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>41</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>4</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>51</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>5</height>
<vgLevels>2</vgLevels>
<vgRows>5</vgRows>
<vg>
<b>1</b>
<e>3</e>
</vg>
<vg>
<b>2</b>
</vg>
<vg>
<b>4</b>
<t>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Итоговая</v8:content>
</v8:item>
</t>
<o>false</o>
<g>Begin</g>
</vg>
<hg>
<b>1</b>
<e>2</e>
<t>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Показатели</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content>Values</v8:content>
</v8:item>
</t>
</hg>
<hg>
<b>2</b>
<g>End</g>
</hg>
<format>
<width>10</width>
</format>
</document>
@@ -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:" }
}
@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://v8.1c.ru/8.2/data/spreadsheet" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<languageSettings>
<currentLanguage>ru</currentLanguage>
<defaultLanguage>ru</defaultLanguage>
<languageInfo>
<id>ru</id>
<code>Русский</code>
<description>Русский</description>
</languageInfo>
</languageSettings>
<columns>
<size>3</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>11</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>12</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>13</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>1</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>21</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>22</v8:content>
</v8:item>
</tl>
</c>
</c>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>23</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>2</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>31</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>3</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>41</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>4</index>
<row>
<c>
<c>
<f>0</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>51</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>5</height>
<vgLevels>2</vgLevels>
<vgRows>5</vgRows>
<vg>
<b>1</b>
<e>3</e>
</vg>
<vg>
<b>2</b>
</vg>
<vg>
<b>4</b>
<t>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Итоговая</v8:content>
</v8:item>
</t>
<o>false</o>
<g>Begin</g>
</vg>
<hg>
<b>1</b>
<e>2</e>
<t>
<v8:item>
<v8:lang>#</v8:lang>
<v8:content>R2</v8:content>
</v8:item>
</t>
</hg>
<hg>
<b>2</b>
<g>End</g>
</hg>
<format>
<width>10</width>
</format>
</document>
@@ -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"]] }] }