feat(mxl-compile,mxl-decompile): шрифт ссылкой на стиль или системный

Шрифт бывает не собственным описанием, а ссылкой: на элемент стиля конфигурации
(style:) или на системный шрифт (sys:). Своих атрибутов у такого шрифта нет, и мы
превращали его в пустую запись — faceName="" height="0". ps1 при сборке подставлял
туда Arial 10, то есть подменял данные молча.

В корпусе таких шрифтов 272 в 213 макетах из 10 924: StyleItem 209, WindowsFont 63.

Запись — та же, что у шрифта в описании формы: { "ref": "style:TextFont" }.
kind выводится из префикса, ключом быть не обязан.

Синтетический стенд показал деталь, которую по корпусу было не разглядеть: префикс
sys в корне документа не объявлен, поэтому платформа дописывает объявление xmlns
прямо на узел шрифта — тот же приём, что с цветами из web-палитры.

Порты после этого сошлись ПОЛНОСТЬЮ: на пилоте из 40 макетов совпадают и JSON
декомпиляторов, и собранный XML. Макет со шрифтами добавлен в побайтовую
регрессию, стенд 8 из 13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-08-12 12:47:24 +03:00
co-authored by Claude Opus 5
parent 0a8671a166
commit 98bcf79b41
8 changed files with 262 additions and 6 deletions
@@ -18,6 +18,18 @@
Шрифт `"default"` используется, когда стиль не указывает шрифт явно. Если не определён,
создаётся автоматически (Arial 10).
Вместо собственного описания шрифт может быть **ссылкой** — на элемент стиля конфигурации
или на системный шрифт. Тогда у него единственное поле:
```json
"fonts": {
"основной": { "ref": "style:TextFont" },
"системный": { "ref": "sys:DefaultGUIFont" }
}
```
Это та же запись, что у шрифта в описании формы.
## Стили (`styles.<name>`)
Ключ стиля — имя свойства так, как оно называется в выгрузке. Ниже частые; полный список
@@ -1,4 +1,4 @@
# mxl-compile v1.39 — Compile 1C spreadsheet from JSON
# mxl-compile v1.40 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -232,8 +232,21 @@ function Format-Num {
return [System.Convert]::ToString($v, [System.Globalization.CultureInfo]::InvariantCulture)
}
# Шрифт бывает не собственным, а ссылкой: на элемент стиля конфигурации (style:) или на
# системный шрифт (sys:). Вид ссылки определяет kind, и он выводится из префикса.
function Get-FontRefKind {
param([string]$ref)
if ($ref -like 'sys:*') { return 'WindowsFont' }
return 'StyleItem'
}
function Add-Font {
param([string]$name, $fontDef)
if ($fontDef.ref) {
$script:fontMap[$name] = $script:fontEntries.Count
$script:fontEntries += @{ Ref = "$($fontDef.ref)"; Kind = (Get-FontRefKind "$($fontDef.ref)") }
return
}
$face = if ($fontDef.face) { $fontDef.face } else { "Arial" }
$size = if ($fontDef.size) { ConvertTo-FontSize $fontDef.size } else { 10 }
$bold = if ($fontDef.bold -eq $true) { "true" } else { "false" }
@@ -1618,6 +1631,16 @@ foreach ($ln in $script:lineRegistry) {
# 7i. Font palette
foreach ($fe in $fontEntries) {
if ($fe.Ref) {
# Префикс sys в корне документа не объявлен, поэтому платформа дописывает объявление
# прямо на узел — тот же приём, что с цветами из web-палитры.
if ($fe.Ref -like 'sys:*') {
X "`t<font xmlns:sys=`"http://v8.1c.ru/8.1/data/ui/fonts/system`" ref=`"$(Esc-Xml $fe.Ref)`" kind=`"$($fe.Kind)`"/>"
} else {
X "`t<font ref=`"$(Esc-Xml $fe.Ref)`" kind=`"$($fe.Kind)`"/>"
}
continue
}
X "`t<font faceName=`"$($fe.Face)`" height=`"$(Format-Num $fe.Size)`" bold=`"$($fe.Bold)`" italic=`"$($fe.Italic)`" underline=`"$($fe.Underline)`" strikeout=`"$($fe.Strikeout)`" kind=`"Absolute`" scale=`"100`"/>"
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.38 — Compile 1C spreadsheet from JSON
# mxl-compile v1.39 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import hashlib
@@ -480,7 +480,17 @@ def main():
font_map = {} # name -> 0-based index
font_entries = [] # list of dicts
def font_ref_kind(ref):
"""Шрифт бывает не собственным, а ссылкой: на элемент стиля конфигурации (style:)
или на системный шрифт (sys:). Вид ссылки определяет kind, он выводится из префикса."""
return 'WindowsFont' if ref.startswith('sys:') else 'StyleItem'
def add_font(name, font_def):
if font_def and font_def.get('ref'):
ref = str(font_def['ref'])
font_map[name] = len(font_entries)
font_entries.append({'Ref': ref, 'Kind': font_ref_kind(ref)})
return
face = font_def.get('face', 'Arial') if font_def else 'Arial'
size = to_font_size(font_def.get('size', 10)) if font_def else 10
bold = 'true' if font_def and font_def.get('bold') is True else 'false'
@@ -1564,6 +1574,15 @@ def main():
# 7i. Font palette
for fe in font_entries:
if fe.get('Ref'):
# Префикс sys в корне документа не объявлен, поэтому платформа дописывает
# объявление прямо на узел — тот же приём, что с цветами из web-палитры.
if fe['Ref'].startswith('sys:'):
lines.append(f' <font xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system"'
f' ref="{esc_xml(fe["Ref"])}" kind="{fe["Kind"]}"/>')
else:
lines.append(f' <font ref="{esc_xml(fe["Ref"])}" kind="{fe["Kind"]}"/>')
continue
lines.append(f'\t<font faceName="{fe["Face"]}" height="{fe["Size"]}" bold="{fe["Bold"]}" italic="{fe["Italic"]}" underline="{fe["Underline"]}" strikeout="{fe["Strikeout"]}" kind="Absolute" scale="100"/>')
# 7j. Format palette
@@ -1,4 +1,4 @@
# mxl-decompile v1.19 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.20 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -97,6 +97,9 @@ function ConvertTo-FontSize {
$rawFonts = @()
foreach ($fNode in $root.SelectNodes("d:font", $ns)) {
$rawFonts += @{
# Шрифт бывает ссылкой на элемент стиля (style:) или системный шрифт (sys:) —
# тогда своих атрибутов у него нет, и без этого он превращался в пустую запись.
Ref = $fNode.GetAttribute("ref")
Face = $fNode.GetAttribute("faceName")
Size = ConvertTo-FontSize $fNode.GetAttribute("height")
Bold = $fNode.GetAttribute("bold") -eq "true"
@@ -454,6 +457,7 @@ if ($rawFonts.Count -gt 0) {
function Get-FontKey {
param($f)
if ($f.Ref) { return "ref=$($f.Ref)" }
return "$($f.Face)|$($f.Size)|$($f.Bold)|$($f.Italic)|$($f.Underline)|$($f.Strikeout)"
}
@@ -473,7 +477,11 @@ for ($i = 1; $i -lt $rawFonts.Count; $i++) {
$name = $null
if ($f.Face -eq $df.Face -and $f.Size -eq $df.Size) {
if ($f.Ref) {
# Имя из ссылки: style:SmallTextFont → small-text-font-подобное читаемое имя не даёт
# ничего сверх самой ссылки, поэтому берём часть после префикса.
$name = ($f.Ref -replace '^[^:]+:', '').ToLowerInvariant()
} elseif ($f.Face -eq $df.Face -and $f.Size -eq $df.Size) {
if ($f.Bold -and -not $df.Bold -and -not $f.Italic -and -not $f.Underline -and -not $f.Strikeout) {
$name = "bold"
} elseif ($f.Italic -and -not $df.Italic -and -not $f.Bold) {
@@ -1115,6 +1123,7 @@ if ($colWidthMap.Count -gt 0) {
$fontsOut = [ordered]@{}
foreach ($name in $fontDefs.Keys) {
$f = $fontDefs[$name]
if ($f.Ref) { $fontsOut[$name] = [ordered]@{ ref = $f.Ref }; continue }
$fOut = [ordered]@{ face = $f.Face; size = $f.Size }
if ($f.Bold) { $fOut["bold"] = $true }
if ($f.Italic) { $fOut["italic"] = $true }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-decompile v1.19 — Decompile 1C spreadsheet to JSON
# mxl-decompile v1.20 — Decompile 1C spreadsheet to JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -267,6 +267,9 @@ def main():
raw_fonts = []
for f_node in findall(root, "d:font"):
raw_fonts.append({
# Шрифт бывает ссылкой на элемент стиля (style:) или системный шрифт (sys:) —
# тогда своих атрибутов у него нет, и без этого он превращался в пустую запись.
"Ref": f_node.get("ref", ""),
"Face": f_node.get("faceName", ""),
"Size": to_font_size(f_node.get("height", "0")),
"Bold": f_node.get("bold") == "true",
@@ -610,6 +613,10 @@ def main():
font_defs["default"] = raw_fonts[0]
def get_font_key(f):
# Ссылку различаем по ней самой: своих атрибутов у такого шрифта нет, и без этого
# две РАЗНЫЕ ссылки схлопывались в одну как «пустые».
if f.get("Ref"):
return "ref=" + f["Ref"]
return f"{f['Face']}|{f['Size']}|{f['Bold']}|{f['Italic']}|{f['Underline']}|{f['Strikeout']}"
font_key_map = {}
@@ -628,7 +635,10 @@ def main():
name = None
if f["Face"] == df["Face"] and f["Size"] == df["Size"]:
if f.get("Ref"):
# Имя из ссылки: читаемее самой ссылки ничего не придумать.
name = f["Ref"].split(":", 1)[1].lower()
elif f["Face"] == df["Face"] and f["Size"] == df["Size"]:
if f["Bold"] and not df["Bold"] and not f["Italic"] and not f["Underline"] and not f["Strikeout"]:
name = "bold"
elif f["Italic"] and not df["Italic"] and not f["Bold"]:
@@ -1117,6 +1127,9 @@ def main():
fonts_out = OrderedDict()
for name, f in font_defs.items():
if f.get("Ref"):
fonts_out[name] = OrderedDict([("ref", f["Ref"])])
continue
f_out = OrderedDict()
f_out["face"] = f["Face"]
f_out["size"] = f["Size"]
+12
View File
@@ -18,6 +18,18 @@
Шрифт `"default"` используется, когда стиль не указывает шрифт явно. Если не определён,
создаётся автоматически (Arial 10).
Вместо собственного описания шрифт может быть **ссылкой** — на элемент стиля конфигурации
или на системный шрифт. Тогда у него единственное поле:
```json
"fonts": {
"основной": { "ref": "style:TextFont" },
"системный": { "ref": "sys:DefaultGUIFont" }
}
```
Это та же запись, что у шрифта в описании формы.
## Стили (`styles.<name>`)
Ключ стиля — имя свойства так, как оно называется в выгрузке. Ниже частые; полный список
@@ -0,0 +1,143 @@
<?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>1</size>
</columns>
<rowsItem>
<index>0</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>
<rowsItem>
<index>1</index>
<row>
<c>
<c>
<f>1</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Шрифт стиля</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>2</index>
<row>
<c>
<c>
<f>2</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>системный шрифт</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>3</index>
<row>
<c>
<c>
<f>3</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Другой шрифт</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>4</index>
<row>
<c>
<c>
<f>4</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Полужирный</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<rowsItem>
<index>5</index>
<row>
<c>
<c>
<f>5</f>
<tl>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>размер</v8:content>
</v8:item>
</tl>
</c>
</c>
</row>
</rowsItem>
<templateMode>true</templateMode>
<defaultFormatIndex>6</defaultFormatIndex>
<height>6</height>
<vgRows>6</vgRows>
<font ref="style:SmallTextFont" kind="StyleItem"/>
<font xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" ref="sys:ANSIVariableFont" kind="WindowsFont"/>
<font faceName="Times New Roman" height="8" bold="false" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
<font faceName="Arial" height="8" bold="true" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
<font faceName="Arial" height="12" bold="false" italic="false" underline="false" strikeout="false" kind="Absolute" scale="100"/>
<format>
<font>0</font>
</format>
<format>
<font>1</font>
</format>
<format>
<font>2</font>
</format>
<format>
<font>3</font>
</format>
<format>
<font>4</font>
</format>
<format>
<width>72</width>
</format>
</document>
@@ -0,0 +1,25 @@
{
"name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: Шрифты",
"setup": "fixture:platform-roundtrip",
"inputFrom": "back.json",
"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"
}
}
}