Files

4721 lines
203 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (c) 2026 Lark Technologies Pte. Ltd.
# SPDX-License-Identifier: MIT
from __future__ import annotations
import itertools
import json
import subprocess
import sys
import tempfile
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
from unittest import mock
import sxsd_validator
import xml_lint
class XmlTextOverlapLintGeometryTest(unittest.TestCase):
def assertNoXmlTextOverlapLintErrors(self, result: dict, sample_name: str) -> None:
issue_summaries = []
for slide in result.get("slides", []):
for issue in slide.get("issues", []):
issue_summaries.append(
f"slide {slide['slide_number']}: {issue['level']} {issue['code']} {issue['message']}"
)
if result.get("issues"):
for issue in result["issues"]:
issue_summaries.append(f"{issue['level']} {issue['code']} {issue['message']}")
self.assertEqual(
result["summary"]["error_count"],
0,
f"{sample_name} has XML text overlap lint errors:\n" + "\n".join(issue_summaries),
)
def test_cli_suggests_input_flag_for_positional_argument(self) -> None:
script_path = Path(xml_lint.__file__).resolve()
input_path = "/sandboxdata/workspace/file/full_presentation.xml"
completed = subprocess.run(
[sys.executable, str(script_path), input_path],
capture_output=True,
check=False,
text=True,
)
self.assertEqual(completed.returncode, 1)
self.assertEqual(completed.stdout, "")
self.assertEqual(
completed.stderr,
f"xml-lint error: unexpected argument: {input_path}, need --input\n",
)
def test_cli_preserves_requested_symlink_path_in_result(self) -> None:
script_path = Path(xml_lint.__file__).resolve()
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
resolved_path = temp_path / "resolved.xml"
requested_path = temp_path / "requested.xml"
resolved_path.write_text(
'<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">'
'<slide xmlns="https://www.larkoffice.com/sml/2.0"><data>'
'<shape type="text" topLeftX="10" topLeftY="10" width="100" height="30">'
'<content><p><span>Test</span></p></content>'
'</shape></data></slide>'
'</presentation>',
encoding="utf-8",
)
requested_path.symlink_to(resolved_path)
completed = subprocess.run(
[sys.executable, str(script_path), "--input", str(requested_path)],
capture_output=True,
check=False,
text=True,
)
self.assertEqual(completed.returncode, 0, completed.stderr)
result = json.loads(completed.stdout)
self.assertEqual(result["file"], str(requested_path))
def test_cli_reports_structured_slide_sxsd_error_outside_skill_directory(self) -> None:
script_path = Path(xml_lint.__file__).resolve()
with tempfile.TemporaryDirectory() as temp_dir:
input_path = Path(temp_dir) / "invalid-slide.xml"
input_path.write_text(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data><shape type="text" topLeftX="10" topLeftY="20" width="300"/></data>
</slide>
""",
encoding="utf-8",
)
completed = subprocess.run(
[sys.executable, str(script_path), "--input", str(input_path)],
cwd=temp_dir,
capture_output=True,
check=False,
text=True,
)
result = json.loads(completed.stdout)
issue = result["slides"][0]["errors"][0]
self.assertEqual(completed.returncode, 1)
self.assertEqual(completed.stderr, "")
self.assertEqual(issue["code"], "sxsd_missing_required_attr")
self.assertEqual(issue["path"], "slide/data/shape")
self.assertEqual(issue["attr"], "height")
self.assertEqual(issue["target"]["slide_number"], 1)
self.assertTrue(issue["hint"])
def test_xml_lint_accepts_inline_fixture_xml_samples(self) -> None:
samples = {
"image-led-cover": """
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<style><fill><fillColor color="rgb(15,23,42)"/></fill></style>
<data>
<img src="tok" topLeftX="560" topLeftY="0" width="400" height="540"/>
<shape type="text" topLeftX="64" topLeftY="150" width="420" height="70">
<content textType="title"><p><span fontSize="42">Quarterly Review</span></p></content>
</shape>
<shape type="text" topLeftX="64" topLeftY="235" width="420" height="36">
<content textType="sub-headline"><p><span fontSize="20">Focus, progress, and next steps</span></p></content>
</shape>
</data>
</slide>
</presentation>
""",
"content-grid": """
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="60" topLeftY="44" width="620" height="46">
<content textType="title"><p><span fontSize="30">Execution Snapshot</span></p></content>
</shape>
<shape type="rect" topLeftX="60" topLeftY="126" width="250" height="150"/>
<shape type="text" topLeftX="84" topLeftY="152" width="200" height="36">
<content textType="headline"><p><span fontSize="22">Plan</span></p></content>
</shape>
<shape type="rect" topLeftX="355" topLeftY="126" width="250" height="150"/>
<shape type="text" topLeftX="379" topLeftY="152" width="200" height="36">
<content textType="headline"><p><span fontSize="22">Build</span></p></content>
</shape>
<shape type="rect" topLeftX="650" topLeftY="126" width="250" height="150"/>
<shape type="text" topLeftX="674" topLeftY="152" width="200" height="36">
<content textType="headline"><p><span fontSize="22">Launch</span></p></content>
</shape>
</data>
</slide>
</presentation>
""",
}
self.assertTrue(samples)
for sample_name, sample_xml in samples.items():
with self.subTest(sample=sample_name):
result = xml_lint.lint_xml(
sample_xml,
sample_name,
)
self.assertNoXmlTextOverlapLintErrors(result, sample_name)
def test_lint_xml_reports_unescaped_ampersand_in_text(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Q&A</p></content>
</shape>
</data>
</slide>
"""
)
issue = result["issues"][0]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(issue["code"], "xml_not_well_formed")
self.assertIsInstance(issue["line"], int)
self.assertIsInstance(issue["column"], int)
self.assertIn("Q&A", issue["context"])
self.assertIn("&amp;", issue["hint"])
def test_lint_xml_reports_unescaped_ampersand_in_attribute(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p><a href="https://example.com/?a=1&b=2">link</a></p></content>
</shape>
</data>
</slide>
"""
)
issue = result["issues"][0]
self.assertEqual(issue["code"], "xml_not_well_formed")
self.assertIn("attribute", issue["hint"])
self.assertIn("a=1&amp;b=2", issue["hint"])
def test_lint_xml_reports_mismatched_xml_tag(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Broken XML</content>
</shape>
</data>
</slide>
"""
)
issue = result["issues"][0]
self.assertEqual(result["summary"]["slide_count"], 0)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(issue["code"], "xml_not_well_formed")
self.assertIsInstance(issue["line"], int)
self.assertIsInstance(issue["column"], int)
self.assertIn("Broken XML", issue["context"])
self.assertEqual(issue["related_objects"], [])
self.assertNotIn("Locate via related_objects[].xml_path.", issue["hint"])
def test_lint_xml_rejects_prefixed_sml_tags(self) -> None:
result = xml_lint.lint_xml(
"""
<ns0:slide xmlns:ns0="https://www.larkoffice.com/sml/2.0">
<ns0:data>
<sml:shape xmlns:sml="https://www.larkoffice.com/sml/2.0" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<sml:content textType="body"><sml:p>Prefixed SML</sml:p></sml:content>
</sml:shape>
</ns0:data>
</ns0:slide>
"""
)
issues = result["issues"]
self.assertEqual(result["summary"]["error_count"], 5)
self.assertEqual([issue["code"] for issue in issues], ["sml_prefixed_tag"] * 5)
self.assertEqual(issues[0]["tag"], "ns0:slide")
self.assertIn("default namespace", issues[0]["hint"])
self.assertEqual({issue["namespace"] for issue in issues}, {SML_NAMESPACE})
def test_lint_xml_reports_bound_namespace_for_prefixed_sml_tags(self) -> None:
for namespace in (
"http://www.larkoffice.com/sml/2.0",
"/sml/2.0",
SML_NAMESPACE,
):
with self.subTest(namespace=namespace):
result = xml_lint.lint_xml(
f"""
<sml:slide xmlns:sml="{namespace}">
<sml:data>
<sml:shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<sml:content textType="body"><sml:p>Prefixed SML</sml:p></sml:content>
</sml:shape>
</sml:data>
</sml:slide>
"""
)
issues = result["issues"]
self.assertEqual([issue["code"] for issue in issues], ["sml_prefixed_tag"] * 5)
self.assertEqual({issue["namespace"] for issue in issues}, {namespace})
def test_lint_xml_accepts_server_readback_slide_without_namespace(self) -> None:
result = xml_lint.lint_xml(
"""
<slide id="server-slide-id">
<data>
<shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Unprefixed SML</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_accepts_server_readback_presentation_short_namespace(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="/sml/2.0" width="960" height="540" id="server-presentation-id">
<slide id="server-slide-id">
<data>
<shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Server readback presentation</p></content>
</shape>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 1)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_accepts_server_readback_presentation_https_namespace(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540" id="server-presentation-id">
<slide id="server-slide-id">
<data>
<shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Server readback presentation</p></content>
</shape>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 1)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_accepts_legacy_http_namespace(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540" id="legacy-http-id">
<slide id="server-slide-id">
<data>
<shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Legacy HTTP namespace</p></content>
</shape>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 1)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_rejects_server_readback_presentation_wrong_namespace(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://example.com/not-sml" width="960" height="540">
<slide/>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["issues"][0]["code"], "sxsd_invalid_namespace")
def test_lint_xml_rejects_xml_declaration(self) -> None:
result = xml_lint.lint_xml(
'<?xml version="1.0"?><slide xmlns="https://www.larkoffice.com/sml/2.0"><data/></slide>'
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["issues"][0]["code"], "sxsd_unsupported_declaration")
def test_lint_xml_reports_missing_required_sxsd_attribute(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data><shape type="text" topLeftX="80" topLeftY="80" width="300"/></data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertNotIn("issues", result)
issue = result["slides"][0]["issues"][0]
self.assertEqual(issue["code"], "sxsd_missing_required_attr")
self.assertEqual(issue["attr"], "height")
def test_lint_xml_rejects_child_order_that_violates_xsd_sequence(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide/>
<title>Late title</title>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["issues"][0]["code"], "sxsd_invalid_child_order")
def test_lint_xml_accepts_escaped_entities_without_suspicious_entity_warning(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Q&amp;A</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertNotIn("issues", result)
def test_lint_xml_accepts_chinese_full_width_punctuation(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="620" height="90">
<content textType="body"><p>承诺:按期交付;持续复盘|风险透明</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_single_slide_reports_out_of_canvas_and_blank_slide_errors(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="1000" topLeftY="500" width="120" height="80">
<content textType="body"><p>Body text outside the canvas</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["slide_size"], {"width": 960, "height": 540})
self.assertEqual(result["summary"]["slide_count"], 1)
self.assertEqual(result["summary"]["error_count"], 2)
self.assertEqual(
[issue["code"] for issue in result["slides"][0]["errors"]],
["shape_out_of_canvas", "blank_slide"],
)
def test_lint_xml_preserves_presentation_canvas_and_slide_order(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="1280" height="720">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="first" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>First slide</p></content>
</shape>
</data>
</slide>
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<img id="second" src="tok" topLeftX="100" topLeftY="120" width="240" height="160"/>
<shape id="second-shape" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Second slide</p></content>
</shape>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["slide_size"], {"width": 1280, "height": 720})
self.assertEqual(result["summary"]["slide_count"], 2)
self.assertEqual([slide["slide_number"] for slide in result["slides"]], [1, 2])
self.assertEqual([slide["element_count"] for slide in result["slides"]], [1, 2])
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(result["summary"]["warning_count"], 0)
def test_lint_xml_handles_self_closing_slide_before_normal_slide(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide/>
<slide>
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Second slide</p></content>
</shape>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 2)
self.assertEqual(
[issue["code"] for issue in result["slides"][0]["errors"]],
["blank_slide"],
)
self.assertEqual(result["slides"][1]["status"], "passed")
def test_lint_xml_keeps_trailing_self_closing_slide(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide>
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>First slide</p></content>
</shape>
</data>
</slide>
<slide/>
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 2)
self.assertEqual(
[issue["code"] for issue in result["slides"][1]["errors"]],
["blank_slide"],
)
def test_lint_xml_ignores_slide_markup_inside_xml_comments(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide>
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="body"><p>Real slide</p></content>
</shape>
</data>
</slide>
<!-- Old draft: <slide id="ghost"/> -->
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 1)
self.assertEqual(result["slides"][0]["status"], "passed")
def test_lint_xml_ignores_invalid_slide_markup_inside_xml_comments(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide><data/></slide>
<!-- Old draft: <slide bogus="x"/> -->
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 1)
self.assertEqual(
[issue["code"] for issue in result["slides"][0]["errors"]],
["blank_slide"],
)
def test_lint_xml_skips_only_invalid_slide_and_continues_geometry_checks(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide>
<data>
<shape id="invalid" type="text" topLeftX="1000" topLeftY="80" width="300">
<content textType="body"><p>Missing height</p></content>
</shape>
</data>
</slide>
<slide>
<data>
<shape id="outside" type="text" topLeftX="1000" topLeftY="80" width="120" height="60">
<content textType="body"><p>Outside canvas</p></content>
</shape>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 2)
self.assertEqual(result["slides"][0]["element_count"], 0)
self.assertEqual(
[issue["code"] for issue in result["slides"][0]["errors"]],
["sxsd_missing_required_attr"],
)
self.assertNotIn(
"shape_out_of_canvas",
[issue["code"] for issue in result["slides"][0]["issues"]],
)
self.assertIn(
"shape_out_of_canvas",
[issue["code"] for issue in result["slides"][1]["errors"]],
)
def test_lint_xml_scopes_invalid_slide_root_attribute_to_that_slide(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide bogusAttr="oops">
<data><shape type="rect" topLeftX="80" topLeftY="80" width="100" height="100"/></data>
</slide>
<slide>
<data><shape type="rect" topLeftX="1000" topLeftY="80" width="100" height="100"/></data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 2)
self.assertEqual(
[issue["code"] for issue in result["slides"][0]["errors"]],
["sxsd_unsupported_attr"],
)
self.assertIn(
"shape_out_of_canvas",
[issue["code"] for issue in result["slides"][1]["errors"]],
)
def test_lint_xml_allows_svg_subtree_inside_embed(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<data>
<embed topLeftX="80" topLeftY="120" width="240" height="140">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 140">
<rect x="10" y="10" width="220" height="120" rx="12" fill="#EFF6FF"/>
<circle cx="70" cy="70" r="34" fill="#2563EB"/>
<text x="130" y="76" font-size="18" fill="#1E3A8A">SVG OK</text>
<foreignObject x="0" y="0" width="1" height="1">
<embed xmlns="http://www.w3.org/1999/xhtml">
<rect xmlns="http://www.w3.org/2000/svg" width="1" height="1"/>
</embed>
</foreignObject>
</svg>
</embed>
</data>
</slide>
"""
)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertNotIn("sxsd_unsupported_tag", codes)
self.assertNotIn("sxsd_unsupported_attr", codes)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_enforces_embed_svg_contract(self) -> None:
cases = [
("missing SVG", "", "sxsd_missing_required_child"),
("wrong namespace", '<svg viewBox="0 0 20 20"/>', "sxsd_unsupported_tag"),
(
"multiple SVG roots",
'<svg xmlns="http://www.w3.org/2000/svg"/><svg xmlns="http://www.w3.org/2000/svg"/>',
"sxsd_too_many_children",
),
(
"non-SVG root element",
'<rect xmlns="http://www.w3.org/2000/svg" width="20" height="20"/>',
"sxsd_unexpected_child",
),
(
"SVG after reflection",
'<reflection/><svg xmlns="http://www.w3.org/2000/svg"/>',
"sxsd_invalid_child_order",
),
]
for name, children, expected_code in cases:
with self.subTest(name=name):
result = xml_lint.lint_xml(
f"""
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<data>
<embed topLeftX="80" topLeftY="120" width="240" height="140">
{children}
</embed>
</data>
</slide>
"""
)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertIn(expected_code, codes)
def test_lint_xml_enforces_embed_svg_root_for_readback_namespaces(self) -> None:
document_templates = [
("bare slide", "<slide>{content}</slide>"),
(
"short namespace",
'<presentation xmlns="/sml/2.0" width="960" height="540">'
"<slide>{content}</slide>"
"</presentation>",
),
(
"HTTPS namespace",
'<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">'
"<slide>{content}</slide>"
"</presentation>",
),
]
embed_template = """
<data>
<embed topLeftX="80" topLeftY="120" width="240" height="140">
{svg_root}
</embed>
</data>
"""
roots = [
("valid SVG root", '<svg xmlns="http://www.w3.org/2000/svg"/>', False),
(
"non-SVG root element",
'<rect xmlns="http://www.w3.org/2000/svg" width="20" height="20"/>',
True,
),
]
for document_name, document_template in document_templates:
for root_name, svg_root, should_error in roots:
with self.subTest(document=document_name, root=root_name):
content = embed_template.format(svg_root=svg_root)
result = xml_lint.lint_xml(
document_template.format(content=content)
)
codes = [
issue["code"]
for slide in result["slides"]
for issue in slide["issues"]
]
if should_error:
self.assertIn("sxsd_unexpected_child", codes)
else:
self.assertNotIn("sxsd_unexpected_child", codes)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_reports_sxsd_unsupported_tag_with_alias_hint(self) -> None:
cases = [
("textbox", '<textbox topLeftX="80" topLeftY="80" width="300" height="60">Text</textbox>', '<shape type="text">'),
("image", '<image src="tok" topLeftX="80" topLeftY="80" width="300" height="180"/>', "<img>"),
]
for tag_name, element_xml, expected_hint in cases:
with self.subTest(tag=tag_name):
result = xml_lint.lint_xml(
f"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>{element_xml}</data>
</slide>
"""
)
slide_issues = result["slides"][0]["issues"]
issue = next(
issue for issue in slide_issues if issue["code"] == "sxsd_unsupported_tag"
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(
[reported["code"] for reported in slide_issues],
["sxsd_unsupported_tag"],
)
self.assertEqual(issue["code"], "sxsd_unsupported_tag")
self.assertEqual(issue["tag"], tag_name)
self.assertIn(expected_hint, issue["hint"])
def test_lint_xml_reports_sxsd_unsupported_attr_with_alias_hint(self) -> None:
cases = [
("shape", "x", "topLeftX", '<shape type="text" x="80" topLeftY="80" width="300" height="60"><content><p>Text</p></content></shape>'),
("shape", "heigth", "height", '<shape type="text" topLeftX="80" topLeftY="80" width="300" heigth="60"><content><p>Text</p></content></shape>'),
("content", "fontColor", "color", '<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60"><content fontColor="rgba(0, 0, 0, 1)"><p>Text</p></content></shape>'),
]
for tag_name, attr_name, expected_attr, element_xml in cases:
with self.subTest(attr=attr_name):
result = xml_lint.lint_xml(
f"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>{element_xml}</data>
</slide>
"""
)
slide_issues = result["slides"][0]["issues"]
issue = next(
issue for issue in slide_issues if issue["code"] == "sxsd_unsupported_attr"
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(
[reported["code"] for reported in slide_issues],
["sxsd_unsupported_attr"],
)
self.assertEqual(issue["code"], "sxsd_unsupported_attr")
self.assertEqual(issue["tag"], tag_name)
self.assertEqual(issue["attr"], attr_name)
self.assertIn(expected_attr, issue["hint"])
def test_lint_xml_keeps_unrelated_unsupported_and_missing_attrs(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" bogus="x" topLeftX="80" topLeftY="80" width="300"/>
</data>
</slide>
"""
)
self.assertEqual(
[issue["code"] for issue in result["slides"][0]["issues"]],
["sxsd_unsupported_attr", "sxsd_missing_required_attr"],
)
def test_lint_xml_keeps_missing_attrs_when_suggestion_is_ambiguous(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeft="80" width="300" height="60"/>
</data>
</slide>
"""
)
self.assertEqual(
[issue["code"] for issue in result["slides"][0]["issues"]],
[
"sxsd_unsupported_attr",
"sxsd_missing_required_attr",
"sxsd_missing_required_attr",
],
)
def test_lint_xml_suppresses_only_missing_attr_that_resolves_ambiguous_suggestion(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftXX="80" topLeftY="80" width="300" height="60"/>
</data>
</slide>
"""
)
self.assertEqual(
[
(issue["code"], issue.get("attr"))
for issue in result["slides"][0]["issues"]
],
[("sxsd_unsupported_attr", "topLeftXX")],
)
def test_lint_xml_ignores_server_filled_id_attrs(self) -> None:
result = xml_lint.lint_xml(
"""
<slide id="server-slide-id" xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="server-shape-id" type="rect" topLeftX="80" topLeftY="80" width="300" height="160">
<fill id="server-fill-id" unexpected="value">
<fillColor color="rgba(255, 255, 255, 1)"/>
</fill>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(
[(issue["tag"], issue["attr"]) for issue in result["slides"][0]["issues"]],
[("fill", "unexpected")],
)
def test_lint_xml_ignores_chart_roundtrip_attrs(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<chart updated="true" topLeftX="80" topLeftY="80" width="300" height="160">
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
<chartData isStaticData="true">
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
</chartData>
</chart>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_ignores_chart_parsed_values_roundtrip_tags(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<chart topLeftX="80" topLeftY="80" width="300" height="160">
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
<chartData isStaticData="true">
<dim1>
<chartField name="category" valueType="string">
A<chartParsedValues>A</chartParsedValues>
</chartField>
</dim1>
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
</chartData>
</chart>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_ignores_chart_parsed_values_roundtrip_tag(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<chart topLeftX="80" topLeftY="80" width="300" height="160">
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
<chartData>
<dim1>
<chartField name="category" valueType="string">
Africa<chartParsedValues>Africa</chartParsedValues>
</chartField>
</dim1>
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
</chartData>
</chart>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertNotIn("issues", result)
def test_lint_xml_rejects_chart_parsed_values_outside_chart_field(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="rect" topLeftX="80" topLeftY="80" width="300" height="160">
<chartParsedValues>unexpected</chartParsedValues>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(
[issue["code"] for issue in result["slides"][0]["issues"]],
["sxsd_unsupported_tag"],
)
self.assertEqual(
result["slides"][0]["issues"][0]["path"],
"slide/data/shape/chartParsedValues",
)
def test_lint_xml_limits_chart_roundtrip_attrs_to_matching_tags(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<chart isStaticData="true" topLeftX="80" topLeftY="80" width="300" height="160">
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
<chartData updated="true">
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
</chartData>
</chart>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 2)
slide_issues = result["slides"][0]["issues"]
self.assertEqual(
{(issue["tag"], issue["attr"]) for issue in slide_issues},
{("chart", "isStaticData"), ("chartData", "updated")},
)
self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in slide_issues))
def test_lint_xml_reports_gradient_shorthand_attrs_on_fill_color(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="rect" topLeftX="80" topLeftY="80" width="300" height="160">
<fill>
<fillColor
type="gradient"
color1="rgba(255, 0, 0, 1)"
color2="rgba(0, 0, 255, 1)"
angle="45"
stop1="0%"
stop2="100%"/>
</fill>
</shape>
</data>
</slide>
"""
)
slide_issues = result["slides"][0]["issues"]
unsupported_attrs = {issue["attr"] for issue in slide_issues}
self.assertEqual(result["summary"]["error_count"], 6)
self.assertEqual(
unsupported_attrs,
{"type", "color1", "color2", "angle", "stop1", "stop2"},
)
self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in slide_issues))
self.assertTrue(all(issue["tag"] == "fillColor" for issue in slide_issues))
def test_lint_xml_accepts_chart_field_simple_content_attrs(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<chart topLeftX="80" topLeftY="80" width="520" height="320">
<chartPlotArea>
<chartPlot type="line"/>
</chartPlotArea>
<chartData>
<dim1>
<chartField name="month" valueType="string">Jan, Feb</chartField>
</dim1>
<dim2>
<chartField name="value" valueType="number">1, 2</chartField>
</dim2>
</chartData>
</chart>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertNotIn("issues", result)
def test_lint_xml_does_not_load_iconpark_index_without_icons(self) -> None:
original_loader = xml_lint.load_iconpark_icon_types
def fail_if_loaded() -> set[str]:
raise AssertionError("iconpark index should not be loaded without <icon iconType>")
xml_lint.load_iconpark_icon_types = fail_if_loaded
try:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content><p>No icons here</p></content>
</shape>
</data>
</slide>
"""
)
finally:
xml_lint.load_iconpark_icon_types = original_loader
self.assertEqual(result["summary"]["error_count"], 0)
self.assertNotIn("issues", result)
def test_lint_xml_accepts_iconpark_icon_type_from_index(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48">
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
</icon>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertNotIn("issues", result)
def test_lint_xml_reports_icon_missing_fill_color(self) -> None:
cases = [
'<icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48"/>',
'<icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48"><fill/></icon>',
(
'<icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48">'
"<fill><fillColor/></fill></icon>"
),
]
for icon_xml in cases:
with self.subTest(icon=icon_xml):
result = xml_lint.lint_xml(
f"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>{icon_xml}</data>
</slide>
"""
)
issue = result["issues"][0]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(issue["code"], "icon_missing_fill_color")
self.assertEqual(issue["tag"], "icon")
def test_lint_xml_reports_icon_transparent_fill_color(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<icon iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="80" width="48" height="48">
<fill><fillColor color="rgba(37, 99, 235, 0)"/></fill>
</icon>
</data>
</slide>
"""
)
issue = result["issues"][0]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(issue["code"], "icon_transparent_fill_color")
self.assertEqual(issue["tag"], "icon")
self.assertEqual(issue["attr"], "fillColor")
def test_lint_xml_reports_iconpark_icon_type_outside_index(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<icon iconType="iconpark/Base/settng.svg" topLeftX="80" topLeftY="80" width="48" height="48">
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
</icon>
</data>
</slide>
"""
)
issue = result["issues"][0]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(issue["code"], "iconpark_unsupported_icon_type")
self.assertEqual(issue["tag"], "icon")
self.assertEqual(issue["attr"], "iconType")
self.assertEqual(issue["iconType"], "iconpark/Base/settng.svg")
self.assertIn("iconpark-index.json", issue["hint"])
self.assertIn("iconpark/Base/setting.svg", issue["hint"])
def test_lint_xml_skips_iconpark_validation_inside_embedded_svg(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<data>
<embed topLeftX="80" topLeftY="120" width="240" height="140">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 140">
<rect x="10" y="10" width="220" height="120" fill="#EFF6FF"/>
<icon iconType="not-an-iconpark-name"/>
<foreignObject x="0" y="0" width="60" height="60">
<icon xmlns="http://www.w3.org/1999/xhtml" iconType="not-an-iconpark-name"/>
</foreignObject>
</svg>
</embed>
</data>
</slide>
"""
)
codes = [issue["code"] for issue in result["document"]["errors"]]
self.assertNotIn("iconpark_unsupported_icon_type", codes)
self.assertNotIn("icon_missing_fill_color", codes)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_detects_overlapping_text_boxes(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content textType="title"><p>Title</p></content>
</shape>
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="80">
<content textType="body"><p>Body</p></content>
</shape>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["summary"]["warning_count"], 0)
self.assertEqual(result["slides"][0]["issues"][0]["code"], "bbox_overlap")
def test_lint_xml_detects_current_itinerary_cjk_caption_occlusion(self) -> None:
result = xml_lint.lint_xml(
"""
<slide id="pQO" xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape width="190" height="80" topLeftX="580" topLeftY="170" presetHandlers="0" type="rect" id="blI">
<fill><fillColor color="rgba(255, 255, 255, 0.9)"/></fill>
<border color="rgba(220, 205, 185, 1)" width="1"/>
<content fontSize="16" fontFamily="思源黑体" color="rgba(31, 35, 41, 1)"/>
</shape>
<shape width="160" height="25" topLeftX="595" topLeftY="180" type="text" id="blX">
<content fontSize="14" fontFamily="思源黑体" color="rgba(120, 80, 40, 1)" bold="true"><p>日照金山</p></content>
</shape>
<shape width="160" height="40" topLeftX="595" topLeftY="205" type="text" id="blY">
<content textType="caption" fontSize="11" fontFamily="思源黑体" color="rgba(130, 100, 70, 1)"><p>清晨躺在床上看玉龙雪山日照金山奇观</p></content>
</shape>
<shape width="180" height="80" topLeftX="730" topLeftY="170" presetHandlers="0" type="rect" id="blH">
<fill><fillColor color="rgba(255, 255, 255, 0.9)"/></fill>
<border color="rgba(220, 205, 185, 1)" width="1"/>
<content fontSize="16" fontFamily="思源黑体" color="rgba(31, 35, 41, 1)"/>
</shape>
<shape width="150" height="25" topLeftX="745" topLeftY="180" type="text" id="blp">
<content fontSize="14" fontFamily="思源黑体" color="rgba(120, 80, 40, 1)" bold="true"><p>午餐返程</p></content>
</shape>
<shape width="150" height="40" topLeftX="745" topLeftY="205" type="text" id="blV">
<content textType="caption" fontSize="11" fontFamily="思源黑体" color="rgba(130, 100, 70, 1)"><p>享用特色午餐,带着美好回忆返程</p></content>
</shape>
<shape width="190" height="80" topLeftX="580" topLeftY="310" presetHandlers="0" type="rect" id="blP">
<fill><fillColor color="rgba(255, 255, 255, 0.9)"/></fill>
<border color="rgba(220, 205, 185, 1)" width="1"/>
<content fontSize="16" fontFamily="思源黑体" color="rgba(31, 35, 41, 1)"/>
</shape>
<shape width="160" height="25" topLeftX="595" topLeftY="320" type="text" id="blG">
<content fontSize="14" fontFamily="思源黑体" color="rgba(120, 80, 40, 1)" bold="true"><p>高路徒步</p></content>
</shape>
<shape width="160" height="40" topLeftX="595" topLeftY="345" type="text" id="blQ">
<content textType="caption" fontSize="11" fontFamily="思源黑体" color="rgba(130, 100, 70, 1)"><p>经典高路徒步,28道拐,龙洞瀑布,中虎跳峡</p></content>
</shape>
<shape width="180" height="80" topLeftX="730" topLeftY="310" presetHandlers="0" type="rect" id="blw">
<fill><fillColor color="rgba(255, 255, 255, 0.9)"/></fill>
<border color="rgba(220, 205, 185, 1)" width="1"/>
<content fontSize="16" fontFamily="思源黑体" color="rgba(31, 35, 41, 1)"/>
</shape>
<shape width="150" height="25" topLeftX="745" topLeftY="320" type="text" id="blZ">
<content fontSize="14" fontFamily="思源黑体" color="rgba(120, 80, 40, 1)" bold="true"><p>伴手礼</p></content>
</shape>
<shape width="150" height="40" topLeftX="745" topLeftY="345" type="text" id="blS">
<content textType="caption" fontSize="11" fontFamily="思源黑体" color="rgba(130, 100, 70, 1)"><p>酒店精心准备的归途伴手礼,留下难忘纪念</p></content>
</shape>
</data>
</slide>
"""
)
overlap_pairs = {tuple(issue["elements"]) for issue in result["slides"][0]["issues"]}
self.assertEqual(result["summary"]["error_count"], 2)
self.assertIn(("blY", "blV"), overlap_pairs)
self.assertIn(("blQ", "blS"), overlap_pairs)
def test_lint_xml_detects_horizontal_text_overflow_across_declared_box_gap(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="source" type="text" topLeftX="80" topLeftY="100" width="160" height="40">
<content fontSize="18" wrap="false"><p>这是一个足够长的中文文本用于检测跨越间隙的横向溢出</p></content>
</shape>
<shape id="target" type="text" topLeftX="260" topLeftY="100" width="160" height="40">
<content fontSize="18"><p>目标</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["summary"]["warning_count"], 0)
issue = result["slides"][0]["issues"][0]
self.assertEqual(issue["code"], "bbox_overlap")
self.assertEqual(issue["elements"], ["source", "target"])
self.assertGreater(issue["measurement"]["intersection_area"], 0)
self.assertIsNotNone(issue.get("hint"))
def test_lint_xml_allows_horizontal_text_with_default_wrap(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="source" type="text" topLeftX="80" topLeftY="100" width="160" height="40">
<content fontSize="18"><p>这是一个足够长的中文文本用于检测默认自动换行</p></content>
</shape>
<shape id="target" type="text" topLeftX="260" topLeftY="100" width="160" height="40">
<content fontSize="18"><p>目标</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["summary"]["warning_count"], 0)
self.assertEqual(result["slides"][0]["issues"][0]["code"], "text_may_overflow_shape")
self.assertEqual(result["slides"][0]["issues"][0]["level"], "error")
self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["source"])
def test_lint_xml_reports_text_out_of_canvas_and_warns_for_text_height(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="180" height="20">
<content textType="body" fontSize="18"><p>This paragraph is intentionally much longer than the box can safely contain.</p></content>
</shape>
<shape type="text" topLeftX="1000" topLeftY="500" width="120" height="80">
<content textType="body"><p>Body text outside the canvas</p></content>
</shape>
</data>
</slide>
</presentation>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(result["summary"]["error_count"], 2)
self.assertEqual(result["summary"]["warning_count"], 0)
self.assertEqual(issue["code"], "shape_out_of_canvas")
self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 160, "bottom": 40})
def test_lint_xml_warns_when_text_may_overflow_its_own_shape(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="overflowing" type="text" topLeftX="80" topLeftY="80" width="360" height="80">
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit">
<p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
</content>
</shape>
<shape id="fitting" type="text" topLeftX="480" topLeftY="80" width="360" height="120">
<content fontSize="20" lineSpacing="multiple:1.5">
<p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
</content>
</shape>
<shape id="auto-fit" type="text" topLeftX="80" topLeftY="240" width="360" height="80">
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="normal-auto-fit">
<p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
</content>
</shape>
<shape id="shape-auto-fit" type="text" topLeftX="480" topLeftY="240" width="360" height="30">
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="shape-auto-fit">
<p>第一段</p><p>第二段</p><p>第三段</p>
</content>
</shape>
</data>
</slide>
"""
)
issues = result["slides"][0]["issues"]
overflow_issues = [issue for issue in issues if issue["code"] == "text_may_overflow_shape"]
self.assertEqual(result["summary"]["error_count"], 1)
overflow_ids = {issue["elements"][0] for issue in overflow_issues}
self.assertIn("overflowing", overflow_ids)
self.assertNotIn("auto-fit", overflow_ids)
self.assertNotIn("shape-auto-fit", overflow_ids)
self.assertNotIn("fitting", overflow_ids)
overflowing_issue = next(issue for issue in overflow_issues if issue["elements"] == ["overflowing"])
self.assertEqual(overflowing_issue["line_count"], 4)
self.assertEqual(overflowing_issue["estimated_height"], 110)
self.assertEqual(overflowing_issue["available_height"], 80)
self.assertEqual(overflowing_issue["overflow"], 30)
self.assertIn('wrap="true" autoFit="normal-auto-fit"', overflowing_issue["message"])
def test_lint_xml_uses_fixed_line_spacing_for_text_height_warning(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="fixed-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="50">
<content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
<p>第一段</p><p>第二段</p><p>第三段</p>
</content>
</shape>
</data>
</slide>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(result["summary"]["warning_count"], 1)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(issue["level"], "warning")
self.assertEqual(issue["line_height"], 20)
self.assertEqual(issue["estimated_height"], 60)
self.assertEqual(issue["overflow"], 10)
def test_lint_xml_ignores_subpixel_text_height_overflow_tolerance(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="minor-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="39.8">
<content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
<p>第一段</p><p>第二段</p>
</content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(overflow_issues, [])
def test_lint_xml_allows_single_line_width_estimation_jitter(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="metric" type="text" topLeftX="80" topLeftY="80" width="152" height="54">
<content fontSize="36" lineSpacing="multiple:1.2" autoFit="no-auto-fit"><p>4.16万亿</p></content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(overflow_issues, [])
def test_lint_xml_allows_short_metric_text_with_separators_as_single_line(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="metric" type="text" topLeftX="80" topLeftY="80" width="150" height="50">
<content textType="title" fontSize="36" autoFit="no-auto-fit"><p>4.16万亿</p></content>
</shape>
<shape id="table-number" type="text" topLeftX="80" topLeftY="160" width="25" height="20">
<content fontSize="10" textAlign="center" autoFit="no-auto-fit"><p>1,380</p></content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(overflow_issues, [])
def test_lint_xml_reports_plain_short_metric_when_it_wraps(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="plain-age" type="text" topLeftX="80" topLeftY="80" width="50" height="80">
<content textType="title" fontSize="36" bold="true" autoFit="no-auto-fit"><p>82岁</p></content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(len(overflow_issues), 1)
self.assertEqual(overflow_issues[0]["elements"], ["plain-age"])
def test_lint_xml_allows_centered_short_label_near_fit_as_single_line(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="centered-label" type="text" topLeftX="80" topLeftY="80" width="200" height="30">
<content fontSize="14" bold="true" textAlign="center" autoFit="no-auto-fit">
<p>参数服务器 (Parameter Server)</p>
</content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(overflow_issues, [])
def test_lint_xml_allows_headline_near_fit_as_single_line(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="headline" type="text" topLeftX="80" topLeftY="80" width="700" height="50">
<content textType="headline" fontSize="26" bold="true" lineSpacing="multiple:1.3" autoFit="no-auto-fit">
<p>全球半导体市场规模持续高速增长,AI驱动新一轮景气周期</p>
</content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(overflow_issues, [])
def test_lint_xml_allows_dense_body_line_spacing_estimation_slack(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="dense-body" type="text" topLeftX="80" topLeftY="80" width="360" height="140">
<content fontSize="13" bold="true" lineSpacing="multiple:1.7" autoFit="no-auto-fit">
<p>总体目标:</p>
<p>建立深度神经网络高效训练的统一理论框架,实现训练效率与模型性能的协同优化。</p>
<p>具体目标:</p>
<p>提出自适应优化算法,收敛速度提升 2-3 倍</p>
<p>实现结构化压缩方法,模型体积减少 10 倍以上</p>
<p>构建分布式训练策略,64 GPU 加速比 &gt; 50x</p>
</content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(overflow_issues, [])
def test_lint_xml_reports_dense_body_when_adjusted_height_still_overflows(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="dense-body" type="text" topLeftX="80" topLeftY="80" width="360" height="100">
<content fontSize="13" bold="true" lineSpacing="multiple:1.7" autoFit="no-auto-fit">
<p>总体目标:</p>
<p>建立深度神经网络高效训练的统一理论框架,实现训练效率与模型性能的协同优化。</p>
<p>具体目标:</p>
<p>提出自适应优化算法,收敛速度提升 2-3 倍</p>
<p>实现结构化压缩方法,模型体积减少 10 倍以上</p>
<p>构建分布式训练策略,64 GPU 加速比 &gt; 50x</p>
</content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(len(overflow_issues), 1)
self.assertEqual(overflow_issues[0]["elements"], ["dense-body"])
def test_lint_xml_reports_letter_spaced_caption_near_fit(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="caption" type="text" topLeftX="80" topLeftY="80" width="120" height="20">
<content textType="caption" fontSize="11" letterSpacing="1" autoFit="no-auto-fit">
<p>RISKS &amp; CHALLENGES</p>
</content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(len(overflow_issues), 1)
self.assertEqual(overflow_issues[0]["elements"], ["caption"])
def test_lint_xml_reports_micro_caption_when_wrapping_overflows(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="micro-caption" type="text" topLeftX="80" topLeftY="60" width="200" height="16">
<content textType="caption" fontSize="3" lineSpacing="multiple:1.3" letterSpacing="160" autoFit="no-auto-fit">
<p>MARKET INSIGHT · 市场洞察</p>
</content>
</shape>
</data>
</slide>
"""
)
overflow_issues = [
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
]
self.assertEqual(len(overflow_issues), 1)
self.assertEqual(overflow_issues[0]["elements"], ["micro-caption"])
def test_lint_xml_text_may_overflow_shape_upgrades_to_error_above_threshold(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="just-warning" type="text" topLeftX="80" topLeftY="80" width="360" height="50">
<content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
<p>第一段</p><p>第二段</p><p>第三段</p>
</content>
</shape>
<shape id="error-overflow" type="text" topLeftX="80" topLeftY="200" width="360" height="30">
<content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
<p>第一段</p><p>第二段</p><p>第三段</p>
</content>
</shape>
</data>
</slide>
"""
)
issues = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
self.assertEqual(issues["just-warning"]["level"], "warning")
self.assertEqual(issues["just-warning"]["overflow"], 10)
self.assertEqual(issues["error-overflow"]["level"], "error")
self.assertEqual(issues["error-overflow"]["overflow"], 30)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["summary"]["warning_count"], 1)
def test_lint_xml_text_may_overflow_shape_downgrades_background_decoration_to_info(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="bg-deco" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
</shape>
<shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
<content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
</shape>
</data>
</slide>
"""
)
issues = {
issue["elements"][0]: issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape"
}
self.assertEqual(issues["bg-deco"]["level"], "info")
self.assertEqual(result["summary"]["warning_count"], 0)
self.assertEqual(result["summary"]["info_count"], 1)
self.assertEqual(result["slides"][0]["infos"], [issues["bg-deco"]])
self.assertIn("background decoration", issues["bg-deco"]["message"])
def test_lint_xml_reports_shape_alpha_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="ghost-number" type="text" topLeftX="-60" topLeftY="30" width="360" height="180" alpha="0.2">
<content fontSize="160" lineSpacing="fixed:160" wrap="false"><p>01</p></content>
</shape>
<shape id="title" type="text" topLeftX="80" topLeftY="80" width="360" height="80">
<content fontSize="30" lineSpacing="fixed:36"><p>Annual Review</p></content>
</shape>
</data>
</slide>
"""
)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertIn("shape_out_of_canvas", codes)
self.assertNotIn("bbox_overlap", codes)
def test_lint_xml_reports_content_color_alpha_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="ghost-year" type="text" topLeftX="760" topLeftY="20" width="260" height="160">
<content fontSize="140" color="rgba(0,0,0,0.2)" lineSpacing="fixed:140" wrap="false"><p>2026</p></content>
</shape>
<shape id="headline" type="text" topLeftX="700" topLeftY="70" width="220" height="80">
<content fontSize="28" lineSpacing="fixed:34"><p>Forecast</p></content>
</shape>
</data>
</slide>
"""
)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertIn("shape_out_of_canvas", codes)
self.assertNotIn("bbox_overlap", codes)
def test_lint_xml_reports_faint_medium_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="medium-ghost" type="text" topLeftX="820" topLeftY="300" width="270" height="72" alpha="0.32">
<content fontSize="40" lineSpacing="fixed:40" wrap="false"><p>OFF EDGE</p></content>
</shape>
<shape id="caption" type="text" topLeftX="760" topLeftY="315" width="180" height="36">
<content fontSize="16" lineSpacing="fixed:20"><p>Readable caption</p></content>
</shape>
</data>
</slide>
"""
)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertIn("shape_out_of_canvas", codes)
self.assertNotIn("bbox_overlap", codes)
def test_lint_xml_allows_ghost_text_image_overlap(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="ghost-label" type="text" topLeftX="100" topLeftY="40" width="560" height="160" alpha="0.2">
<content fontSize="120" lineSpacing="fixed:120" wrap="false"><p>2026</p></content>
</shape>
<img id="photo" src="token" topLeftX="160" topLeftY="70" width="260" height="160"/>
<shape id="title" type="text" topLeftX="610" topLeftY="95" width="320" height="60">
<content fontSize="28" lineSpacing="fixed:34"><p>Annual Review</p></content>
</shape>
</data>
</slide>
"""
)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertNotIn("image_covers_text", codes)
self.assertNotIn("bbox_overlap", codes)
def test_lint_slide_allows_ghost_text_whiteboard_overlap(self) -> None:
result = xml_lint.lint_slide(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<whiteboard id="board" topLeftX="180" topLeftY="70" width="420" height="300"/>
<shape id="ghost-label" type="text" topLeftX="100" topLeftY="40" width="560" height="160" alpha="0.2">
<content fontSize="120" lineSpacing="fixed:120" wrap="false"><p>2026</p></content>
</shape>
<shape id="title" type="text" topLeftX="610" topLeftY="95" width="220" height="60">
<content fontSize="28" lineSpacing="fixed:34"><p>Annual Review</p></content>
</shape>
</data>
</slide>
""",
1,
)
codes = [issue["code"] for issue in result["issues"]]
self.assertNotIn("whiteboard_external_overlap", codes)
def test_lint_xml_reports_faint_ghost_text_out_of_canvas_without_area_threshold(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="small-ghost" type="text" topLeftX="940" topLeftY="300" width="40" height="40" alpha="0.32">
<content fontSize="36" lineSpacing="fixed:36" wrap="false"><p>土</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
def test_lint_xml_keeps_out_of_canvas_error_for_medium_text_without_faint_alpha(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="medium-not-ghost" type="text" topLeftX="820" topLeftY="300" width="270" height="72" alpha="0.36">
<content fontSize="54" lineSpacing="fixed:54" wrap="false"><p>OFF EDGE</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["medium-not-ghost"])
def test_lint_xml_keeps_out_of_canvas_error_for_half_alpha_large_text(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="half-alpha" type="text" topLeftX="760" topLeftY="20" width="260" height="160">
<content fontSize="140" color="rgba(0,0,0,0.5)" lineSpacing="fixed:140" wrap="false"><p>2026</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["half-alpha"])
def test_lint_xml_text_may_overflow_shape_keeps_error_when_alpha_not_low(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="opaque-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.9">
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
</shape>
<shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
<content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
</shape>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["opaque-big"]
)
self.assertEqual(issue["level"], "error")
def test_lint_xml_text_may_overflow_shape_keeps_error_when_no_foreground_text(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="lonely-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
</shape>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["lonely-big"]
)
self.assertEqual(issue["level"], "error")
def test_lint_xml_text_may_overflow_shape_keeps_error_when_foreground_alpha_zero(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="bg-deco" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
</shape>
<shape id="transparent-foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60" alpha="0">
<content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
</shape>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["bg-deco"]
)
self.assertEqual(issue["level"], "error")
def test_lint_xml_text_may_overflow_shape_keeps_error_when_foreground_is_below_in_order(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
<content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
</shape>
<shape id="top-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
</shape>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["top-big"]
)
self.assertEqual(issue["level"], "error")
def test_lint_xml_uses_paragraph_spacing_overrides_for_text_height_warning(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="paragraph-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="30">
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit">
<p lineSpacing="fixed:10" beforeLineSpacing="fixed:5" afterLineSpacing="fixed:5">第一行<br/>第二行</p>
</content>
</shape>
<shape id="paragraph-fitting" type="text" topLeftX="480" topLeftY="80" width="360" height="40">
<content fontSize="20" lineSpacing="multiple:1.5">
<p lineSpacing="fixed:10">第一行<br/>第二行<br/>第三行</p>
</content>
</shape>
</data>
</slide>
"""
)
issues = result["slides"][0]["issues"]
self.assertEqual(result["summary"]["warning_count"], 1)
self.assertEqual(issues[0]["elements"], ["paragraph-overflow"])
self.assertEqual(issues[0]["line_count"], 2)
self.assertEqual(issues[0]["line_height"], 10)
self.assertEqual(issues[0]["estimated_height"], 40)
self.assertEqual(issues[0]["overflow"], 10)
def test_lint_xml_uses_letter_spacing_for_text_overflow_warning(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="baseline" type="text" topLeftX="0" topLeftY="0" width="120" height="30">
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit"><p>一二三四五六</p></content>
</shape>
<shape id="content-spaced" type="text" topLeftX="200" topLeftY="0" width="120" height="30">
<content fontSize="20" lineSpacing="multiple:1.5" letterSpacing="2" autoFit="no-auto-fit"><p>一二三四五六</p></content>
</shape>
<shape id="paragraph-spaced" type="text" topLeftX="400" topLeftY="0" width="120" height="30">
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit"><p letterSpacing="2">一二三四五六</p></content>
</shape>
</data>
</slide>
"""
)
issues = result["slides"][0]["issues"]
overflow_ids = [issue["elements"][0] for issue in issues if issue["code"] == "text_may_overflow_shape"]
self.assertNotIn("baseline", overflow_ids)
self.assertIn("content-spaced", overflow_ids)
self.assertIn("paragraph-spaced", overflow_ids)
by_id = {issue["elements"][0]: issue for issue in issues if issue["code"] == "text_may_overflow_shape"}
self.assertEqual(by_id["content-spaced"]["line_count"], 2)
self.assertEqual(by_id["content-spaced"]["estimated_height"], 50)
self.assertEqual(by_id["content-spaced"]["overflow"], 20)
self.assertEqual(by_id["paragraph-spaced"]["line_count"], 2)
def test_strip_xml_paragraphs_preserves_br_as_hard_line_break(self) -> None:
self.assertEqual(
xml_lint.strip_xml_paragraphs("<p>第一行<br/>第二行<br />第三行</p>"),
"第一行\n第二行\n第三行",
)
def test_lint_xml_allows_template_style_images_outside_canvas(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<img src="tok" topLeftX="-120" topLeftY="20" width="360" height="360"/>
<shape type="text" topLeftX="40" topLeftY="80" width="180" height="80">
<content textType="title" fontSize="44"><p>Title</p></content>
</shape>
<shape type="text" topLeftX="40" topLeftY="120" width="180" height="40">
<content textType="sub-headline" fontSize="20"><p>Subtitle</p></content>
</shape>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(result["summary"]["warning_count"], 0)
def test_extract_elements_preserves_supported_element_geometry_order_and_text_metadata(self) -> None:
elements = xml_lint.extract_elements(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<img id="photo" src="tok" topLeftX="10" topLeftY="20" width="100" height="80"/>
<shape id="headline" type="text" topLeftX="40" topLeftY="60" width="320" height="90">
<content textType="headline" textAlign="center" autoFit="normal-auto-fit" fontSize="28">
<p><![CDATA[Growth & scale]]></p>
<p>Focused execution</p>
</content>
</shape>
<table id="table" topLeftX="400" topLeftY="60" width="220" height="120"></table>
<chart id="chart" topLeftX="640" topLeftY="60" width="220" height="120"/>
<whiteboard id="wb" topLeftX="80" topLeftY="220" width="760" height="240"/>
<embed id="emb" topLeftX="600" topLeftY="320" width="240" height="140">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 140"><rect x="0" y="0" width="240" height="140"/></svg>
</embed>
<shape id="missing-height" type="text" topLeftX="80" topLeftY="480" width="320">
<content><p>Skipped</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual([element["id"] for element in elements], ["photo", "headline", "table", "chart", "wb", "emb"])
self.assertEqual([element["kind"] for element in elements], ["img", "shape", "table", "chart", "whiteboard", "embed"])
self.assertEqual([element["order"] for element in elements], [0, 1, 2, 3, 4, 5])
self.assertEqual(elements[1]["type"], "text")
self.assertEqual(elements[1]["textType"], "headline")
self.assertEqual(elements[1]["textAlign"], "center")
self.assertEqual(elements[1]["autoFit"], "normal-auto-fit")
self.assertEqual(elements[1]["fontSize"], 28)
self.assertEqual(elements[1]["text"], "Growth & scale\nFocused execution")
def test_lint_xml_ignores_small_out_of_bounds_images(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<img src="tok" topLeftX="-20" topLeftY="20" width="120" height="120"/>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_ignores_out_of_canvas_images(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<img src="right" topLeftX="780" topLeftY="0" width="500" height="540"/>
<img src="bottom" topLeftX="0" topLeftY="430" width="900" height="280"/>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_ignores_full_bleed_images_outside_canvas(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<img src="tok" topLeftX="-80" topLeftY="-20" width="1080" height="600"/>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_reports_text_and_chart_but_not_image_out_of_canvas(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="outside-shape" type="text" topLeftX="-10" topLeftY="40" width="50" height="50"/>
<img id="outside-img" src="token" topLeftX="120" topLeftY="-20" width="50" height="50"/>
<chart id="outside-chart" topLeftX="900" topLeftY="100" width="100" height="100">
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
<chartData>
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
</chartData>
</chart>
</data>
</slide>
</presentation>
"""
)
issues = result["slides"][0]["issues"]
self.assertEqual(result["summary"]["error_count"], 2)
self.assertEqual(
[(issue["code"], issue["elements"], issue["overflow"]) for issue in issues],
[
("shape_out_of_canvas", ["outside-shape"], {"left": 10, "top": 0, "right": 0, "bottom": 0}),
("chart_out_of_canvas", ["outside-chart"], {"left": 0, "top": 0, "right": 40, "bottom": 0}),
],
)
def test_lint_xml_ignores_line_out_of_canvas(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="body" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content fontSize="18"><p>Visible content</p></content>
</shape>
<line id="connector" startX="80" startY="120" endX="980" endY="120"><border/></line>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(result["slides"][0]["issues"], [])
def test_lint_xml_reports_horizontal_line_crossing_headline_glyphs(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="title" type="text" topLeftX="80" topLeftY="200" width="500" height="90">
<content fontSize="60"><p>测试文字 ABC</p></content>
</shape>
<line id="strike" startX="80" startY="245" endX="560" endY="245">
<border color="rgb(255, 0, 0)" width="4"/>
</line>
</data>
</slide>
"""
)
crossing = [
issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"strike", "title"}
]
self.assertEqual(len(crossing), 1)
self.assertEqual(crossing[0]["code"], "bbox_overlap")
def test_lint_xml_reports_vertical_line_crossing_multiline_text(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="col" type="text" topLeftX="700" topLeftY="180" width="240" height="180">
<content fontSize="20"><p>第一行文字内容</p><p>第二行文字内容</p><p>第三行文字内容</p></content>
</shape>
<line id="vbar" startX="740" startY="170" endX="740" endY="360">
<border color="rgb(0, 0, 255)" width="3"/>
</line>
</data>
</slide>
"""
)
crossing = [
issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"vbar", "col"}
]
self.assertEqual(len(crossing), 1)
def test_lint_xml_reports_diagonal_line_crossing_text_block(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="para" type="text" topLeftX="80" topLeftY="400" width="420" height="140">
<content fontSize="18"><p>这是一段测试文字用于验证线条穿过</p></content>
</shape>
<line id="diag" startX="80" startY="410" endX="500" endY="530">
<border color="rgb(255, 0, 0)" width="3"/>
</line>
</data>
</slide>
"""
)
crossing = [
issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"diag", "para"}
]
self.assertEqual(len(crossing), 1)
def test_lint_xml_ignores_diagonal_line_whose_bbox_but_not_segment_crosses_text(self) -> None:
# The diagonal's axis-aligned bounding box overlaps the text, but the segment itself passes
# through empty space in the opposite corner -- a naive bbox test would false-positive here.
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="corner-text" type="text" topLeftX="80" topLeftY="80" width="120" height="40">
<content fontSize="18"><p>corner</p></content>
</shape>
<line id="far-diag" startX="700" startY="80" endX="90" endY="500">
<border color="rgb(255, 0, 0)" width="3"/>
</line>
</data>
</slide>
"""
)
crossing = [
issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"far-diag", "corner-text"}
]
self.assertEqual(crossing, [])
def test_lint_xml_ignores_line_touching_text_frame_but_not_glyphs(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="lbl" type="text" topLeftX="80" topLeftY="80" width="300" height="200">
<content fontSize="18" verticalAlign="top"><p>短标签</p></content>
</shape>
<line id="below" startX="80" startY="270" endX="380" endY="270">
<border color="rgb(255, 0, 0)" width="2"/>
</line>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_ignores_invisible_line_crossing_text(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="title" type="text" topLeftX="80" topLeftY="200" width="500" height="90">
<content fontSize="60"><p>测试文字 ABC</p></content>
</shape>
<line id="ghost-line" startX="80" startY="245" endX="560" endY="245">
<border color="rgba(255, 0, 0, 0.03)" width="4"/>
</line>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_ignores_vertical_line_grazing_text_left_edge(self) -> None:
# Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 4: a vertical line sitting on the text
# frame's left edge renders before the first glyph, so it must not be flagged.
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape width="240" height="60" topLeftX="120" topLeftY="100" type="text" id="bmm">
<content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
<p>Vertical edge graze</p>
</content>
</shape>
<line id="bmX" startX="120.00000000000001" startY="90" endX="120.00000000000001" endY="150.00833275470998">
<border color="rgba(0, 0, 0, 1)"/>
</line>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_ignores_polyline_crossing_text(self) -> None:
# Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 6: the crossing check is scoped to
# <line> only, so a <polyline> over text is not flagged.
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape width="240" height="60" topLeftX="120" topLeftY="100" type="text" id="bmr">
<content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
<p>Polyline target</p>
</content>
</shape>
<polyline id="bmH" width="270" height="55" topLeftX="110" topLeftY="95">
<border color="rgba(0, 0, 0, 1)"/>
</polyline>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_ignores_line_below_visual_glyph_height(self) -> None:
# Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 7: the shape frame is 80px tall but the
# single 20px line of glyphs occupies only its top; a line at the frame's lower region grazes
# under the visual glyph box (underline look) and must not be flagged.
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape width="240" height="80" topLeftX="120" topLeftY="100" type="text" id="bmB">
<content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
<p>Visual height target</p>
</content>
</shape>
<line id="bmQ" startX="110" startY="150" endX="380.00185184550116" endY="150">
<border color="rgba(0, 0, 0, 1)"/>
</line>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
def test_lint_xml_uses_rotated_text_and_chart_bounds_for_canvas_validation(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="rotated-text" type="text" topLeftX="0" topLeftY="0" width="100" height="100" rotation="45"/>
<chart id="rotated-chart" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45">
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
<chartData>
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
</chartData>
</chart>
</data>
</slide>
</presentation>
"""
)
issues_by_element = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
self.assertEqual(result["summary"]["error_count"], 2)
self.assertEqual(issues_by_element["rotated-text"]["code"], "shape_out_of_canvas")
self.assertAlmostEqual(issues_by_element["rotated-text"]["overflow"]["left"], 20.710678, places=5)
self.assertAlmostEqual(issues_by_element["rotated-text"]["overflow"]["top"], 20.710678, places=5)
self.assertEqual(issues_by_element["rotated-chart"]["code"], "chart_out_of_canvas")
self.assertAlmostEqual(issues_by_element["rotated-chart"]["overflow"]["right"], 20.710678, places=5)
def test_lint_xml_uses_declared_bounds_for_rect_and_ignores_images(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="rotated-rect" type="rect" topLeftX="900" topLeftY="0" width="100" height="100" rotation="45"/>
<img id="rotated-image" src="token" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45"/>
</data>
</slide>
</presentation>
"""
)
issues_by_element = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(issues_by_element["rotated-rect"]["code"], "shape_out_of_canvas")
self.assertEqual(issues_by_element["rotated-rect"]["overflow"], {"left": 0, "top": 0, "right": 40, "bottom": 0})
self.assertNotIn("rotated-image", issues_by_element)
def test_detect_elements_out_of_canvas_limits_detection_to_whitelist(self) -> None:
issues = xml_lint.detect_elements_out_of_canvas(
[
{"id": "table", "kind": "table", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 45},
{"id": "chart", "kind": "chart", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 0},
{
"id": "text",
"kind": "shape",
"type": "text",
"x": 95,
"y": 0,
"width": 10,
"height": 10,
"rotation": 0,
},
{
"id": "rect",
"kind": "shape",
"type": "rect",
"x": 95,
"y": 0,
"width": 10,
"height": 10,
"rotation": 45,
},
{"id": "image", "kind": "img", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 0},
{
"id": "ellipse",
"kind": "shape",
"type": "ellipse",
"x": 95,
"y": 0,
"width": 10,
"height": 10,
"rotation": 0,
},
],
100,
100,
)
self.assertEqual([issue["elements"] for issue in issues], [["table"], ["chart"], ["text"], ["rect"]])
self.assertEqual(issues[-1]["bbox"], {"x": 95, "y": 0, "width": 10, "height": 10})
def test_lint_xml_rejects_non_finite_rotation_values_from_xsd(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="infinite" type="text" topLeftX="-10" topLeftY="0" width="20" height="20" rotation="inf"/>
<shape id="negative-infinite" type="text" topLeftX="0" topLeftY="-10" width="20" height="20" rotation="-inf"/>
<chart id="not-a-number" topLeftX="950" topLeftY="0" width="20" height="20" rotation="nan">
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
<chartData>
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
</chartData>
</chart>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 3)
slide_issues = result["slides"][0]["issues"]
self.assertTrue(all(issue["code"] == "sxsd_invalid_scalar" for issue in slide_issues))
self.assertEqual({issue["actual"] for issue in slide_issues}, {"inf", "-inf", "nan"})
def test_lint_xml_reports_table_bottom_overflow_from_declared_bounds(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="score-table" topLeftX="54" topLeftY="238" width="414" height="385">
<tr><td><content><p>Score</p></content></td></tr>
</table>
</data>
</slide>
</presentation>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(issue["code"], "table_out_of_canvas")
self.assertEqual(issue["elements"], ["score-table"])
self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 0, "bottom": 83})
self.assertEqual(issue["bbox"], {"x": 54, "y": 238, "width": 414, "height": 385})
def test_lint_xml_reports_table_right_overflow_from_declared_bounds(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="wide-table" topLeftX="850" topLeftY="80" width="180" height="120">
<tr><td><content><p>Score</p></content></td></tr>
</table>
</data>
</slide>
</presentation>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(issue["code"], "table_out_of_canvas")
self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 70, "bottom": 0})
def test_lint_xml_allows_table_with_declared_bounds_inside_canvas(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="inside-table" topLeftX="40" topLeftY="120" width="880" height="360">
<tr><td><content><p>Score</p></content></td></tr>
</table>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(result["summary"]["warning_count"], 0)
def test_lint_xml_reports_resolved_table_bounds_when_declared_sizes_are_missing(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="implicit-size-table" topLeftX="850" topLeftY="480">
<colgroup><col/><col/></colgroup>
<tr><td/><td/></tr>
<tr><td/><td/></tr>
</table>
</data>
</slide>
</presentation>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(issue["code"], "table_out_of_canvas")
self.assertEqual(issue["bbox"], {"x": 850, "y": 480, "width": 220, "height": 74})
self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 110, "bottom": 14})
def test_lint_xml_xml_path_preserves_source_index_after_filtered_table(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<data>
<table id="t1" topLeftX="20" topLeftY="20">
<tr><td/></tr>
</table>
<table id="t2" topLeftX="20" topLeftY="100" width="9999" height="100">
<tr><td/></tr>
</table>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "table_out_of_canvas"
)
self.assertEqual(issue["element_ids"], ["t2"])
self.assertEqual(
issue["related_objects"][0]["xml_path"],
"slide[1]/data/table[2]",
)
def test_lint_xml_duplicate_id_keeps_issue_bound_to_original_shape(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="dup" type="rect" topLeftX="-20" topLeftY="40" width="50" height="50"/>
<shape id="dup" type="rect" topLeftX="100" topLeftY="40" width="50" height="50"/>
</data>
</slide>
"""
)
canvas_issue = next(
issue for issue in result["slides"][0]["issues"]
if issue["code"] == "shape_out_of_canvas"
)
self.assertEqual(canvas_issue["element_ids"], ["dup"])
self.assertEqual(
canvas_issue["related_objects"],
[
{
"element_id": "dup",
"kind": "shape",
"type": "rect",
"bbox": {"x": -20, "y": 40, "width": 50, "height": 50},
"xml_path": "slide[1]/data/shape[1]",
}
],
)
self.assertTrue(
canvas_issue["hint"].startswith(
"Locate via related_objects[].xml_path. "
)
)
duplicate_issue = next(
issue for issue in result["slides"][0]["issues"]
if issue["code"] == "duplicate_element_id"
)
self.assertEqual(
duplicate_issue["hint"],
"Locate via related_objects[].xml_path. "
"Do not invent replacement IDs. For newly authored elements, remove the id attribute. "
"When updating read-back XML, keep the server ID on the original element only and remove it "
"from copied or new elements.",
)
self.assertEqual(duplicate_issue["element_ids"], ["dup", "dup"])
self.assertEqual(
[obj["xml_path"] for obj in duplicate_issue["related_objects"]],
["slide[1]/data/shape[1]", "slide[1]/data/shape[2]"],
)
def test_lint_xml_blocks_duplicate_table_cell_ids(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="table-1" topLeftX="80" topLeftY="80" width="800" height="120">
<colgroup><col width="400"/><col width="400"/></colgroup>
<tr height="120">
<td id="bjs"><content fontSize="24"><p>Original cell</p></content></td>
<td id="bjs"><content fontSize="24"><p>Copied cell</p></content></td>
</tr>
</table>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "duplicate_element_id"
)
self.assertFalse(result["summary"]["release_ready"])
self.assertEqual(issue["element_ids"], ["bjs", "bjs"])
self.assertEqual(
issue["related_objects"],
[
{
"element_id": "bjs",
"kind": "td",
"type": "td",
"xml_path": "slide[1]/data/table[1]/tr[1]/td[1]",
},
{
"element_id": "bjs",
"kind": "td",
"type": "td",
"xml_path": "slide[1]/data/table[1]/tr[1]/td[2]",
},
],
)
def test_lint_xml_blocks_duplicate_id_shared_by_shape_and_table_cell(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="baa" type="rect" topLeftX="40" topLeftY="40" width="80" height="80"/>
<table id="table-1" topLeftX="160" topLeftY="40" width="200" height="80">
<tr height="80"><td id="baa"><content fontSize="12"><p>Cell</p></content></td></tr>
</table>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "duplicate_element_id"
)
self.assertEqual(issue["element_ids"], ["baa", "baa"])
self.assertEqual(
[obj["xml_path"] for obj in issue["related_objects"]],
["slide[1]/data/shape[1]", "slide[1]/data/table[1]/tr[1]/td[1]"],
)
def test_lint_xml_blocks_duplicate_id_shared_by_shape_and_undefined(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="dup" type="rect" topLeftX="40" topLeftY="40" width="80" height="80"/>
<undefined id="dup" type="video"/>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "duplicate_element_id"
)
self.assertFalse(result["summary"]["release_ready"])
self.assertEqual(issue["element_ids"], ["dup", "dup"])
self.assertEqual(
issue["related_objects"],
[
{
"element_id": "dup",
"kind": "shape",
"type": "rect",
"bbox": {"x": 40, "y": 40, "width": 80, "height": 80},
"xml_path": "slide[1]/data/shape[1]",
},
{
"element_id": "dup",
"kind": "undefined",
"type": "video",
"xml_path": "slide[1]/data/undefined[1]",
},
],
)
def test_lint_xml_does_not_report_unique_table_cell_and_note_ids(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="table-1" topLeftX="80" topLeftY="80" width="800" height="120">
<colgroup><col width="400"/><col width="400"/></colgroup>
<tr height="120"><td id="baa"/><td id="bab"/></tr>
</table>
</data>
<note id="bac"><content fontSize="12"><p>Note</p></content></note>
</slide>
"""
)
self.assertNotIn(
"duplicate_element_id",
[issue["code"] for issue in result["slides"][0]["issues"]],
)
def test_lint_xml_blocks_duplicate_ids_across_slides(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide>
<data>
<shape id="dup" type="rect" topLeftX="40" topLeftY="40" width="80" height="80"/>
</data>
</slide>
<slide>
<data>
<shape id="dup" type="rect" topLeftX="140" topLeftY="40" width="80" height="80"/>
</data>
</slide>
</presentation>
"""
)
issue = next(
issue
for issue in result["document"]["errors"]
if issue["code"] == "duplicate_element_id"
)
self.assertFalse(result["summary"]["release_ready"])
self.assertEqual(issue["element_ids"], ["dup", "dup"])
self.assertEqual(
issue["related_objects"],
[
{
"element_id": "dup",
"kind": "shape",
"type": "rect",
"bbox": {"x": 40, "y": 40, "width": 80, "height": 80},
"xml_path": "slide[1]/data/shape[1]",
},
{
"element_id": "dup",
"kind": "shape",
"type": "rect",
"bbox": {"x": 140, "y": 40, "width": 80, "height": 80},
"xml_path": "slide[2]/data/shape[1]",
},
],
)
def test_lint_xml_does_not_treat_slide_ids_as_element_ids(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide id="dup"><data/></slide>
<slide id="dup"><data/></slide>
</presentation>
"""
)
self.assertNotIn(
"duplicate_element_id",
[issue["code"] for issue in result["document"]["errors"]],
)
def test_lint_xml_does_not_treat_presentation_id_as_element_id(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" id="dup" width="960" height="540">
<slide>
<data><undefined id="dup" type="video"/></data>
</slide>
</presentation>
"""
)
self.assertNotIn(
"duplicate_element_id",
[
issue["code"]
for issue in [
*result["document"]["errors"],
*result["slides"][0]["errors"],
]
],
)
def test_lint_xml_blocks_duplicate_note_ids_across_slides(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide>
<note id="baa"><content fontSize="12"><p>First note</p></content></note>
</slide>
<slide>
<note id="baa"><content fontSize="12"><p>Copied note</p></content></note>
</slide>
</presentation>
"""
)
issue = next(
issue
for issue in result["document"]["errors"]
if issue["code"] == "duplicate_element_id"
)
self.assertFalse(result["summary"]["release_ready"])
self.assertEqual(issue["element_ids"], ["baa", "baa"])
self.assertEqual(
issue["related_objects"],
[
{
"element_id": "baa",
"kind": "note",
"type": "note",
"xml_path": "slide[1]/note[1]",
},
{
"element_id": "baa",
"kind": "note",
"type": "note",
"xml_path": "slide[2]/note[1]",
},
],
)
def test_lint_xml_cross_kind_duplicate_id_does_not_change_related_object_kind(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="dup" type="rect" topLeftX="-20" topLeftY="40" width="50" height="50"/>
<img id="dup" src="token" topLeftX="100" topLeftY="40" width="50" height="50"/>
</data>
</slide>
"""
)
issue = next(
issue for issue in result["slides"][0]["issues"]
if issue["code"] == "shape_out_of_canvas"
)
self.assertEqual(issue["related_objects"][0]["kind"], "shape")
self.assertEqual(
issue["related_objects"][0]["xml_path"],
"slide[1]/data/shape[1]",
)
duplicate_issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "duplicate_element_id"
)
self.assertEqual(
[obj["xml_path"] for obj in duplicate_issue["related_objects"]],
["slide[1]/data/shape[1]", "slide[1]/data/img[1]"],
)
def test_normalize_issue_does_not_repeat_xml_path_hint_prefix(self) -> None:
xml_path = "slide[1]/data/shape[1]"
element = {
"id": "box",
"_source_id": "box",
"_ref": xml_path,
"xml_path": xml_path,
"kind": "shape",
"type": "rect",
"x": 0,
"y": 0,
"width": 40,
"height": 40,
}
prefix = "Locate via related_objects[].xml_path."
issue = xml_lint.normalize_issue(
{
"level": "error",
"code": "shape_out_of_canvas",
"elements": [xml_path],
"canvas": {"width": 960, "height": 540},
"bbox": {"x": -10, "y": 0, "width": 40, "height": 40},
"overflow": {"left": 10, "top": 0, "right": 0, "bottom": 0},
"hint": f"{prefix} Move the shape inside the canvas.",
},
1,
{xml_path: element},
)
self.assertEqual(issue["hint"].count(prefix), 1)
def test_lint_xml_elements_keep_locator_for_anonymous_related_object(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="text" topLeftX="100" topLeftY="100" width="300" height="100">
<content fontSize="24"><p>Important text</p></content>
</shape>
<img id="srv-42" src="token" topLeftX="100" topLeftY="100" width="300" height="100"/>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "image_covers_text"
)
self.assertEqual(
issue["elements"],
["srv-42", "slide[1]/data/shape[1]"],
)
self.assertEqual(issue["element_ids"], ["srv-42"])
self.assertEqual(len(issue["related_objects"]), 2)
def test_normalize_issue_deduplicates_repeated_element_refs(self) -> None:
xml_path = "slide[1]/data/shape[1]"
element = {
"id": "srv-42",
"_source_id": "srv-42",
"_ref": xml_path,
"xml_path": xml_path,
"kind": "shape",
"type": "rect",
"x": 0,
"y": 0,
"width": 40,
"height": 40,
}
issue = xml_lint.normalize_issue(
{
"level": "warning",
"code": "blank_slide",
"measurement": {
"visible_element_count": 0,
"declared_element_count": 1,
},
"elements": [xml_path, xml_path],
},
1,
{xml_path: element},
)
self.assertEqual(issue["elements"], ["srv-42"])
self.assertEqual(issue["element_ids"], ["srv-42"])
self.assertEqual(len(issue["related_objects"]), 1)
def test_lint_xml_reports_duplicate_ids_for_every_linted_element_kind(self) -> None:
duplicate_pairs = {
"shape": (
'<shape id="dup" type="rect" topLeftX="10" topLeftY="10" width="40" height="40"/>',
'<shape id="dup" type="rect" topLeftX="60" topLeftY="10" width="40" height="40"/>',
),
"chart": (
'<chart id="dup" topLeftX="10" topLeftY="10" width="40" height="40"><chartPlotArea><chartPlot type="line"/></chartPlotArea><chartData><dim1><chartField name="category" valueType="string">A</chartField></dim1><dim2><chartField name="value" valueType="number">1</chartField></dim2></chartData></chart>',
'<chart id="dup" topLeftX="60" topLeftY="10" width="40" height="40"><chartPlotArea><chartPlot type="line"/></chartPlotArea><chartData><dim1><chartField name="category" valueType="string">A</chartField></dim1><dim2><chartField name="value" valueType="number">1</chartField></dim2></chartData></chart>',
),
"table": (
'<table id="dup" topLeftX="10" topLeftY="10" width="40" height="40"><colgroup><col width="40"/></colgroup><tr height="40"><td/></tr></table>',
'<table id="dup" topLeftX="60" topLeftY="10" width="40" height="40"><colgroup><col width="40"/></colgroup><tr height="40"><td/></tr></table>',
),
"img": (
'<img id="dup" src="token" topLeftX="10" topLeftY="10" width="40" height="40"/>',
'<img id="dup" src="token" topLeftX="60" topLeftY="10" width="40" height="40"/>',
),
"line": (
'<line id="dup" startX="10" startY="10" endX="40" endY="40"><border color="rgb(0, 0, 0)"/></line>',
'<line id="dup" startX="60" startY="10" endX="90" endY="40"><border color="rgb(0, 0, 0)"/></line>',
),
"icon": (
'<icon id="dup" iconType="iconpark/Base/setting.svg" topLeftX="10" topLeftY="10" width="40" height="40"><fill><fillColor color="rgb(0, 0, 0)"/></fill></icon>',
'<icon id="dup" iconType="iconpark/Base/setting.svg" topLeftX="60" topLeftY="10" width="40" height="40"><fill><fillColor color="rgb(0, 0, 0)"/></fill></icon>',
),
"polyline": (
'<polyline id="dup" topLeftX="10" topLeftY="10" width="40" height="40"><border color="rgb(0, 0, 0)"/></polyline>',
'<polyline id="dup" topLeftX="60" topLeftY="10" width="40" height="40"><border color="rgb(0, 0, 0)"/></polyline>',
),
}
for kind, pair in duplicate_pairs.items():
with self.subTest(kind=kind):
result = xml_lint.lint_xml(
f'<slide xmlns="https://www.larkoffice.com/sml/2.0"><data>{pair[0]}{pair[1]}</data></slide>'
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "duplicate_element_id"
)
self.assertEqual(issue["element_ids"], ["dup", "dup"])
self.assertEqual(
[obj["xml_path"] for obj in issue["related_objects"]],
[
f"slide[1]/data/{kind}[1]",
f"slide[1]/data/{kind}[2]",
],
)
def test_lint_xml_missing_id_does_not_collide_with_explicit_synthetic_like_id(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="rect" topLeftX="-20" topLeftY="40" width="50" height="50"/>
<shape id="shape-1" type="rect" topLeftX="100" topLeftY="40" width="50" height="50"/>
</data>
</slide>
"""
)
issue = next(
issue for issue in result["slides"][0]["issues"]
if issue["code"] == "shape_out_of_canvas"
)
self.assertEqual(issue["element_ids"], [])
self.assertNotIn("element_id", issue["related_objects"][0])
self.assertEqual(
issue["related_objects"][0]["xml_path"],
"slide[1]/data/shape[1]",
)
self.assertTrue(
issue["hint"].startswith("Locate via related_objects[].xml_path. ")
)
self.assertNotIn(
"duplicate_element_id",
[candidate["code"] for candidate in result["slides"][0]["issues"]],
)
def test_lint_xml_empty_id_is_not_exposed_as_an_element_id(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="" type="rect" topLeftX="-20" topLeftY="40" width="50" height="50"/>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "shape_out_of_canvas"
)
self.assertEqual(issue["element_ids"], [])
self.assertNotIn("element_id", issue["related_objects"][0])
self.assertEqual(
issue["related_objects"][0]["xml_path"],
"slide[1]/data/shape[1]",
)
def test_lint_xml_uses_resolved_table_bounds_for_canvas_validation(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="resolved-overflow-table" topLeftX="800" topLeftY="80" width="100" height="40">
<colgroup><col width="100"/><col width="100"/></colgroup>
<tr height="40"><td/><td/></tr>
</table>
</data>
</slide>
</presentation>
"""
)
issues = result["slides"][0]["issues"]
canvas_issue = next(issue for issue in issues if issue["code"] == "table_out_of_canvas")
mismatch_issue = next(issue for issue in issues if issue["code"] == "table_resolved_size_mismatch")
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(canvas_issue["bbox"], {"x": 800, "y": 80, "width": 200, "height": 40})
self.assertEqual(canvas_issue["overflow"]["right"], 40)
self.assertEqual(mismatch_issue["dimension"], "width")
self.assertEqual(mismatch_issue["resolved_size"], canvas_issue["bbox"]["width"])
def test_lint_xml_uses_the_same_anonymous_table_path_for_all_table_diagnostics(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="200" height="40"/>
<img id="logo" src="token" topLeftX="40" topLeftY="100" width="40" height="40"/>
<table topLeftX="900" topLeftY="80" width="100" height="40">
<colgroup><col width="100"/><col width="100"/></colgroup>
<tr height="40"><td/><td/></tr>
</table>
</data>
</slide>
</presentation>
"""
)
issues = result["slides"][0]["issues"]
canvas_issue = next(issue for issue in issues if issue["code"] == "table_out_of_canvas")
mismatch_issue = next(issue for issue in issues if issue["code"] == "table_resolved_size_mismatch")
self.assertEqual(canvas_issue["elements"], ["slide[1]/data/table[1]"])
self.assertEqual(mismatch_issue["elements"], ["slide[1]/data/table[1]"])
self.assertEqual(
canvas_issue["related_objects"][0]["xml_path"],
"slide[1]/data/table[1]",
)
self.assertEqual(
mismatch_issue["related_objects"][0]["xml_path"],
"slide[1]/data/table[1]",
)
self.assertNotIn("element_id", canvas_issue["related_objects"][0])
self.assertNotIn("element_id", mismatch_issue["related_objects"][0])
def test_lint_xml_reports_info_when_table_target_size_resolves_larger_than_declared(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="size-mismatch" topLeftX="40" topLeftY="120" width="200" height="80">
<colgroup><col span="2" width="100"/><col width="50"/></colgroup>
<tr height="40"><td/><td/><td/></tr>
<tr height="60"><td/><td/><td/></tr>
</table>
</data>
</slide>
</presentation>
"""
)
issues_by_dimension = {issue["dimension"]: issue for issue in result["slides"][0]["issues"]}
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(result["summary"]["warning_count"], 0)
self.assertEqual(result["summary"]["info_count"], 2)
self.assertEqual(issues_by_dimension["width"]["level"], "info")
self.assertEqual(issues_by_dimension["width"]["code"], "table_resolved_size_mismatch")
self.assertEqual(issues_by_dimension["width"]["resolved_sizes"], [100, 100, 50])
self.assertEqual(issues_by_dimension["width"]["resolved_size"], 250)
self.assertEqual(issues_by_dimension["height"]["resolved_sizes"], [40, 60])
self.assertEqual(issues_by_dimension["height"]["resolved_size"], 100)
def test_lint_xml_does_not_report_info_when_table_target_size_is_resolved_exactly(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="size-match" topLeftX="40" topLeftY="120" width="300" height="100">
<colgroup><col width="100"/><col/></colgroup>
<tr height="40"><td/><td/></tr>
<tr><td/><td/></tr>
</table>
</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(result["summary"]["warning_count"], 0)
self.assertEqual(result["slides"][0]["issues"], [])
def test_lint_xml_keeps_resolved_table_sizes_positive_when_target_is_too_small(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<table id="narrow-table" topLeftX="40" topLeftY="120" width="1">
<colgroup><col/><col/></colgroup>
<tr><td/><td/></tr>
</table>
</data>
</slide>
</presentation>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(issue["dimension"], "width")
self.assertEqual(issue["resolved_sizes"], [1, 1])
self.assertEqual(issue["resolved_size"], 2)
def test_fill_last_size_gap_preserves_target_when_positive_sizes_are_possible(self) -> None:
final_sizes = xml_lint.fill_last_size_gap([10, 10], 3)
self.assertEqual(final_sizes, [2, 1])
self.assertEqual(sum(final_sizes), 3)
def test_cli_reports_table_layout_size_info_for_weighted_min_layout_cases(self) -> None:
cases = {
"target-exact": (
"""
<table topLeftX="40" topLeftY="120" width="360" height="150">
<colgroup><col width="100"/><col width="200"/></colgroup>
<tr height="40"><td/><td/></tr><tr height="60"><td/><td/></tr>
</table>
""",
0,
),
"declared-size-exceeds-target": (
"""
<table topLeftX="40" topLeftY="120" width="200" height="80">
<colgroup><col span="2" width="100"/><col width="50"/></colgroup>
<tr height="40"><td/><td/><td/></tr><tr height="60"><td/><td/><td/></tr>
</table>
""",
2,
),
"remaining-space-insufficient": (
"""
<table topLeftX="40" topLeftY="120" width="80" height="30">
<colgroup><col width="80"/><col/></colgroup>
<tr height="40"><td/><td/></tr><tr><td/><td/></tr>
</table>
""",
2,
),
"no-target-size": (
"""
<table topLeftX="40" topLeftY="120">
<colgroup><col width="80"/><col/></colgroup>
<tr height="40"><td/><td/></tr><tr><td/><td/></tr>
</table>
""",
0,
),
}
script_path = Path(xml_lint.__file__).resolve()
with tempfile.TemporaryDirectory() as temp_dir:
for name, (table_xml, expected_info_count) in cases.items():
with self.subTest(case=name):
input_path = Path(temp_dir) / f"{name}.xml"
input_path.write_text(
f"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0"><data>{table_xml}</data></slide>
</presentation>
""",
encoding="utf-8",
)
completed = subprocess.run(
[sys.executable, str(script_path), "--input", str(input_path)],
capture_output=True,
check=False,
text=True,
)
result = json.loads(completed.stdout)
self.assertEqual(completed.returncode, 0, completed.stderr)
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(result["summary"]["warning_count"], 0)
self.assertEqual(result["summary"]["info_count"], expected_info_count)
self.assertTrue(
all(issue["level"] == "info" for issue in result["slides"][0]["issues"]),
result["slides"][0]["issues"],
)
def test_lint_xml_detects_invalid_template_text_stack_overlap(self) -> None:
cases = [
(
"subtitle-too-high",
"""
<shape type="text" topLeftX="40" topLeftY="80" width="240" height="90">
<content textType="title" fontSize="44"><p>Title</p></content>
</shape>
<shape type="text" topLeftX="40" topLeftY="90" width="240" height="80">
<content textType="sub-headline" fontSize="20"><p>Subtitle</p></content>
</shape>
""",
),
]
for name, shapes in cases:
with self.subTest(name=name):
result = xml_lint.lint_xml(
f"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>{shapes}</data>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["slides"][0]["issues"][0]["code"], "bbox_overlap")
def test_lint_xml_reports_vertical_text_image_overlap_as_warning(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0"><data>
<shape id="text" type="text" vert="vert" topLeftX="100" topLeftY="100" width="100" height="100">
<content><p>Vertical</p></content>
</shape>
<img id="image" src="token" topLeftX="120" topLeftY="120" width="20" height="20"/>
</data></slide>
"""
)
issue = next(issue for issue in result["slides"][0]["issues"] if issue["code"] == "image_may_cover_vertical_text")
self.assertEqual(issue["level"], "info")
self.assertEqual(result["summary"]["error_count"], 0)
self.assertEqual(result["summary"]["info_count"], 1)
def test_lint_xml_related_objects_include_source_xml_paths(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide>
<data>
<shape type="text" topLeftX="80" topLeftY="80" width="400" height="60">
<content fontSize="24"><p>Control slide</p></content>
</shape>
</data>
</slide>
<slide>
<data>
<shape type="text" topLeftX="70" topLeftY="55" width="820" height="70">
<content fontSize="32"><p>shape-3 mapping experiment</p></content>
</shape>
<shape type="text" topLeftX="80" topLeftY="165" width="400" height="70">
<content fontSize="18"><p>First, preserve source order.</p></content>
</shape>
<shape type="text" topLeftX="80" topLeftY="315" width="400" height="64">
<content fontSize="26"><p>TARGET_SHAPE_THREE</p></content>
</shape>
<img src="token" topLeftX="80" topLeftY="305" width="400" height="110"/>
</data>
</slide>
</presentation>
"""
)
issue = next(
issue
for issue in result["slides"][1]["issues"]
if issue["code"] == "image_covers_text"
)
self.assertEqual(
[(obj["kind"], obj["xml_path"]) for obj in issue["related_objects"]],
[
("img", "slide[2]/data/img[1]"),
("shape", "slide[2]/data/shape[3]"),
],
)
self.assertTrue(
all("element_id" not in obj for obj in issue["related_objects"])
)
def test_lint_xml_related_objects_include_line_xml_path(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<data>
<shape id="label" type="text" topLeftX="100" topLeftY="100" width="200" height="80">
<content fontSize="24"><p>Crossed text</p></content>
</shape>
<line id="connector" startX="80" startY="130" endX="330" endY="130">
<border color="rgb(15, 23, 42)" width="2"/>
</line>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "bbox_overlap" and issue["elements"][0] == "connector"
)
self.assertEqual(
{
obj["element_id"]: obj["xml_path"]
for obj in issue["related_objects"]
},
{
"connector": "slide[1]/data/line[1]",
"label": "slide[1]/data/shape[1]",
},
)
class XmlTextOverlapLintDensityTest(unittest.TestCase):
def test_lint_xml_sparse_container_related_objects_include_icon_xml_path(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="60" topLeftY="120" width="400" height="300"/>
<icon id="visual" iconType="iconpark/Base/setting.svg" topLeftX="80" topLeftY="140" width="32" height="32">
<fill><fillColor color="rgb(37, 99, 235)"/></fill>
</icon>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "sparse_container_content"
)
self.assertEqual(
{
obj["element_id"]: obj["xml_path"]
for obj in issue["related_objects"]
},
{
"card": "slide[1]/data/shape[1]",
"visual": "slide[1]/data/icon[1]",
},
)
def test_lint_xml_blocks_blank_slide(self) -> None:
result = xml_lint.lint_xml(
"""
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540">
<slide id="content-slide">
<data>
<shape id="title" type="text" topLeftX="60" topLeftY="60" width="400" height="50">
<content fontSize="28"><p>Investment report</p></content>
</shape>
</data>
</slide>
<slide id="blank-slide">
<style><fill><fillColor color="rgba(255, 255, 255, 1)"/></fill></style>
<data/>
<note><content/></note>
</slide>
</presentation>
"""
)
self.assertEqual(result["summary"]["slide_count"], 2)
self.assertEqual(result["summary"]["warning_count"], 0)
self.assertEqual(result["summary"]["error_count"], 1)
self.assertEqual(result["summary"]["status"], "blocked")
self.assertFalse(result["summary"]["release_ready"])
self.assertEqual(result["slides"][0]["issues"], [])
self.assertEqual(result["slides"][1]["element_count"], 0)
issue = result["slides"][1]["errors"][0]
self.assertEqual(issue["level"], "error")
self.assertEqual(issue["code"], "blank_slide")
self.assertEqual(issue["element_ids"], [])
self.assertEqual(issue["rule"]["id"], "blank_slide")
self.assertEqual(issue["measurement"]["visible_element_count"], 0)
self.assertEqual(issue["related_objects"], [])
def test_lint_xml_blocks_blank_slide_with_only_transparent_image(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<img id="ghost" src="token" topLeftX="60" topLeftY="60" width="200" height="200" alpha="0"/>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 1)
issue = result["slides"][0]["errors"][0]
self.assertEqual(issue["code"], "blank_slide")
def test_lint_xml_warns_when_large_container_is_mostly_empty(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="trend-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="370"/>
<shape id="trend-title" type="text" topLeftX="515" topLeftY="147" width="380" height="28">
<content fontSize="15"><p>Core trends</p></content>
</shape>
<shape id="trend-copy" type="text" topLeftX="515" topLeftY="177" width="380" height="315">
<content fontSize="12"><p>First point</p><p>Second point</p><p>Third point</p></content>
</shape>
</data>
</slide>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(issue["code"], "sparse_container_content")
self.assertEqual(issue["target"]["container_id"], "trend-card")
self.assertEqual(issue["target"], {
"slide_number": 1,
"container_id": "trend-card",
"container_xml_path": "slide[1]/data/shape[1]",
"container_type": "rect",
"bbox": {"x": 500, "y": 135, "width": 410, "height": 370},
})
self.assertLess(issue["measurement"]["content_coverage_ratio"], 0.15)
self.assertEqual(issue["rule"], {
"name": "large_container_visible_content_coverage",
"threshold": 0.15,
"comparison": "content_coverage_ratio < threshold",
"id": "sparse_container_content",
})
self.assertEqual(issue["measurement"]["container_area"], 151700)
self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.03)
self.assertEqual(issue["elements"], ["trend-card", "trend-title", "trend-copy"])
self.assertEqual(issue["element_ids"], ["trend-card", "trend-title", "trend-copy"])
self.assertEqual(
[obj["element_id"] for obj in issue["related_objects"]],
["trend-card", "trend-title", "trend-copy"],
)
self.assertEqual(result["slides"][0]["status"], "needs_screenshot_review")
self.assertEqual(result["slides"][0]["warnings"], result["slides"][0]["issues"])
def test_lint_xml_uses_xml_path_in_anonymous_sparse_container_message(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape type="rect" topLeftX="500" topLeftY="135" width="410" height="370"/>
<shape id="trend-title" type="text" topLeftX="515" topLeftY="147" width="380" height="28">
<content fontSize="15"><p>Core trends</p></content>
</shape>
</data>
</slide>
"""
)
issue = next(
issue
for issue in result["slides"][0]["issues"]
if issue["code"] == "sparse_container_content"
)
self.assertNotIn("container_id", issue["target"])
self.assertEqual(
issue["target"]["container_xml_path"],
"slide[1]/data/shape[1]",
)
self.assertEqual(
issue["message"],
"large card slide[1]/data/shape[1] content coverage 1.0% is below 15.0%",
)
def test_lint_xml_warns_for_sparse_short_cards(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card-1" type="rect" topLeftX="60" topLeftY="180" width="400" height="105"/>
<shape id="text-1" type="text" topLeftX="80" topLeftY="220" width="360" height="30">
<content fontSize="14"><p>期待认识大家</p></content>
</shape>
<shape id="card-2" type="rect" topLeftX="490" topLeftY="180" width="400" height="105"/>
<shape id="text-2" type="text" topLeftX="510" topLeftY="220" width="360" height="30">
<content fontSize="14"><p>化学一起讨论</p></content>
</shape>
<shape id="card-3" type="rect" topLeftX="60" topLeftY="310" width="400" height="105"/>
<shape id="text-3" type="text" topLeftX="80" topLeftY="350" width="360" height="30">
<content fontSize="14"><p>吉他随时交流</p></content>
</shape>
<shape id="card-4" type="rect" topLeftX="490" topLeftY="310" width="400" height="105"/>
<shape id="text-4" type="text" topLeftX="510" topLeftY="350" width="360" height="30">
<content fontSize="14"><p>共度美好四年</p></content>
</shape>
</data>
</slide>
"""
)
container_issues = [
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
]
self.assertEqual(
[issue["target"]["container_id"] for issue in container_issues],
["card-1", "card-2", "card-3", "card-4"],
)
self.assertTrue(all(issue["target"]["bbox"]["height"] == 105 for issue in container_issues))
self.assertTrue(all(issue["measurement"]["content_coverage_ratio"] < 0.15 for issue in container_issues))
self.assertEqual(
[issue["code"] for issue in result["slides"][0]["issues"]],
[
"sparse_container_content",
"sparse_container_content",
"sparse_container_content",
"sparse_container_content",
"sparse_slide_content",
],
)
def test_lint_xml_warns_when_whole_slide_has_too_little_effective_content(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540"/>
<shape id="text-1" type="text" topLeftX="60" topLeftY="80" width="200" height="30">
<content fontSize="14"><p>One short line</p></content>
</shape>
<shape id="text-2" type="text" topLeftX="500" topLeftY="180" width="200" height="30">
<content fontSize="14"><p>Another line</p></content>
</shape>
<shape id="text-3" type="text" topLeftX="60" topLeftY="310" width="200" height="30">
<content fontSize="14"><p>Third line</p></content>
</shape>
<shape id="text-4" type="text" topLeftX="500" topLeftY="410" width="200" height="30">
<content fontSize="14"><p>Fourth line</p></content>
</shape>
</data>
</slide>
"""
)
issues = [issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_slide_content"]
self.assertEqual(len(issues), 1)
issue = issues[0]
self.assertEqual(issue["target"]["bbox"], {"x": 0, "y": 0, "width": 960, "height": 540})
self.assertEqual(issue["rule"]["threshold"], 0.035)
self.assertLess(issue["measurement"]["content_coverage_ratio"], 0.035)
self.assertEqual(issue["measurement"]["content_element_count"], 4)
self.assertNotIn("background", issue["elements"])
def test_lint_xml_ignores_isolated_short_layout_bar(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="summary-bar" type="rect" topLeftX="52" topLeftY="82" width="856" height="105"/>
<shape id="summary" type="text" topLeftX="72" topLeftY="115" width="816" height="30">
<content fontSize="14"><p>One concise summary</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["slides"][0]["issues"], [])
def test_lint_xml_counts_rect_own_content_as_visible_content(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="load-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
<content fontSize="18">
<p>被吊物</p>
<p><span fontSize="36">32.0 t</span></p>
<p>钢结构模块</p>
</content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["slides"][0]["issues"], [])
def test_lint_xml_reports_nonzero_coverage_for_rect_own_content_reproduction(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="load-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
<content fontSize="18">
<p>被吊物</p>
<p>32.0 t</p>
<p>钢结构模块</p>
</content>
</shape>
</data>
</slide>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertGreater(issue["measurement"]["visible_content_area"], 0)
self.assertEqual(issue["measurement"]["content_element_count"], 1)
self.assertGreater(issue["measurement"]["content_coverage_ratio"], 0)
def test_lint_xml_still_warns_for_sparse_rect_own_content(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="sparse-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
<content fontSize="12"><p>A</p></content>
</shape>
</data>
</slide>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(issue["target"]["container_id"], "sparse-card")
self.assertGreater(issue["measurement"]["visible_content_area"], 0)
self.assertEqual(issue["measurement"]["content_element_count"], 1)
self.assertEqual(issue["elements"], ["sparse-card"])
def test_lint_xml_unions_rect_own_content_with_child_content(self) -> None:
self_only = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
<content fontSize="12"><p>A</p></content>
</shape>
</data>
</slide>
"""
)
with_overlapping_child = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
<content fontSize="12"><p>A</p></content>
</shape>
<shape id="child" type="text" topLeftX="60" topLeftY="140" width="220" height="184">
<content fontSize="12"><p>A</p></content>
</shape>
</data>
</slide>
"""
)
self_issue = self_only["slides"][0]["issues"][0]
mixed_issue = with_overlapping_child["slides"][0]["issues"][0]
self.assertEqual(
mixed_issue["measurement"]["visible_content_area"],
self_issue["measurement"]["visible_content_area"],
)
self.assertEqual(mixed_issue["measurement"]["content_element_count"], 2)
def test_extract_density_elements_reads_nested_font_size_from_rect_content(self) -> None:
elements = xml_lint.extract_density_elements(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
<content fontSize="12"><p><span fontSize="36">32.0 t</span></p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(elements[0]["fontSize"], 36)
def test_extract_density_elements_does_not_attach_following_text_to_self_closing_rect(self) -> None:
elements = xml_lint.extract_density_elements(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184"/>
<shape id="title" type="text" topLeftX="80" topLeftY="160" width="180" height="30">
<content fontSize="18"><p>Following title</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(elements[0]["text"], "")
self.assertEqual(elements[1]["text"], "Following title")
def test_lint_xml_allows_container_with_large_visual_child(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
<chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220">
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
<chartData>
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
</chartData>
</chart>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["warning_count"], 0)
def test_lint_xml_does_not_let_transparent_visual_child_suppress_sparse_warning(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
<content fontSize="20"><p>Section title</p></content>
</shape>
<shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
<chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220" alpha="0">
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
<chartData>
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
</chartData>
</chart>
</data>
</slide>
"""
)
issue = next(
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
)
self.assertEqual(issue["target"]["container_id"], "chart-card")
def test_lint_xml_warns_for_small_empty_visual_placeholder_cards(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="letter-placeholder" type="rect" topLeftX="520" topLeftY="180" width="200" height="200"/>
<shape id="letter" type="text" topLeftX="540" topLeftY="250" width="160" height="70">
<content fontSize="46"><p>Z</p></content>
</shape>
<shape id="empty-placeholder" type="rect" topLeftX="744" topLeftY="180" width="144" height="200"/>
</data>
</slide>
"""
)
issues = result["slides"][0]["issues"]
self.assertEqual(
[issue["target"]["container_id"] for issue in issues],
["letter-placeholder", "empty-placeholder"],
)
self.assertEqual(issues[1]["measurement"]["content_element_count"], 0)
def test_lint_xml_applies_global_threshold_to_normal_text_card(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="70" topLeftY="184" width="260" height="288"/>
<shape id="title" type="text" topLeftX="90" topLeftY="215" width="220" height="30">
<content fontSize="18"><p>梦境与现实</p></content>
</shape>
<shape id="copy" type="text" topLeftX="90" topLeftY="330" width="220" height="70">
<content fontSize="13"><p>边界溶解,逻辑失效。观众被拽入潜意识的迷宫。</p></content>
</shape>
</data>
</slide>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(issue["target"]["container_id"], "card")
self.assertEqual(issue["rule"]["threshold"], 0.15)
def test_lint_xml_allows_image_overlay_rect(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<img id="hero" topLeftX="560" topLeftY="0" width="400" height="540"/>
<shape id="tint" type="rect" topLeftX="560" topLeftY="0" width="400" height="540"/>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["warning_count"], 0)
def test_lint_xml_does_not_let_transparent_image_overlay_suppress_sparse_warning(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
<content fontSize="20"><p>Section title</p></content>
</shape>
<shape id="card" type="rect" topLeftX="330" topLeftY="120" width="300" height="300"/>
<img id="ghost-overlay" src="token" topLeftX="330" topLeftY="120" width="300" height="300" alpha="0"/>
</data>
</slide>
"""
)
issue = next(
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
)
self.assertEqual(issue["target"]["container_id"], "card")
def test_lint_xml_allows_edge_spanning_layout_panel_and_nested_decoration(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="panel" type="rect" topLeftX="600" topLeftY="0" width="360" height="540"/>
<shape id="decoration" type="rect" topLeftX="660" topLeftY="150" width="240" height="240"/>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["warning_count"], 0)
def test_lint_xml_counts_icons_as_visible_content(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="320" height="240"/>
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="180" height="180">
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
</icon>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["warning_count"], 0)
def test_lint_xml_does_not_count_transparent_icon_as_visible_content(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
<content fontSize="20"><p>Section title</p></content>
</shape>
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="320" height="240"/>
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="180" height="180" alpha="0">
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
</icon>
</data>
</slide>
"""
)
issue = next(
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
)
self.assertEqual(issue["target"]["container_id"], "card")
self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0)
def test_lint_xml_warns_when_coverage_is_below_global_threshold(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="200" height="200"/>
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="70" height="70">
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
</icon>
</data>
</slide>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(issue["target"]["container_id"], "card")
self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.122)
self.assertEqual(issue["rule"]["threshold"], 0.15)
def test_lint_xml_allows_quarter_coverage_under_lower_threshold(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="200" height="200"/>
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="100" height="100">
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
</icon>
</data>
</slide>
"""
)
self.assertEqual(result["slides"][0]["issues"], [])
def test_lint_xml_allows_large_metric_card_above_lower_threshold(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="metric-card" type="rect" topLeftX="80" topLeftY="140" width="360" height="300"/>
<shape id="metric" type="text" topLeftX="104" topLeftY="190" width="340" height="90">
<content fontSize="12"><p><strong><span fontSize="62">400</span></strong>+ 项</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["slides"][0]["issues"], [])
def test_lint_xml_does_not_report_blank_slide_for_embed_only_content(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="http://www.larkoffice.com/sml/2.0">
<data>
<embed id="emb" topLeftX="280" topLeftY="130" width="400" height="280">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 280">
<circle cx="200" cy="140" r="100" fill="#2563EB"/>
</svg>
</embed>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertNotIn("blank_slide", codes)
def test_lint_xml_does_not_report_blank_slide_for_line_only_content(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<line id="l1" startX="100" startY="100" endX="800" endY="100"><border/></line>
<line id="l2" startX="100" startY="200" endX="800" endY="200"><border/></line>
<line id="l3" startX="100" startY="300" endX="800" endY="300"><border/></line>
<line id="l4" startX="100" startY="400" endX="800" endY="400"><border/></line>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertNotIn("blank_slide", codes)
def test_lint_xml_reports_bbox_overlap_measurement_from_decision_time_visual_bbox(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="left" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
<content fontSize="14"><p>overlap text <span fontSize="96">big</span></p></content>
</shape>
<shape id="right" type="text" topLeftX="80" topLeftY="80" width="300" height="80">
<content fontSize="14"><p>other overlap text</p></content>
</shape>
</data>
</slide>
"""
)
issue = result["slides"][0]["issues"][0]
self.assertEqual(issue["code"], "bbox_overlap")
# Must match the visual bbox that should_flag_overlap actually decided with (fontSize=14
# from extract_elements), not the fontSize=96 max-descendant value that
# extract_density_elements computes for the same "left" element id.
self.assertEqual(issue["measurement"]["intersection_width"], 109.2)
self.assertEqual(issue["measurement"]["intersection_height"], 6.8)
self.assertEqual(issue["measurement"]["intersection_area"], 742.56)
def test_has_similar_short_card_peer_excludes_the_element_itself(self) -> None:
card_a = {"kind": "shape", "type": "rect", "x": 0, "y": 0, "width": 300, "height": 100}
card_b = {"kind": "shape", "type": "rect", "x": 400, "y": 0, "width": 300, "height": 100}
card_c = {"kind": "shape", "type": "rect", "x": 0, "y": 200, "width": 300, "height": 100}
self.assertFalse(
xml_lint.has_similar_short_card_peer(card_a, [card_a, card_b])
)
self.assertTrue(
xml_lint.has_similar_short_card_peer(card_a, [card_a, card_b, card_c])
)
def test_lint_xml_reports_schema_version_2_for_sparse_issues(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184"/>
</data>
</slide>
"""
)
issue = next(
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
)
self.assertEqual(issue["schema_version"], "2.0")
def test_lint_xml_does_not_report_blank_slide_for_textless_decorative_shapes(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="deco1" type="ellipse" topLeftX="60" topLeftY="60" width="300" height="300">
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
</shape>
<shape id="deco2" type="triangle" topLeftX="500" topLeftY="200" width="200" height="200">
<fill><fillColor color="rgba(220, 38, 38, 1)"/></fill>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["summary"]["error_count"], 0)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertNotIn("blank_slide", codes)
def test_lint_xml_still_warns_for_sparse_slide_content_despite_full_bleed_background(self) -> None:
# A plain textless shape now counts as "not blank" (see the test above), but a
# full-bleed background rect must still NOT count toward sparse_slide_content's
# meaningful-content coverage ratio -- otherwise every slide with a background would
# trivially "pass" that density check.
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540"/>
<shape id="text-1" type="text" topLeftX="60" topLeftY="80" width="200" height="30">
<content fontSize="14"><p>One short line</p></content>
</shape>
<shape id="text-2" type="text" topLeftX="500" topLeftY="180" width="200" height="30">
<content fontSize="14"><p>Another line</p></content>
</shape>
<shape id="text-3" type="text" topLeftX="60" topLeftY="310" width="200" height="30">
<content fontSize="14"><p>Third line</p></content>
</shape>
<shape id="text-4" type="text" topLeftX="500" topLeftY="410" width="200" height="30">
<content fontSize="14"><p>Fourth line</p></content>
</shape>
</data>
</slide>
"""
)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertIn("sparse_slide_content", codes)
def test_lint_xml_accepts_whitespace_around_attribute_equals_sign(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="visible" type="text" topLeftX = "80" topLeftY = "80" width = "300" height = "60">
<content><p>hello</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(result["slides"][0]["element_count"], 1)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertNotIn("blank_slide", codes)
def test_lint_xml_reports_blank_slide_for_full_canvas_background_only(self) -> None:
result = xml_lint.lint_xml(
"""
<slide xmlns="https://www.larkoffice.com/sml/2.0">
<data>
<shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540">
<fill><fillColor color="rgba(240, 235, 220, 1)"/></fill>
</shape>
</data>
</slide>
"""
)
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
self.assertIn("blank_slide", codes)
def test_has_similar_short_card_peer_ignores_invisible_peers(self) -> None:
visible_card = {"kind": "shape", "type": "rect", "x": 0, "y": 0, "width": 300, "height": 100}
ghost_1 = {
"kind": "shape", "type": "rect", "x": 400, "y": 0, "width": 300, "height": 100, "alpha": 0,
}
ghost_2 = {
"kind": "shape", "type": "rect", "x": 800, "y": 0, "width": 300, "height": 100, "alpha": 0,
}
self.assertFalse(
xml_lint.has_similar_short_card_peer(
visible_card, [visible_card, ghost_1, ghost_2]
)
)
SML_NAMESPACE = "https://www.larkoffice.com/sml/2.0"
class SxsdSyntaxTestCase(unittest.TestCase):
def validate(self, xml: str) -> list[dict[str, object]]:
result = xml_lint.lint_xml(xml)
return [
*result.get("issues", []),
*(issue for slide in result["slides"] for issue in slide["issues"]),
]
def assert_issue(
self,
issues: list[dict[str, object]],
code: str,
*,
path: str | None = None,
attr: str | None = None,
) -> dict[str, object]:
for issue in issues:
if issue.get("code") != code:
continue
if path is not None and issue.get("path") != path:
continue
if attr is not None and issue.get("attr") != attr:
continue
return issue
self.fail(f"missing issue code={code!r} path={path!r} attr={attr!r}: {issues!r}")
def assert_no_issue(self, issues: list[dict[str, object]], code: str) -> None:
self.assertNotIn(code, [issue.get("code") for issue in issues])
class SxsdSyntaxAttributeTest(SxsdSyntaxTestCase):
def test_xsd_pattern_translation_only_expands_whitespace_classes(self) -> None:
self.assertEqual(
sxsd_validator.python_pattern_for_xsd(r"\s+\S+\w+\d+"),
"[ \\t\\n\\r]+[^ \\t\\n\\r]+\\w+\\d+",
)
def test_href_domain_pattern_does_not_use_backtracking_regex(self) -> None:
pattern = r"[\w.-]+[.:]\S*"
adversarial_value = ("a." * 20_000) + " "
original_fullmatch = sxsd_validator.re.fullmatch
translated_pattern = sxsd_validator.python_pattern_for_xsd(pattern)
def reject_unsafe_pattern(candidate: str, value: str):
if candidate == translated_pattern:
raise AssertionError("href domain pattern must not use re.fullmatch")
return original_fullmatch(candidate, value)
with mock.patch.object(sxsd_validator.re, "fullmatch", side_effect=reject_unsafe_pattern):
self.assertFalse(sxsd_validator.xsd_pattern_matches(pattern, adversarial_value))
def test_href_domain_pattern_keeps_xsd_matching_behavior(self) -> None:
pattern = r"[\w.-]+[.:]\S*"
reference_pattern = sxsd_validator.re.compile(
sxsd_validator.python_pattern_for_xsd(pattern)
)
for length in range(5):
for characters in itertools.product("a.:-/ ©", repeat=length):
value = "".join(characters)
with self.subTest(value=value):
self.assertEqual(
sxsd_validator.xsd_pattern_matches(pattern, value),
reference_pattern.fullmatch(value) is not None,
)
def test_accepts_valid_shape_attributes(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content textType="body"><p>Valid</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(issues, [])
def test_reports_missing_required_shape_attribute(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data><shape type="text" topLeftX="10" topLeftY="20" width="300"/></data>
</slide>
"""
)
issue = self.assert_issue(
issues,
"sxsd_missing_required_attr",
path="slide/data/shape",
attr="height",
)
self.assertEqual(issue["expected"], "required attribute of type PositiveSize")
self.assertIsNone(issue["actual"])
def test_reports_invalid_scalar_value(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="NaN" topLeftY="20" width="300" height="80"/>
</data>
</slide>
"""
)
issue = self.assert_issue(issues, "sxsd_invalid_scalar", attr="topLeftX")
self.assertEqual(issue["actual"], "NaN")
def test_rejects_python_only_numeric_separator(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="rect" topLeftX="1_0" topLeftY="20" width="300" height="80"/>
</data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_invalid_scalar", attr="topLeftX")
def test_accepts_xsd_double_lexical_forms(self) -> None:
for top_left_x in ("10", "-0.5", ".5", "1.", "1e2"):
with self.subTest(top_left_x=top_left_x):
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="rect" topLeftX="{top_left_x}" topLeftY="20" width="300" height="80"/>
</data>
</slide>
"""
)
self.assertEqual(issues, [])
def test_accepts_bullet_char_length_boundaries(self) -> None:
for bullet_char in ("A", "12345678"):
with self.subTest(bullet_char=bullet_char):
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content bulletChar="{bullet_char}"><p>Text</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(issues, [])
def test_rejects_bullet_char_outside_length_boundaries(self) -> None:
for bullet_char in ("", "123456789"):
with self.subTest(bullet_char=bullet_char):
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content bulletChar="{bullet_char}"><p>Text</p></content>
</shape>
</data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_value_out_of_range", attr="bulletChar")
def test_rejects_zero_size_that_violates_xsd(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="0" height="80"/>
</data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_value_out_of_range", attr="width")
def test_reports_negative_size_rejected_by_xsd(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="-1" height="80"/>
</data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_value_out_of_range", attr="width")
def test_rejects_shape_enum_that_violates_xsd(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="not-a-shape" topLeftX="10" topLeftY="20" width="300" height="80"/>
</data>
</slide>
"""
)
issue = self.assert_issue(issues, "sxsd_invalid_enum", attr="type")
self.assertLess(len(str(issue["message"])), 300)
self.assertEqual(issue["actual"], "not-a-shape")
def test_rejects_rotation_upper_bound_that_violates_xsd(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80" rotation="360"/>
</data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_value_out_of_range", attr="rotation")
def test_rejects_fill_color_that_violates_xsd(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<style><fill><fillColor color="red"/></fill></style>
<data/>
</slide>
"""
)
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="color")
def test_reports_missing_required_image_src(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data><img topLeftX="10" topLeftY="20" width="300" height="80"/></data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_missing_required_attr", attr="src")
def test_accepts_inline_attribute_simple_type(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content><p><a href="https://example.com">Link</a></p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(issues, [])
def test_reports_inline_attribute_pattern_mismatch(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content><p><a href="not a uri">Link</a></p></content>
</shape>
</data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="href")
def test_accepts_values_matching_inline_union_members(self) -> None:
for bullet_size in ("25%", "100%", "400%", "6", "14", "400"):
with self.subTest(bullet_size=bullet_size):
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content bulletSize="{bullet_size}"><p>Text</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(issues, [])
def test_rejects_values_outside_inline_union_members(self) -> None:
for bullet_size in ("24%", "401%", "5", "401", "abc"):
with self.subTest(bullet_size=bullet_size):
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content bulletSize="{bullet_size}"><p>Text</p></content>
</shape>
</data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="bulletSize")
def test_rejects_symbol_outside_python_word_semantics_in_href(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content><p><a href="©:resource">Link</a></p></content>
</shape>
</data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="href")
def test_accepts_common_email_href_with_python_regex_semantics(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content><p><a href="mailto:user@example.com">Email</a></p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(issues, [])
def test_accepts_common_gradient_with_python_regex_semantics(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<style>
<fill>
<fillColor color="linear-gradient(90deg, rgb(255, 0, 0) 0%, rgb(0, 0, 255) 100%)"/>
</fill>
</style>
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content><p>Gradient</p></content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(issues, [])
def test_rejects_non_xsd_whitespace_in_color_pattern(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<style><fill><fillColor color="rgb(1,\u00a02,3)"/></fill></style>
<data/>
</slide>
"""
)
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="color")
class SxsdSyntaxStructureTest(SxsdSyntaxTestCase):
def test_accepts_nested_content_in_referenced_rich_text_shadow(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
<content>
<p><span><shadow color="rgba(0, 0, 0, 1)"><strong>Text</strong></shadow></span></p>
</content>
</shape>
</data>
</slide>
"""
)
self.assertEqual(issues, [])
def test_keeps_shape_effect_shadow_as_childless_local_type(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data>
<shape type="rect" topLeftX="10" topLeftY="20" width="300" height="80">
<shadow><strong>Not rich text</strong></shadow>
</shape>
</data>
</slide>
"""
)
self.assert_issue(
issues,
"sxsd_unexpected_child",
path="slide/data/shape/shadow/strong",
)
def test_accepts_standalone_slide_fragment_without_namespace(self) -> None:
issues = self.validate(
'<slide><data><shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">'
'<content><p>Text</p></content></shape></data></slide>'
)
self.assertEqual(issues, [])
def test_rejects_presentation_without_namespace(self) -> None:
issues = self.validate(
'<presentation width="960" height="540"><slide/></presentation>'
)
self.assert_issue(issues, "sxsd_invalid_namespace", path="presentation")
def test_rejects_wrong_namespace_that_violates_xsd(self) -> None:
issues = self.validate('<slide xmlns="https://example.com/not-sml"><data/></slide>')
self.assert_issue(issues, "sxsd_invalid_namespace", path="slide")
def test_rejects_descendant_outside_document_namespace(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data xmlns="">
<shape xmlns="{SML_NAMESPACE}" type="rect" topLeftX="10" topLeftY="20" width="300" height="80"/>
</data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_invalid_namespace", path="slide/data")
def test_rejects_unexpected_child_that_violates_xsd(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80"/>
</slide>
"""
)
self.assert_issue(issues, "sxsd_unexpected_child", path="slide/shape")
def test_rejects_child_order_that_violates_xsd(self) -> None:
issues = self.validate(
f"""
<presentation xmlns="{SML_NAMESPACE}" width="1920" height="1080">
<slide/>
<title>Late title</title>
</presentation>
"""
)
self.assert_issue(issues, "sxsd_invalid_child_order", path="presentation/title")
def test_enforces_presentation_slide_minimum_from_xsd(self) -> None:
issues = self.validate(
f'<presentation xmlns="{SML_NAMESPACE}" width="1920" height="1080"/>'
)
self.assert_issue(issues, "sxsd_missing_required_child", path="presentation")
def test_enforces_presentation_slide_maximum_from_xsd(self) -> None:
slides = "".join("<slide/>" for _ in range(101))
issues = self.validate(
f'<presentation xmlns="{SML_NAMESPACE}" width="1920" height="1080">{slides}</presentation>'
)
self.assert_issue(issues, "sxsd_too_many_children", path="presentation/slide")
def test_rejects_multiple_choice_children_that_violate_xsd(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<style>
<fill><fillColor/><fillImg src="token"/></fill>
</style>
</slide>
"""
)
self.assert_issue(issues, "sxsd_too_many_children", path="slide/style/fill")
def test_rejects_line_without_required_border_from_xsd(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data><line startX="0" startY="0" endX="100" endY="100"/></data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_missing_required_child", path="slide/data/line")
def test_reports_missing_required_chart_structure(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data><chart topLeftX="0" topLeftY="0" width="300" height="200"/></data>
</slide>
"""
)
self.assert_issue(issues, "sxsd_missing_required_child", path="slide/data/chart")
def test_reports_missing_required_nested_sequence_child(self) -> None:
issues = self.validate(
f"""
<slide xmlns="{SML_NAMESPACE}">
<data><table topLeftX="0" topLeftY="0"><tr/></table></data>
</slide>
"""
)
issue = self.assert_issue(issues, "sxsd_missing_required_child", path="slide/data/table/tr")
self.assertEqual(issue["expected"], "td (at least 1)")
class SxsdSchemaModelTest(unittest.TestCase):
def test_reports_unsupported_xsd_pattern_without_crashing(self) -> None:
schema = rf"""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:sml="{SML_NAMESPACE}"
targetNamespace="{SML_NAMESPACE}"
elementFormDefault="qualified">
<xs:simpleType name="UnsupportedPatternType">
<xs:union>
<xs:simpleType>
<xs:restriction base="xs:string"><xs:pattern value="[\S]"/></xs:restriction>
</xs:simpleType>
<xs:simpleType>
<xs:restriction base="xs:string"><xs:pattern value="z+"/></xs:restriction>
</xs:simpleType>
</xs:union>
</xs:simpleType>
<xs:complexType name="SlideType">
<xs:attribute name="value" type="sml:UnsupportedPatternType"/>
</xs:complexType>
</xs:schema>
"""
with tempfile.TemporaryDirectory() as temp_dir:
schema_path = Path(temp_dir) / "schema.xsd"
schema_path.write_text(schema, encoding="utf-8")
try:
issues = sxsd_validator.validate_sxsd(
ET.fromstring(f'<slide xmlns="{SML_NAMESPACE}" value="A"/>'),
schema_path,
)
except (ValueError, sxsd_validator.re.error) as error:
self.fail(f"SXSD pattern capability errors must be reported, not raised: {error}")
self.assertEqual([issue["code"] for issue in issues], ["sxsd_unsupported_pattern"])
self.assertEqual(issues[0]["attr"], "value")
self.assertIn("pattern interpreter", str(issues[0]["hint"]).lower())
def test_standalone_slide_uses_slide_type_without_global_element(self) -> None:
schema = f"""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:sml="{SML_NAMESPACE}"
targetNamespace="{SML_NAMESPACE}"
elementFormDefault="qualified">
<xs:complexType name="SlideType"><xs:sequence/></xs:complexType>
<xs:complexType name="PresentationType">
<xs:sequence><xs:element name="slide" type="sml:SlideType"/></xs:sequence>
</xs:complexType>
<xs:element name="presentation" type="sml:PresentationType"/>
</xs:schema>
"""
with tempfile.TemporaryDirectory() as temp_dir:
schema_path = Path(temp_dir) / "schema.xsd"
schema_path.write_text(schema, encoding="utf-8")
issues = sxsd_validator.validate_sxsd(
ET.fromstring(f'<slide xmlns="{SML_NAMESPACE}"/>'),
schema_path,
)
self.assertEqual(issues, [])
def test_standalone_slide_requires_slide_type_in_xsd(self) -> None:
schema = f"""
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:sml="{SML_NAMESPACE}"
targetNamespace="{SML_NAMESPACE}"
elementFormDefault="qualified">
<xs:complexType name="PresentationType"><xs:sequence/></xs:complexType>
<xs:element name="presentation" type="sml:PresentationType"/>
</xs:schema>
"""
with tempfile.TemporaryDirectory() as temp_dir:
schema_path = Path(temp_dir) / "schema.xsd"
schema_path.write_text(schema, encoding="utf-8")
issues = sxsd_validator.validate_sxsd(
ET.fromstring(f'<slide xmlns="{SML_NAMESPACE}"/>'),
schema_path,
)
self.assertEqual([issue["code"] for issue in issues], ["sxsd_unexpected_root"])
if __name__ == "__main__":
unittest.main()