From d7be4205d0e41de5305da026af2642370bfa686c Mon Sep 17 00:00:00 2001 From: zhanghuanxu Date: Mon, 29 Jun 2026 13:28:58 +0800 Subject: [PATCH] fix(slides): lint unsupported slide font families --- .../slides_xml_schema_definition.xml | 2 +- .../references/xml-schema-quick-ref.md | 2 +- .../scripts/xml_text_overlap_lint.py | 91 ++++++++++++++++++- .../scripts/xml_text_overlap_lint_test.py | 55 +++++++++++ 4 files changed, 145 insertions(+), 5 deletions(-) diff --git a/skills/lark-slides/references/slides_xml_schema_definition.xml b/skills/lark-slides/references/slides_xml_schema_definition.xml index 1b9aa438..be8649f2 100644 --- a/skills/lark-slides/references/slides_xml_schema_definition.xml +++ b/skills/lark-slides/references/slides_xml_schema_definition.xml @@ -150,7 +150,7 @@ - 字体族名称, 支持任意字体。 + 字体族名称, 支持下列字体。 常用中文字体: 思源宋体、寒蝉德黑体、标小智无界黑、寒蝉锦书宋、站酷小薇体、 diff --git a/skills/lark-slides/references/xml-schema-quick-ref.md b/skills/lark-slides/references/xml-schema-quick-ref.md index 6ac38d9d..ccf0da4a 100644 --- a/skills/lark-slides/references/xml-schema-quick-ref.md +++ b/skills/lark-slides/references/xml-schema-quick-ref.md @@ -74,7 +74,7 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出 | `textAlign` | 文本对齐方式 | | `lineSpacing` | 行间距,schema 默认 `multiple:1.5` | | `fontSize` | 字号 | -| `fontFamily` | 字体 | +| `fontFamily` | 字体,必须来自 `slides_xml_schema_definition.xml` 的 `FontFamilyType` 清单 | | `color` | 字体颜色 | | `bold` / `italic` / `underline` / `strikethrough` | 文本样式 | diff --git a/skills/lark-slides/scripts/xml_text_overlap_lint.py b/skills/lark-slides/scripts/xml_text_overlap_lint.py index d87ec87e..a678965d 100644 --- a/skills/lark-slides/scripts/xml_text_overlap_lint.py +++ b/skills/lark-slides/scripts/xml_text_overlap_lint.py @@ -17,6 +17,10 @@ class XmlTextOverlapLintError(Exception): pass +FONT_FAMILY_PLACEHOLDER_VALUES = {"undefined"} +_SUPPORTED_FONT_FAMILIES: set[str] | None = None + + def fail(message: str) -> None: raise XmlTextOverlapLintError(message) @@ -75,6 +79,81 @@ def xml_local_name(tag: str) -> str: return tag.rsplit("}", 1)[-1] if tag.startswith("{") else tag +def schema_definition_path() -> Path: + return Path(__file__).resolve().parents[1] / "references" / "slides_xml_schema_definition.xml" + + +def extract_supported_font_families(schema_xml: str) -> set[str]: + simple_type_match = re.search( + r'([\s\S]*?)', + schema_xml, + ) + if simple_type_match is None: + fail("FontFamilyType definition not found in slides XML schema") + + documentation_match = re.search( + r"([\s\S]*?)", + simple_type_match.group(1), + ) + if documentation_match is None: + fail("FontFamilyType documentation not found in slides XML schema") + + font_families: set[str] = set() + for raw_line in documentation_match.group(1).splitlines(): + line = raw_line.strip() + if not line or line.startswith("字体族名称") or line.endswith(":"): + continue + for font_family in re.split(r"[、,,]", line): + font_family = font_family.strip() + if font_family: + font_families.add(font_family) + + if not font_families: + fail("FontFamilyType supported font list is empty") + return font_families + + +def supported_font_families() -> set[str]: + global _SUPPORTED_FONT_FAMILIES + if _SUPPORTED_FONT_FAMILIES is None: + _SUPPORTED_FONT_FAMILIES = extract_supported_font_families(read_file(schema_definition_path())) + return _SUPPORTED_FONT_FAMILIES + + +def line_column_at_offset(source: str, offset: int) -> tuple[int, int]: + line = source.count("\n", 0, offset) + 1 + line_start = source.rfind("\n", 0, offset) + column = offset + 1 if line_start == -1 else offset - line_start + return line, column + + +def lint_font_families(xml: str) -> list[dict[str, Any]]: + issues: list[dict[str, Any]] = [] + allowed_font_families = supported_font_families() + for match in re.finditer(r"\bfontFamily\s*=\s*([\"'])(.*?)\1", xml): + font_family = match.group(2).strip() + if not font_family or font_family in FONT_FAMILY_PLACEHOLDER_VALUES: + continue + if font_family in allowed_font_families: + continue + line, column = line_column_at_offset(xml, match.start()) + issues.append( + { + "level": "error", + "code": "unsupported_font_family", + "message": f'fontFamily "{font_family}" is not supported', + "line": line, + "column": column, + "fontFamily": font_family, + "hint": ( + "Use a FontFamilyType value from slides_xml_schema_definition.xml, " + "or omit fontFamily to use the default font." + ), + } + ) + return issues + + def extract_error_context(xml: str, line: int | None, column: int | None, radius: int = 40) -> str | None: if line is None or column is None: return None @@ -326,18 +405,24 @@ def lint_xml(xml: str, source_path: str | None = None) -> dict[str, Any]: } presentation = parse_presentation(xml) + global_issues = lint_font_families(xml) slides = [ lint_slide(slide_xml, index + 1) for index, slide_xml in enumerate(presentation["slides"]) ] - error_count = sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "error") - warning_count = sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "warning") - return { + error_count = sum(1 for issue in global_issues if issue["level"] == "error") + error_count += sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "error") + warning_count = sum(1 for issue in global_issues if issue["level"] == "warning") + warning_count += sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "warning") + result = { "file": source_path, "slide_size": {"width": presentation["width"], "height": presentation["height"]}, "summary": {"slide_count": len(slides), "error_count": error_count, "warning_count": warning_count}, "slides": slides, } + if global_issues: + result["issues"] = global_issues + return result def print_usage() -> None: diff --git a/skills/lark-slides/scripts/xml_text_overlap_lint_test.py b/skills/lark-slides/scripts/xml_text_overlap_lint_test.py index 69114347..d3f39f72 100644 --- a/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +++ b/skills/lark-slides/scripts/xml_text_overlap_lint_test.py @@ -110,6 +110,61 @@ class XmlTextOverlapLintTest(unittest.TestCase): ) self.assertEqual(result["summary"]["error_count"], 0) + def test_lint_xml_accepts_supported_font_family(self) -> None: + result = xml_text_overlap_lint.lint_xml( + """ + + + + + + + + + +

Body text

+
+
+
+
+ """ + ) + self.assertEqual(result["summary"]["error_count"], 0) + self.assertNotIn("issues", result) + + def test_lint_xml_allows_legacy_undefined_font_family_placeholder(self) -> None: + result = xml_text_overlap_lint.lint_xml( + """ + + + +

Body text

+
+
+
+ """ + ) + self.assertEqual(result["summary"]["error_count"], 0) + self.assertNotIn("issues", result) + + def test_lint_xml_reports_unsupported_font_family(self) -> None: + result = xml_text_overlap_lint.lint_xml( + """ + + + +

Body text

+
+
+
+ """ + ) + issue = result["issues"][0] + self.assertEqual(result["summary"]["error_count"], 1) + self.assertEqual(issue["code"], "unsupported_font_family") + self.assertEqual(issue["fontFamily"], "微软雅黑") + self.assertIn("FontFamilyType", issue["hint"]) + def test_lint_xml_single_slide_uses_default_canvas_without_bounds_checks(self) -> None: result = xml_text_overlap_lint.lint_xml( """