fix(mxl-compile,mxl-decompile): пустой текст ячейки и расхождение портов на нём

Пустой текст платформа хранит только самозакрывающимся тегом: в корпусе ERP
таких 1 224 460, а из 780 934 непустых блоков нет ни одного, где пусты все
языки. Компилятор же на `text: ""` писал блок с пустым элементом языка —
запись, которой в выгрузках не существует. Теперь любая пустая форма
(строка, пустой объект, все языки пустыми) даёт один и тот же тег, а
декомпилятор возвращает её канонической пустой строкой.

Отсюда же росло расхождение портов. Свёртка текста на пустой карте языков
возвращала null, если ДРУГОГО текста в макете нет вовсе: набор языков
документа при этом пуст. Дальше py писал пропуск колонки, а ps1 наступал
на разворачивание одноэлементного массива — список из одного $null
превращался в $null, и строка целиком уходила в пустые.

Проверено на 12 корпусных макетах: 1411 пустых текстов из 1411 вернулись
на место, JSON портов совпадает.
This commit is contained in:
Nick Shirokov
2026-08-16 12:30:19 +03:00
parent fef55043cf
commit e048fcff70
9 changed files with 186 additions and 14 deletions
@@ -1,4 +1,4 @@
# mxl-compile v1.50 — Compile 1C spreadsheet from JSON # mxl-compile v1.51 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -1191,10 +1191,12 @@ function Emit-CellText {
} else { } else {
foreach ($l in $textLanguages) { $pairs += @{ Lang = $l; Text = "$value" } } foreach ($l in $textLanguages) { $pairs += @{ Lang = $l; Text = "$value" } }
} }
# Пустой объект — отдельное состояние: тег текста есть, языков в нём нет. Платформа # Пустой текст платформа хранит ТОЛЬКО самозакрывающимся тегом: в корпусе ERP таких
# пишет его самозакрывающимся. Для авторинга бесполезно (визуально это тот же пустой # 1 224 460, а из 780 934 непустых блоков ни одного со всеми пустыми языками нет.
# текст, что и ""), поэтому в описании DSL записи нет — она нужна раундтрипу. # Поэтому и пустая строка, и пустой объект дают одну и ту же запись.
if ($pairs.Count -eq 0) { $allEmpty = $true
foreach ($pr in $pairs) { if ("$($pr.Text)" -ne '') { $allEmpty = $false; break } }
if ($pairs.Count -eq 0 -or $allEmpty) {
X "`t`t`t`t`t<tl/>" X "`t`t`t`t`t<tl/>"
return return
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# mxl-compile v1.50 — Compile 1C spreadsheet from JSON # mxl-compile v1.51 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import hashlib import hashlib
@@ -1210,10 +1210,10 @@ def main():
pairs = [(str(k), str(v)) for k, v in value.items()] pairs = [(str(k), str(v)) for k, v in value.items()]
else: else:
pairs = [(lang, str(value)) for lang in text_languages] pairs = [(lang, str(value)) for lang in text_languages]
# Пустой объект — отдельное состояние: тег текста есть, языков в нём нет. Платформа # Пустой текст платформа хранит ТОЛЬКО самозакрывающимся тегом: в корпусе ERP таких
# пишет его самозакрывающимся. Для авторинга бесполезно (визуально это тот же пустой # 1 224 460, а из 780 934 непустых блоков ни одного со всеми пустыми языками нет.
# текст, что и ""), поэтому в описании DSL записи нет — она нужна раундтрипу. # Поэтому и пустая строка, и пустой объект дают одну и ту же запись.
if not pairs: if not pairs or all(content == '' for _, content in pairs):
lines.append('\t\t\t\t\t<tl/>') lines.append('\t\t\t\t\t<tl/>')
return return
lines.append('\t\t\t\t\t<tl>') lines.append('\t\t\t\t\t<tl>')
@@ -1,4 +1,4 @@
# mxl-decompile v1.29 — Decompile 1C spreadsheet to JSON # mxl-decompile v1.30 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -723,6 +723,10 @@ $textLanguages = @($docLangs.Keys)
function Get-DslText { function Get-DslText {
param($byLang) param($byLang)
if ($byLang -isnot [System.Collections.IDictionary]) { return $byLang } if ($byLang -isnot [System.Collections.IDictionary]) { return $byLang }
# Пустой текст (<tl/>) записывается в DSL пустой строкой — той же формой, что
# авторская. Без этой ветки макет, где ДРУГОГО текста нет вовсе, отдавал $null,
# и ячейка с пустым текстом теряла содержимое.
if ($byLang.Count -eq 0) { return '' }
if ($byLang.Count -ne $textLanguages.Count) { return $byLang } if ($byLang.Count -ne $textLanguages.Count) { return $byLang }
$common = $null $common = $null
foreach ($l in $textLanguages) { foreach ($l in $textLanguages) {
@@ -1219,8 +1223,10 @@ function ConvertTo-PositionalCells {
# Позиционная форма ценна компактностью: если она длиннее объектной, смысла в ней нет. # Позиционная форма ценна компактностью: если она длиннее объектной, смысла в ней нет.
$a = Try-InlineJson $out $a = Try-InlineJson $out
$b = Try-InlineJson $cells $b = Try-InlineJson $cells
if ($null -ne $a -and $null -ne $b -and $a.Length -gt $b.Length) { return $cells } if ($null -ne $a -and $null -ne $b -and $a.Length -gt $b.Length) { return ,$cells }
return $out # Запятая обязательна: без неё список из одной ячейки разворачивается в саму ячейку,
# а список из одного $null — в $null, и строка целиком уходила в пустые.
return ,$out
} }
# --- 12. Build areas --- # --- 12. Build areas ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# mxl-decompile v1.29 — Decompile 1C spreadsheet to JSON # mxl-decompile v1.30 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -849,6 +849,11 @@ def main():
def get_dsl_text(by_lang): def get_dsl_text(by_lang):
if not isinstance(by_lang, dict): if not isinstance(by_lang, dict):
return by_lang return by_lang
# Пустой текст (<tl/>) записывается в DSL пустой строкой — той же формой, что
# авторская. Без этой ветки макет, где ДРУГОГО текста нет вовсе, отдавал null,
# и ячейка с пустым текстом теряла содержимое.
if not by_lang:
return ''
if len(by_lang) != len(text_languages): if len(by_lang) != len(text_languages):
return by_lang return by_lang
common = None common = None
@@ -0,0 +1,16 @@
{
"name": "Пустой текст ячейки: строка, объект и все языки пустые",
"input": {
"columns": 2,
"textLanguages": ["ru", "en"],
"areas": [{
"rows": [
{ "cells": [{ "col": 1, "text": "" }] },
{ "cells": [{ "col": 1, "text": {} }] },
{ "cells": [{ "col": 1, "text": { "ru": "", "en": "" } }] },
{ "cells": [{ "col": 1, "text": { "ru": "А", "en": "" } }] }
]
}]
},
"params": { "outputPath": "Template.xml" }
}
@@ -0,0 +1,75 @@
<?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>2</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>0</f>
<tl/>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>1</index>
<row>
<c>
<c>
<f>0</f>
<tl/>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>2</index>
<row>
<c>
<c>
<f>0</f>
<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>А</v8:content>
</v8:item>
<v8:item>
<v8:lang>en</v8:lang>
<v8:content></v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>4</height>
<vgRows>4</vgRows>
<format>
<width>10</width>
</format>
</document>
@@ -0,0 +1,18 @@
{
"name": "Roundtrip — пустой текст ячейки возвращается как \"\"",
"preRun": [
{ "script": "mxl-compile/scripts/mxl-compile",
"input": {
"columns": 2,
"areas": [{ "rows": [
{ "cells": [{ "col": 1, "text": "" }] },
{ "cells": [{ "col": 1, "text": "Текст" }] }
]}]
},
"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,49 @@
<?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>2</size>
</columns>
<rowsItem>
<index>0</index>
<row>
<c>
<c>
<f>0</f>
<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>Текст</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<templateMode>true</templateMode>
<defaultFormatIndex>1</defaultFormatIndex>
<height>2</height>
<vgRows>2</vgRows>
<format>
<width>10</width>
</format>
</document>
@@ -0,0 +1 @@
{ "columns": 2, "defaultWidth": 10, "fonts": {}, "styles": {}, "areas": [{ "rows": [[""], ["Текст"]] }] }