From 98bcf79b418cf901e52c0539748825413a092cd8 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Wed, 12 Aug 2026 12:47:24 +0300 Subject: [PATCH] =?UTF-8?q?feat(mxl-compile,mxl-decompile):=20=D1=88=D1=80?= =?UTF-8?q?=D0=B8=D1=84=D1=82=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D1=81=D1=82=D0=B8=D0=BB=D1=8C=20=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D0=BD=D1=8B=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Шрифт бывает не собственным описанием, а ссылкой: на элемент стиля конфигурации (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) --- .../skills/mxl-compile/reference/styles.md | 12 ++ .../mxl-compile/scripts/mxl-compile.ps1 | 25 ++- .../skills/mxl-compile/scripts/mxl-compile.py | 21 ++- .../mxl-decompile/scripts/mxl-decompile.ps1 | 13 +- .../mxl-decompile/scripts/mxl-decompile.py | 17 ++- docs/mxl-dsl-styles.md | 12 ++ .../fixtures/platform-roundtrip/Шрифты.xml | 143 ++++++++++++++++++ .../cases/mxl-compile/platform-fonts.json | 25 +++ 8 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Шрифты.xml create mode 100644 tests/skills/cases/mxl-compile/platform-fonts.json diff --git a/.claude/skills/mxl-compile/reference/styles.md b/.claude/skills/mxl-compile/reference/styles.md index 93e24cca..8f7908ef 100644 --- a/.claude/skills/mxl-compile/reference/styles.md +++ b/.claude/skills/mxl-compile/reference/styles.md @@ -18,6 +18,18 @@ Шрифт `"default"` используется, когда стиль не указывает шрифт явно. Если не определён, создаётся автоматически (Arial 10). +Вместо собственного описания шрифт может быть **ссылкой** — на элемент стиля конфигурации +или на системный шрифт. Тогда у него единственное поле: + +```json +"fonts": { + "основной": { "ref": "style:TextFont" }, + "системный": { "ref": "sys:DefaultGUIFont" } +} +``` + +Это та же запись, что у шрифта в описании формы. + ## Стили (`styles.`) Ключ стиля — имя свойства так, как оно называется в выгрузке. Ниже частые; полный список diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 99b0ec6d..6ed48953 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.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" + } else { + X "`t" + } + continue + } X "`t" } diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index 6960b088..4c651d88 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.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' ') + else: + lines.append(f' ') + continue lines.append(f'\t') # 7j. Format palette diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index 93e27ff2..c736c20e 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.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 } diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 1bc80ee6..bd9d7e10 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.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"] diff --git a/docs/mxl-dsl-styles.md b/docs/mxl-dsl-styles.md index e423a486..58c74fb1 100644 --- a/docs/mxl-dsl-styles.md +++ b/docs/mxl-dsl-styles.md @@ -18,6 +18,18 @@ Шрифт `"default"` используется, когда стиль не указывает шрифт явно. Если не определён, создаётся автоматически (Arial 10). +Вместо собственного описания шрифт может быть **ссылкой** — на элемент стиля конфигурации +или на системный шрифт. Тогда у него единственное поле: + +```json +"fonts": { + "основной": { "ref": "style:TextFont" }, + "системный": { "ref": "sys:DefaultGUIFont" } +} +``` + +Это та же запись, что у шрифта в описании формы. + ## Стили (`styles.`) Ключ стиля — имя свойства так, как оно называется в выгрузке. Ниже частые; полный список 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..ea1b2749 --- /dev/null +++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Шрифты.xml @@ -0,0 +1,143 @@ + + + + ru + ru + + ru + Русский + Русский + + + en + Английский + Английский + + + + 1 + + + 0 + + + + 0 + + + ru + Умолчание + + + + + + + + 1 + + + + 1 + + + ru + Шрифт стиля + + + + + + + + 2 + + + + 2 + + + ru + системный шрифт + + + + + + + + 3 + + + + 3 + + + ru + Другой шрифт + + + + + + + + 4 + + + + 4 + + + ru + Полужирный + + + + + + + + 5 + + + + 5 + + + ru + размер + + + + + + + true + 6 + 6 + 6 + + + + + + + 0 + + + 1 + + + 2 + + + 3 + + + 4 + + + 72 + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/platform-fonts.json b/tests/skills/cases/mxl-compile/platform-fonts.json new file mode 100644 index 00000000..254514d9 --- /dev/null +++ b/tests/skills/cases/mxl-compile/platform-fonts.json @@ -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" + } + } +} \ No newline at end of file