mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
Compare commits
2 Commits
refactor/o
...
feat/slide
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2b77acfdb | ||
|
|
91edc443e8 |
@@ -1076,6 +1076,7 @@
|
||||
<xs:element ref="sml:img"/>
|
||||
<xs:element ref="sml:table"/>
|
||||
<xs:element ref="sml:icon"/>
|
||||
<xs:element ref="sml:embed"/>
|
||||
<xs:element ref="sml:chart"/>
|
||||
<xs:element ref="sml:undefined"/>
|
||||
</xs:choice>
|
||||
@@ -1622,6 +1623,49 @@
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<!-- 嵌入内容元素 -->
|
||||
<xs:element name="embed">
|
||||
<xs:annotation>
|
||||
<xs:documentation>
|
||||
嵌入内容容器, 外层承载 Slides 摆放和效果属性, 内层承载外部标准内容。
|
||||
当前内层内容为标准 SVG, 必须使用 http://www.w3.org/2000/svg 命名空间。
|
||||
后续可扩展承载 HTML 等其他外部内容。
|
||||
|
||||
必需属性:
|
||||
- topLeftX/topLeftY: 左上角坐标
|
||||
- width/height: 显示尺寸
|
||||
|
||||
可选属性:
|
||||
- id: 唯一标识符
|
||||
- rotation: 旋转角度[0, 360)
|
||||
- flipX/flipY: 水平/垂直翻转
|
||||
- alpha: 不透明度[0,1]
|
||||
|
||||
子元素:
|
||||
- svg: 标准 SVG 根元素, 仅描述嵌入内容, 不承载 Slides 布局属性
|
||||
- reflection: 倒影样式, 无reflection标签代表无倒影, 空reflection标签代表使用默认样式
|
||||
- shadow: 阴影样式, 无shadow标签代表无阴影, 空shadow标签代表使用默认样式
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:any namespace="http://www.w3.org/2000/svg" processContents="lax" minOccurs="1" maxOccurs="1"/>
|
||||
<xs:element name="reflection" type="sml:ReflectionType" minOccurs="0"/>
|
||||
<xs:element name="shadow" type="sml:ShadowType" minOccurs="0"/>
|
||||
</xs:sequence>
|
||||
|
||||
<xs:attribute name="id" type="xs:string" use="optional"/>
|
||||
<xs:attribute name="topLeftX" type="sml:XType" use="required"/>
|
||||
<xs:attribute name="topLeftY" type="sml:YType" use="required"/>
|
||||
<xs:attribute name="width" type="sml:PositiveSize" use="required"/>
|
||||
<xs:attribute name="height" type="sml:PositiveSize" use="required"/>
|
||||
<xs:attribute name="rotation" type="sml:RotationType" use="optional" default="0"/>
|
||||
<xs:attribute name="flipX" type="xs:boolean" use="optional" default="false"/>
|
||||
<xs:attribute name="flipY" type="xs:boolean" use="optional" default="false"/>
|
||||
<xs:attribute name="alpha" type="sml:AlphaType" use="optional" default="1"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<!-- 表格元素 -->
|
||||
<xs:element name="table">
|
||||
<xs:annotation>
|
||||
|
||||
@@ -81,7 +81,7 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
|
||||
**子元素:**
|
||||
|
||||
- `<style>?` - 页面样式,目前可放 `<fill>`
|
||||
- `<data>?` - 页面元素容器,可放 `shape`、`line`、`polyline`、`img`、`table`、`icon`、`chart`、`undefined`
|
||||
- `<data>?` - 页面元素容器,可放 `shape`、`line`、`polyline`、`img`、`table`、`icon`、`embed`、`chart`、`undefined`
|
||||
- `<note>?` - 演讲者备注,内部可放 `<content>`
|
||||
|
||||
这意味着 `<title>`、`<headline>`、`<body>`、`<caption>` 不能直接放在 `<slide>` 下。
|
||||
@@ -355,6 +355,20 @@ XSD 中的 `title`、`headline`、`sub-headline`、`body`、`caption` 主要出
|
||||
|
||||
详细用法见 [slides_xml_schema_definition.xml](slides_xml_schema_definition.xml)。
|
||||
|
||||
### embed
|
||||
|
||||
嵌入内容容器:外层 `<embed>` 承载 Slides 的摆放和效果属性(`topLeftX`/`topLeftY`/`width`/`height` 必填,`rotation`/`flipX`/`flipY`/`alpha` 可选),内层承载外部标准内容。当前内层内容为标准 SVG,`<svg>` 必须使用 `http://www.w3.org/2000/svg` 命名空间,且仅描述嵌入内容本身,不承载 Slides 布局属性。
|
||||
|
||||
```xml
|
||||
<embed topLeftX="80" topLeftY="120" width="200" height="120">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 120">
|
||||
<circle cx="100" cy="60" r="40" fill="rgba(37, 99, 235, 1)"/>
|
||||
</svg>
|
||||
</embed>
|
||||
```
|
||||
|
||||
`<embed>` 直接子元素为一个 `<svg>`(必需),以及可选的 `<reflection>`(倒影)与 `<shadow>`(阴影)。
|
||||
|
||||
## 颜色与样式
|
||||
|
||||
### fill
|
||||
|
||||
@@ -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