mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
fix(slides): lint 放行并识别 embed 嵌入元素
schema 已支持 <embed> 嵌入 SVG 协议,但版式准出 lint 未同步: - SXSD 标签校验仅在 <whiteboard> 祖先下放行 SVG 命名空间子树, <embed> 内的合法 <svg> 会被判 sxsd_unsupported_tag 并 block - extract_elements 不提取 embed,仅含 embed 的页面被误报 blank_slide,也不参与重叠/密度检查 修复:SVG 子树白名单加入 embed 祖先并更新 svg 报错 hint;embed 进入元素提取以及 slide_content_visual_bbox、is_large_visual_child 的视觉元素集合。新增 embed 内 svg 放行、embed-only 不报空白页 两个测试,extract_elements 测试覆盖 embed。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -403,7 +403,7 @@ def build_sxsd_tag_hint(tag_name: str, supported_tags: set[str]) -> str:
|
||||
if alias:
|
||||
return f"Use {alias} instead of <{tag_name}>."
|
||||
if tag_name == "svg":
|
||||
return 'Inside <whiteboard>, write SVG as <svg xmlns="http://www.w3.org/2000/svg">...</svg>.'
|
||||
return 'Inside <embed> or <whiteboard>, write SVG as <svg xmlns="http://www.w3.org/2000/svg">...</svg>.'
|
||||
close_matches = get_close_matches(tag_name, sorted(supported_tags), n=3, cutoff=0.72)
|
||||
if close_matches:
|
||||
return "Unsupported SXSD tag. Did you mean " + ", ".join(f"<{match}>" for match in close_matches) + "?"
|
||||
@@ -424,7 +424,7 @@ def build_sxsd_attr_hint(tag_name: str, attr_name: str, allowed_attrs: set[str])
|
||||
|
||||
|
||||
def should_skip_sxsd_subtree(element: ET.Element, ancestors: list[str]) -> bool:
|
||||
return "whiteboard" in ancestors and xml_namespace(element.tag) == SVG_NS
|
||||
return ("whiteboard" in ancestors or "embed" in ancestors) and xml_namespace(element.tag) == SVG_NS
|
||||
|
||||
|
||||
def should_skip_sxsd_attribute(tag_name: str, attr_name: str) -> bool:
|
||||
@@ -702,7 +702,7 @@ def parse_presentation(xml: str) -> dict[str, Any]:
|
||||
def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
|
||||
elements: list[dict[str, Any]] = []
|
||||
|
||||
for match in re.finditer(r"<(shape|img|table|chart|whiteboard)\b([^>]*)>", slide_xml):
|
||||
for match in re.finditer(r"<(shape|img|table|chart|whiteboard|embed)\b([^>]*)>", slide_xml):
|
||||
kind, attrs = match.group(1), match.group(2)
|
||||
is_self_closing = attrs.rstrip().endswith("/")
|
||||
content = ""
|
||||
@@ -1917,7 +1917,7 @@ def slide_content_visual_bbox(
|
||||
# a straight horizontal/vertical line has zero width or height in one axis; clipped_bbox
|
||||
# treats zero-area rects as invisible, so pad to its rendered stroke thickness instead.
|
||||
return clipped_bbox(line_stroke_bbox(element), slide_bbox)
|
||||
if element["kind"] in {"img", "chart", "table", "whiteboard", "icon", "polyline"}:
|
||||
if element["kind"] in {"img", "chart", "table", "whiteboard", "embed", "icon", "polyline"}:
|
||||
return clipped_bbox(element, slide_bbox)
|
||||
return None
|
||||
|
||||
@@ -1955,7 +1955,7 @@ def is_slide_content_present(
|
||||
|
||||
|
||||
def is_large_visual_child(element: dict[str, Any], container: dict[str, Any]) -> bool:
|
||||
if element["kind"] not in {"img", "chart", "table", "whiteboard"}:
|
||||
if element["kind"] not in {"img", "chart", "table", "whiteboard", "embed"}:
|
||||
return False
|
||||
if not is_visually_rendered(element):
|
||||
return False
|
||||
|
||||
@@ -265,6 +265,27 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
|
||||
self.assertEqual(result["summary"]["error_count"], 0)
|
||||
self.assertEqual(result["summary"]["warning_count"], 0)
|
||||
|
||||
def test_lint_xml_allows_svg_subtree_inside_embed(self) -> None:
|
||||
result = xml_text_overlap_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>
|
||||
</svg>
|
||||
</embed>
|
||||
</data>
|
||||
</slide>
|
||||
"""
|
||||
)
|
||||
codes = [issue["code"] for issue in result.get("issues", [])]
|
||||
self.assertNotIn("sxsd_unsupported_tag", codes)
|
||||
self.assertNotIn("sxsd_unsupported_attr", 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">'),
|
||||
@@ -1356,6 +1377,9 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
|
||||
<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>
|
||||
@@ -1363,9 +1387,9 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
|
||||
</slide>
|
||||
"""
|
||||
)
|
||||
self.assertEqual([element["id"] for element in elements], ["photo", "headline", "table", "chart", "wb"])
|
||||
self.assertEqual([element["kind"] for element in elements], ["img", "shape", "table", "chart", "whiteboard"])
|
||||
self.assertEqual([element["order"] for element in elements], [0, 1, 2, 3, 4])
|
||||
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")
|
||||
@@ -2404,6 +2428,25 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result["slides"][0]["issues"], [])
|
||||
|
||||
def test_lint_xml_does_not_report_blank_slide_for_embed_only_content(self) -> None:
|
||||
result = xml_text_overlap_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_text_overlap_lint.lint_xml(
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user