@amaster.ai/pi-lark 0.1.2-beta.45 → 0.1.2-beta.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/lark-apps/references/lark-apps-file.md +1 -1
- package/skills/lark-base/SKILL.md +3 -4
- package/skills/lark-base/references/lark-base-form-submit.md +16 -7
- package/skills/lark-doc/references/lark-doc-xml.md +1 -1
- package/skills/lark-drive/references/lark-drive-search.md +1 -0
- package/skills/lark-im/references/card/card-2.0-schema.md +1 -1
- package/skills/lark-im/references/card/lark-im-card-style.md +4 -4
- package/skills/lark-im/references/card/resource/icons.md +14 -0
- package/skills/lark-slides/SKILL.md +2 -2
- package/skills/lark-slides/references/lark-slides-screenshot.md +4 -4
- package/skills/lark-slides/references/troubleshooting.md +1 -1
- package/skills/lark-slides/references/validation-checklist.md +24 -7
- package/skills/lark-slides/references/xml-schema-quick-ref.md +18 -0
- package/skills/lark-slides/scripts/xml_text_overlap_lint.py +831 -56
- package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +781 -33
- package/skills/lark-task/SKILL.md +7 -0
- package/skills/lark-task/references/lark-task-complete.md +6 -2
- package/skills/lark-task/references/lark-task-update.md +6 -2
|
@@ -12,8 +12,8 @@ from pathlib import Path
|
|
|
12
12
|
import xml_text_overlap_lint
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class
|
|
16
|
-
def
|
|
15
|
+
class XmlTextOverlapLintGeometryTest(unittest.TestCase):
|
|
16
|
+
def assertNoXmlTextOverlapLintErrors(self, result: dict, sample_name: str) -> None:
|
|
17
17
|
issue_summaries = []
|
|
18
18
|
for slide in result.get("slides", []):
|
|
19
19
|
for issue in slide.get("issues", []):
|
|
@@ -28,11 +28,6 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
28
28
|
0,
|
|
29
29
|
f"{sample_name} has XML text overlap lint errors:\n" + "\n".join(issue_summaries),
|
|
30
30
|
)
|
|
31
|
-
self.assertEqual(
|
|
32
|
-
result["summary"]["warning_count"],
|
|
33
|
-
0,
|
|
34
|
-
f"{sample_name} has XML text overlap lint warnings:\n" + "\n".join(issue_summaries),
|
|
35
|
-
)
|
|
36
31
|
|
|
37
32
|
def test_cli_suggests_input_flag_for_positional_argument(self) -> None:
|
|
38
33
|
script_path = Path(xml_text_overlap_lint.__file__).resolve()
|
|
@@ -49,7 +44,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
49
44
|
self.assertEqual(completed.stdout, "")
|
|
50
45
|
self.assertEqual(
|
|
51
46
|
completed.stderr,
|
|
52
|
-
f"xml-text-overlap-lint error: unexpected argument: {input_path}
|
|
47
|
+
f"xml-text-overlap-lint error: unexpected argument: {input_path}, need --input\n",
|
|
53
48
|
)
|
|
54
49
|
|
|
55
50
|
def test_xml_text_overlap_lint_accepts_inline_fixture_xml_samples(self) -> None:
|
|
@@ -101,7 +96,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
101
96
|
sample_xml,
|
|
102
97
|
sample_name,
|
|
103
98
|
)
|
|
104
|
-
self.
|
|
99
|
+
self.assertNoXmlTextOverlapLintErrors(result, sample_name)
|
|
105
100
|
|
|
106
101
|
def test_lint_xml_reports_unescaped_ampersand_in_text(self) -> None:
|
|
107
102
|
result = xml_text_overlap_lint.lint_xml(
|
|
@@ -221,7 +216,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
221
216
|
)
|
|
222
217
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
223
218
|
|
|
224
|
-
def
|
|
219
|
+
def test_lint_xml_single_slide_reports_out_of_canvas_and_blank_slide_errors(self) -> None:
|
|
225
220
|
result = xml_text_overlap_lint.lint_xml(
|
|
226
221
|
"""
|
|
227
222
|
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
@@ -235,8 +230,11 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
235
230
|
)
|
|
236
231
|
self.assertEqual(result["slide_size"], {"width": 960, "height": 540})
|
|
237
232
|
self.assertEqual(result["summary"]["slide_count"], 1)
|
|
238
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
239
|
-
self.assertEqual(
|
|
233
|
+
self.assertEqual(result["summary"]["error_count"], 2)
|
|
234
|
+
self.assertEqual(
|
|
235
|
+
[issue["code"] for issue in result["slides"][0]["errors"]],
|
|
236
|
+
["shape_out_of_canvas", "blank_slide"],
|
|
237
|
+
)
|
|
240
238
|
|
|
241
239
|
def test_lint_xml_preserves_presentation_canvas_and_slide_order(self) -> None:
|
|
242
240
|
result = xml_text_overlap_lint.lint_xml(
|
|
@@ -618,8 +616,11 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
618
616
|
)
|
|
619
617
|
self.assertEqual(result["summary"]["error_count"], 1)
|
|
620
618
|
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
621
|
-
|
|
622
|
-
self.assertEqual(
|
|
619
|
+
issue = result["slides"][0]["issues"][0]
|
|
620
|
+
self.assertEqual(issue["code"], "bbox_overlap")
|
|
621
|
+
self.assertEqual(issue["elements"], ["source", "target"])
|
|
622
|
+
self.assertGreater(issue["measurement"]["intersection_area"], 0)
|
|
623
|
+
self.assertIsNotNone(issue.get("hint"))
|
|
623
624
|
|
|
624
625
|
def test_lint_xml_allows_horizontal_text_with_default_wrap(self) -> None:
|
|
625
626
|
result = xml_text_overlap_lint.lint_xml(
|
|
@@ -752,7 +753,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
752
753
|
"第一行\n第二行\n第三行",
|
|
753
754
|
)
|
|
754
755
|
|
|
755
|
-
def
|
|
756
|
+
def test_lint_xml_blocks_template_style_bleed_outside_canvas(self) -> None:
|
|
756
757
|
result = xml_text_overlap_lint.lint_xml(
|
|
757
758
|
"""
|
|
758
759
|
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
@@ -770,8 +771,9 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
770
771
|
</presentation>
|
|
771
772
|
"""
|
|
772
773
|
)
|
|
773
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
774
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
774
775
|
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
776
|
+
self.assertEqual(result["slides"][0]["errors"][0]["code"], "img_out_of_canvas")
|
|
775
777
|
|
|
776
778
|
def test_extract_elements_preserves_supported_element_geometry_order_and_text_metadata(self) -> None:
|
|
777
779
|
elements = xml_text_overlap_lint.extract_elements(
|
|
@@ -805,7 +807,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
805
807
|
self.assertEqual(elements[1]["fontSize"], 28)
|
|
806
808
|
self.assertEqual(elements[1]["text"], "Growth & scale\nFocused execution")
|
|
807
809
|
|
|
808
|
-
def
|
|
810
|
+
def test_lint_xml_blocks_small_out_of_bounds_images(self) -> None:
|
|
809
811
|
result = xml_text_overlap_lint.lint_xml(
|
|
810
812
|
"""
|
|
811
813
|
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
@@ -817,9 +819,10 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
817
819
|
</presentation>
|
|
818
820
|
"""
|
|
819
821
|
)
|
|
820
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
822
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
823
|
+
self.assertEqual(result["slides"][0]["errors"][0]["code"], "img_out_of_canvas")
|
|
821
824
|
|
|
822
|
-
def
|
|
825
|
+
def test_lint_xml_blocks_out_of_canvas_images(self) -> None:
|
|
823
826
|
result = xml_text_overlap_lint.lint_xml(
|
|
824
827
|
"""
|
|
825
828
|
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
@@ -832,9 +835,13 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
832
835
|
</presentation>
|
|
833
836
|
"""
|
|
834
837
|
)
|
|
835
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
838
|
+
self.assertEqual(result["summary"]["error_count"], 2)
|
|
839
|
+
self.assertEqual(
|
|
840
|
+
[issue["code"] for issue in result["slides"][0]["errors"]],
|
|
841
|
+
["img_out_of_canvas", "img_out_of_canvas"],
|
|
842
|
+
)
|
|
836
843
|
|
|
837
|
-
def
|
|
844
|
+
def test_lint_xml_blocks_full_bleed_images_outside_canvas(self) -> None:
|
|
838
845
|
result = xml_text_overlap_lint.lint_xml(
|
|
839
846
|
"""
|
|
840
847
|
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
@@ -846,7 +853,8 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
846
853
|
</presentation>
|
|
847
854
|
"""
|
|
848
855
|
)
|
|
849
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
856
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
857
|
+
self.assertEqual(result["slides"][0]["errors"][0]["code"], "img_out_of_canvas")
|
|
850
858
|
|
|
851
859
|
def test_lint_xml_reports_text_and_chart_out_of_canvas(self) -> None:
|
|
852
860
|
result = xml_text_overlap_lint.lint_xml(
|
|
@@ -863,15 +871,36 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
863
871
|
"""
|
|
864
872
|
)
|
|
865
873
|
issues = result["slides"][0]["issues"]
|
|
866
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
874
|
+
self.assertEqual(result["summary"]["error_count"], 3)
|
|
867
875
|
self.assertEqual(
|
|
868
876
|
[(issue["code"], issue["elements"], issue["overflow"]) for issue in issues],
|
|
869
877
|
[
|
|
870
878
|
("shape_out_of_canvas", ["outside-shape"], {"left": 10, "top": 0, "right": 0, "bottom": 0}),
|
|
879
|
+
("img_out_of_canvas", ["outside-img"], {"left": 0, "top": 20, "right": 0, "bottom": 0}),
|
|
871
880
|
("chart_out_of_canvas", ["outside-chart"], {"left": 0, "top": 0, "right": 40, "bottom": 0}),
|
|
872
881
|
],
|
|
873
882
|
)
|
|
874
883
|
|
|
884
|
+
def test_lint_xml_reports_line_out_of_canvas_with_structured_geometry(self) -> None:
|
|
885
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
886
|
+
"""
|
|
887
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
888
|
+
<data>
|
|
889
|
+
<shape id="body" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
890
|
+
<content fontSize="18"><p>Visible content</p></content>
|
|
891
|
+
</shape>
|
|
892
|
+
<line id="connector" startX="80" startY="120" endX="980" endY="120"/>
|
|
893
|
+
</data>
|
|
894
|
+
</slide>
|
|
895
|
+
"""
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
issue = result["slides"][0]["errors"][0]
|
|
899
|
+
self.assertEqual(issue["code"], "line_out_of_canvas")
|
|
900
|
+
self.assertEqual(issue["element_ids"], ["connector"])
|
|
901
|
+
self.assertEqual(issue["measurement"]["overflow"]["right"], 20)
|
|
902
|
+
self.assertEqual(issue["related_objects"][0]["kind"], "line")
|
|
903
|
+
|
|
875
904
|
def test_lint_xml_uses_rotated_text_and_chart_bounds_for_canvas_validation(self) -> None:
|
|
876
905
|
result = xml_text_overlap_lint.lint_xml(
|
|
877
906
|
"""
|
|
@@ -893,6 +922,27 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
893
922
|
self.assertEqual(issues_by_element["rotated-chart"]["code"], "chart_out_of_canvas")
|
|
894
923
|
self.assertAlmostEqual(issues_by_element["rotated-chart"]["overflow"]["right"], 20.710678, places=5)
|
|
895
924
|
|
|
925
|
+
def test_lint_xml_uses_rotated_bounds_for_rect_and_image_canvas_validation(self) -> None:
|
|
926
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
927
|
+
"""
|
|
928
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
929
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
930
|
+
<data>
|
|
931
|
+
<shape id="rotated-rect" type="rect" topLeftX="0" topLeftY="0" width="100" height="100" rotation="45"/>
|
|
932
|
+
<img id="rotated-image" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45"/>
|
|
933
|
+
</data>
|
|
934
|
+
</slide>
|
|
935
|
+
</presentation>
|
|
936
|
+
"""
|
|
937
|
+
)
|
|
938
|
+
issues_by_element = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
|
|
939
|
+
self.assertEqual(result["summary"]["error_count"], 2)
|
|
940
|
+
self.assertEqual(issues_by_element["rotated-rect"]["code"], "shape_out_of_canvas")
|
|
941
|
+
self.assertAlmostEqual(issues_by_element["rotated-rect"]["overflow"]["left"], 20.710678, places=5)
|
|
942
|
+
self.assertAlmostEqual(issues_by_element["rotated-rect"]["overflow"]["top"], 20.710678, places=5)
|
|
943
|
+
self.assertEqual(issues_by_element["rotated-image"]["code"], "img_out_of_canvas")
|
|
944
|
+
self.assertAlmostEqual(issues_by_element["rotated-image"]["overflow"]["right"], 20.710678, places=5)
|
|
945
|
+
|
|
896
946
|
def test_lint_xml_treats_non_finite_rotations_as_zero(self) -> None:
|
|
897
947
|
result = xml_text_overlap_lint.lint_xml(
|
|
898
948
|
"""
|
|
@@ -1057,9 +1107,8 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
1057
1107
|
)
|
|
1058
1108
|
issues_by_dimension = {issue["dimension"]: issue for issue in result["slides"][0]["issues"]}
|
|
1059
1109
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1060
|
-
self.assertEqual(result["summary"]["warning_count"],
|
|
1061
|
-
self.assertEqual(
|
|
1062
|
-
self.assertEqual(issues_by_dimension["width"]["level"], "info")
|
|
1110
|
+
self.assertEqual(result["summary"]["warning_count"], 2)
|
|
1111
|
+
self.assertEqual(issues_by_dimension["width"]["level"], "warning")
|
|
1063
1112
|
self.assertEqual(issues_by_dimension["width"]["code"], "table_resolved_size_mismatch")
|
|
1064
1113
|
self.assertEqual(issues_by_dimension["width"]["resolved_sizes"], [100, 100, 50])
|
|
1065
1114
|
self.assertEqual(issues_by_dimension["width"]["resolved_size"], 250)
|
|
@@ -1084,7 +1133,6 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
1084
1133
|
)
|
|
1085
1134
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1086
1135
|
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1087
|
-
self.assertEqual(result["summary"]["info_count"], 0)
|
|
1088
1136
|
self.assertEqual(result["slides"][0]["issues"], [])
|
|
1089
1137
|
|
|
1090
1138
|
def test_lint_xml_keeps_resolved_table_sizes_positive_when_target_is_too_small(self) -> None:
|
|
@@ -1153,7 +1201,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
1153
1201
|
}
|
|
1154
1202
|
script_path = Path(xml_text_overlap_lint.__file__).resolve()
|
|
1155
1203
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
1156
|
-
for name, (table_xml,
|
|
1204
|
+
for name, (table_xml, expected_warning_count) in cases.items():
|
|
1157
1205
|
with self.subTest(case=name):
|
|
1158
1206
|
input_path = Path(temp_dir) / f"{name}.xml"
|
|
1159
1207
|
input_path.write_text(
|
|
@@ -1173,10 +1221,9 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
1173
1221
|
result = json.loads(completed.stdout)
|
|
1174
1222
|
self.assertEqual(completed.returncode, 0, completed.stderr)
|
|
1175
1223
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1176
|
-
self.assertEqual(result["summary"]["warning_count"],
|
|
1177
|
-
self.assertEqual(result["summary"]["info_count"], expected_info_count)
|
|
1224
|
+
self.assertEqual(result["summary"]["warning_count"], expected_warning_count)
|
|
1178
1225
|
self.assertTrue(
|
|
1179
|
-
all(issue["level"] == "
|
|
1226
|
+
all(issue["level"] == "warning" for issue in result["slides"][0]["issues"]),
|
|
1180
1227
|
result["slides"][0]["issues"],
|
|
1181
1228
|
)
|
|
1182
1229
|
|
|
@@ -1209,7 +1256,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
1209
1256
|
self.assertEqual(result["slides"][0]["issues"][0]["code"], "bbox_overlap")
|
|
1210
1257
|
|
|
1211
1258
|
|
|
1212
|
-
def
|
|
1259
|
+
def test_lint_xml_reports_vertical_text_image_overlap_as_warning(self) -> None:
|
|
1213
1260
|
result = xml_text_overlap_lint.lint_xml(
|
|
1214
1261
|
"""
|
|
1215
1262
|
<slide xmlns="http://www.larkoffice.com/sml/2.0"><data>
|
|
@@ -1221,8 +1268,709 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
1221
1268
|
"""
|
|
1222
1269
|
)
|
|
1223
1270
|
issue = next(issue for issue in result["slides"][0]["issues"] if issue["code"] == "image_may_cover_vertical_text")
|
|
1224
|
-
self.assertEqual(issue["level"], "
|
|
1271
|
+
self.assertEqual(issue["level"], "warning")
|
|
1272
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
class XmlTextOverlapLintDensityTest(unittest.TestCase):
|
|
1276
|
+
def test_lint_xml_blocks_blank_slide(self) -> None:
|
|
1277
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1278
|
+
"""
|
|
1279
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
1280
|
+
<slide id="content-slide">
|
|
1281
|
+
<data>
|
|
1282
|
+
<shape id="title" type="text" topLeftX="60" topLeftY="60" width="400" height="50">
|
|
1283
|
+
<content fontSize="28"><p>Investment report</p></content>
|
|
1284
|
+
</shape>
|
|
1285
|
+
</data>
|
|
1286
|
+
</slide>
|
|
1287
|
+
<slide id="blank-slide">
|
|
1288
|
+
<style><fill><fillColor color="rgba(255, 255, 255, 1)"/></fill></style>
|
|
1289
|
+
<data/>
|
|
1290
|
+
<note><content/></note>
|
|
1291
|
+
</slide>
|
|
1292
|
+
</presentation>
|
|
1293
|
+
"""
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
self.assertEqual(result["summary"]["slide_count"], 2)
|
|
1297
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1298
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1299
|
+
self.assertEqual(result["summary"]["status"], "blocked")
|
|
1300
|
+
self.assertFalse(result["summary"]["release_ready"])
|
|
1301
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
1302
|
+
self.assertEqual(result["slides"][1]["element_count"], 0)
|
|
1303
|
+
issue = result["slides"][1]["errors"][0]
|
|
1304
|
+
self.assertEqual(issue["level"], "error")
|
|
1305
|
+
self.assertEqual(issue["code"], "blank_slide")
|
|
1306
|
+
self.assertEqual(issue["element_ids"], [])
|
|
1307
|
+
self.assertEqual(issue["rule"]["id"], "blank_slide")
|
|
1308
|
+
self.assertEqual(issue["measurement"]["visible_element_count"], 0)
|
|
1309
|
+
self.assertEqual(issue["related_objects"], [])
|
|
1310
|
+
|
|
1311
|
+
def test_lint_xml_blocks_blank_slide_with_only_transparent_image(self) -> None:
|
|
1312
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1313
|
+
"""
|
|
1314
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1315
|
+
<data>
|
|
1316
|
+
<img id="ghost" topLeftX="60" topLeftY="60" width="200" height="200" alpha="0"/>
|
|
1317
|
+
</data>
|
|
1318
|
+
</slide>
|
|
1319
|
+
"""
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1323
|
+
issue = result["slides"][0]["errors"][0]
|
|
1324
|
+
self.assertEqual(issue["code"], "blank_slide")
|
|
1325
|
+
|
|
1326
|
+
def test_lint_xml_warns_when_large_container_is_mostly_empty(self) -> None:
|
|
1327
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1328
|
+
"""
|
|
1329
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1330
|
+
<data>
|
|
1331
|
+
<shape id="trend-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="370"/>
|
|
1332
|
+
<shape id="trend-title" type="text" topLeftX="515" topLeftY="147" width="380" height="28">
|
|
1333
|
+
<content fontSize="15"><p>Core trends</p></content>
|
|
1334
|
+
</shape>
|
|
1335
|
+
<shape id="trend-copy" type="text" topLeftX="515" topLeftY="177" width="380" height="315">
|
|
1336
|
+
<content fontSize="12"><p>First point</p><p>Second point</p><p>Third point</p></content>
|
|
1337
|
+
</shape>
|
|
1338
|
+
</data>
|
|
1339
|
+
</slide>
|
|
1340
|
+
"""
|
|
1341
|
+
)
|
|
1342
|
+
|
|
1343
|
+
issue = result["slides"][0]["issues"][0]
|
|
1344
|
+
self.assertEqual(issue["code"], "sparse_container_content")
|
|
1345
|
+
self.assertEqual(issue["target"]["container_id"], "trend-card")
|
|
1346
|
+
self.assertEqual(issue["target"], {
|
|
1347
|
+
"slide_number": 1,
|
|
1348
|
+
"container_id": "trend-card",
|
|
1349
|
+
"container_type": "rect",
|
|
1350
|
+
"bbox": {"x": 500, "y": 135, "width": 410, "height": 370},
|
|
1351
|
+
})
|
|
1352
|
+
self.assertLess(issue["measurement"]["content_coverage_ratio"], 0.15)
|
|
1353
|
+
self.assertEqual(issue["rule"], {
|
|
1354
|
+
"name": "large_container_visible_content_coverage",
|
|
1355
|
+
"threshold": 0.15,
|
|
1356
|
+
"comparison": "content_coverage_ratio < threshold",
|
|
1357
|
+
"id": "sparse_container_content",
|
|
1358
|
+
})
|
|
1359
|
+
self.assertEqual(issue["measurement"]["container_area"], 151700)
|
|
1360
|
+
self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.032)
|
|
1361
|
+
self.assertEqual(issue["elements"], ["trend-card", "trend-title", "trend-copy"])
|
|
1362
|
+
self.assertEqual(issue["element_ids"], ["trend-card", "trend-title", "trend-copy"])
|
|
1363
|
+
self.assertEqual(
|
|
1364
|
+
[obj["element_id"] for obj in issue["related_objects"]],
|
|
1365
|
+
["trend-card", "trend-title", "trend-copy"],
|
|
1366
|
+
)
|
|
1367
|
+
self.assertEqual(result["slides"][0]["status"], "needs_screenshot_review")
|
|
1368
|
+
self.assertEqual(result["slides"][0]["warnings"], result["slides"][0]["issues"])
|
|
1369
|
+
|
|
1370
|
+
def test_lint_xml_warns_for_sparse_short_cards(self) -> None:
|
|
1371
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1372
|
+
"""
|
|
1373
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1374
|
+
<data>
|
|
1375
|
+
<shape id="card-1" type="rect" topLeftX="60" topLeftY="180" width="400" height="105"/>
|
|
1376
|
+
<shape id="text-1" type="text" topLeftX="80" topLeftY="220" width="360" height="30">
|
|
1377
|
+
<content fontSize="14"><p>期待认识大家</p></content>
|
|
1378
|
+
</shape>
|
|
1379
|
+
<shape id="card-2" type="rect" topLeftX="490" topLeftY="180" width="400" height="105"/>
|
|
1380
|
+
<shape id="text-2" type="text" topLeftX="510" topLeftY="220" width="360" height="30">
|
|
1381
|
+
<content fontSize="14"><p>化学一起讨论</p></content>
|
|
1382
|
+
</shape>
|
|
1383
|
+
<shape id="card-3" type="rect" topLeftX="60" topLeftY="310" width="400" height="105"/>
|
|
1384
|
+
<shape id="text-3" type="text" topLeftX="80" topLeftY="350" width="360" height="30">
|
|
1385
|
+
<content fontSize="14"><p>吉他随时交流</p></content>
|
|
1386
|
+
</shape>
|
|
1387
|
+
<shape id="card-4" type="rect" topLeftX="490" topLeftY="310" width="400" height="105"/>
|
|
1388
|
+
<shape id="text-4" type="text" topLeftX="510" topLeftY="350" width="360" height="30">
|
|
1389
|
+
<content fontSize="14"><p>共度美好四年</p></content>
|
|
1390
|
+
</shape>
|
|
1391
|
+
</data>
|
|
1392
|
+
</slide>
|
|
1393
|
+
"""
|
|
1394
|
+
)
|
|
1395
|
+
|
|
1396
|
+
container_issues = [
|
|
1397
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
1398
|
+
]
|
|
1399
|
+
self.assertEqual(
|
|
1400
|
+
[issue["target"]["container_id"] for issue in container_issues],
|
|
1401
|
+
["card-1", "card-2", "card-3", "card-4"],
|
|
1402
|
+
)
|
|
1403
|
+
self.assertTrue(all(issue["target"]["bbox"]["height"] == 105 for issue in container_issues))
|
|
1404
|
+
self.assertTrue(all(issue["measurement"]["content_coverage_ratio"] < 0.15 for issue in container_issues))
|
|
1405
|
+
self.assertEqual(
|
|
1406
|
+
[issue["code"] for issue in result["slides"][0]["issues"]],
|
|
1407
|
+
[
|
|
1408
|
+
"sparse_container_content",
|
|
1409
|
+
"sparse_container_content",
|
|
1410
|
+
"sparse_container_content",
|
|
1411
|
+
"sparse_container_content",
|
|
1412
|
+
"sparse_slide_content",
|
|
1413
|
+
],
|
|
1414
|
+
)
|
|
1415
|
+
|
|
1416
|
+
def test_lint_xml_warns_when_whole_slide_has_too_little_effective_content(self) -> None:
|
|
1417
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1418
|
+
"""
|
|
1419
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1420
|
+
<data>
|
|
1421
|
+
<shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540"/>
|
|
1422
|
+
<shape id="text-1" type="text" topLeftX="60" topLeftY="80" width="200" height="30">
|
|
1423
|
+
<content fontSize="14"><p>One short line</p></content>
|
|
1424
|
+
</shape>
|
|
1425
|
+
<shape id="text-2" type="text" topLeftX="500" topLeftY="180" width="200" height="30">
|
|
1426
|
+
<content fontSize="14"><p>Another line</p></content>
|
|
1427
|
+
</shape>
|
|
1428
|
+
<shape id="text-3" type="text" topLeftX="60" topLeftY="310" width="200" height="30">
|
|
1429
|
+
<content fontSize="14"><p>Third line</p></content>
|
|
1430
|
+
</shape>
|
|
1431
|
+
<shape id="text-4" type="text" topLeftX="500" topLeftY="410" width="200" height="30">
|
|
1432
|
+
<content fontSize="14"><p>Fourth line</p></content>
|
|
1433
|
+
</shape>
|
|
1434
|
+
</data>
|
|
1435
|
+
</slide>
|
|
1436
|
+
"""
|
|
1437
|
+
)
|
|
1438
|
+
|
|
1439
|
+
issues = [issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_slide_content"]
|
|
1440
|
+
self.assertEqual(len(issues), 1)
|
|
1441
|
+
issue = issues[0]
|
|
1442
|
+
self.assertEqual(issue["target"]["bbox"], {"x": 0, "y": 0, "width": 960, "height": 540})
|
|
1443
|
+
self.assertEqual(issue["rule"]["threshold"], 0.035)
|
|
1444
|
+
self.assertLess(issue["measurement"]["content_coverage_ratio"], 0.035)
|
|
1445
|
+
self.assertEqual(issue["measurement"]["content_element_count"], 4)
|
|
1446
|
+
self.assertNotIn("background", issue["elements"])
|
|
1447
|
+
|
|
1448
|
+
def test_lint_xml_ignores_isolated_short_layout_bar(self) -> None:
|
|
1449
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1450
|
+
"""
|
|
1451
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1452
|
+
<data>
|
|
1453
|
+
<shape id="summary-bar" type="rect" topLeftX="52" topLeftY="82" width="856" height="105"/>
|
|
1454
|
+
<shape id="summary" type="text" topLeftX="72" topLeftY="115" width="816" height="30">
|
|
1455
|
+
<content fontSize="14"><p>One concise summary</p></content>
|
|
1456
|
+
</shape>
|
|
1457
|
+
</data>
|
|
1458
|
+
</slide>
|
|
1459
|
+
"""
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
1463
|
+
|
|
1464
|
+
def test_lint_xml_counts_rect_own_content_as_visible_content(self) -> None:
|
|
1465
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1466
|
+
"""
|
|
1467
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1468
|
+
<data>
|
|
1469
|
+
<shape id="load-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
1470
|
+
<content fontSize="18">
|
|
1471
|
+
<p>被吊物</p>
|
|
1472
|
+
<p><span fontSize="36">32.0 t</span></p>
|
|
1473
|
+
<p>钢结构模块</p>
|
|
1474
|
+
</content>
|
|
1475
|
+
</shape>
|
|
1476
|
+
</data>
|
|
1477
|
+
</slide>
|
|
1478
|
+
"""
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
1482
|
+
|
|
1483
|
+
def test_lint_xml_reports_nonzero_coverage_for_rect_own_content_reproduction(self) -> None:
|
|
1484
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1485
|
+
"""
|
|
1486
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1487
|
+
<data>
|
|
1488
|
+
<shape id="load-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
1489
|
+
<content fontSize="18">
|
|
1490
|
+
<p>被吊物</p>
|
|
1491
|
+
<p>32.0 t</p>
|
|
1492
|
+
<p>钢结构模块</p>
|
|
1493
|
+
</content>
|
|
1494
|
+
</shape>
|
|
1495
|
+
</data>
|
|
1496
|
+
</slide>
|
|
1497
|
+
"""
|
|
1498
|
+
)
|
|
1499
|
+
|
|
1500
|
+
issue = result["slides"][0]["issues"][0]
|
|
1501
|
+
self.assertGreater(issue["measurement"]["visible_content_area"], 0)
|
|
1502
|
+
self.assertEqual(issue["measurement"]["content_element_count"], 1)
|
|
1503
|
+
self.assertGreater(issue["measurement"]["content_coverage_ratio"], 0)
|
|
1504
|
+
|
|
1505
|
+
def test_lint_xml_still_warns_for_sparse_rect_own_content(self) -> None:
|
|
1506
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1507
|
+
"""
|
|
1508
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1509
|
+
<data>
|
|
1510
|
+
<shape id="sparse-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
1511
|
+
<content fontSize="12"><p>A</p></content>
|
|
1512
|
+
</shape>
|
|
1513
|
+
</data>
|
|
1514
|
+
</slide>
|
|
1515
|
+
"""
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
issue = result["slides"][0]["issues"][0]
|
|
1519
|
+
self.assertEqual(issue["target"]["container_id"], "sparse-card")
|
|
1520
|
+
self.assertGreater(issue["measurement"]["visible_content_area"], 0)
|
|
1521
|
+
self.assertEqual(issue["measurement"]["content_element_count"], 1)
|
|
1522
|
+
self.assertEqual(issue["elements"], ["sparse-card"])
|
|
1523
|
+
|
|
1524
|
+
def test_lint_xml_unions_rect_own_content_with_child_content(self) -> None:
|
|
1525
|
+
self_only = xml_text_overlap_lint.lint_xml(
|
|
1526
|
+
"""
|
|
1527
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1528
|
+
<data>
|
|
1529
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
1530
|
+
<content fontSize="12"><p>A</p></content>
|
|
1531
|
+
</shape>
|
|
1532
|
+
</data>
|
|
1533
|
+
</slide>
|
|
1534
|
+
"""
|
|
1535
|
+
)
|
|
1536
|
+
with_overlapping_child = xml_text_overlap_lint.lint_xml(
|
|
1537
|
+
"""
|
|
1538
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1539
|
+
<data>
|
|
1540
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
1541
|
+
<content fontSize="12"><p>A</p></content>
|
|
1542
|
+
</shape>
|
|
1543
|
+
<shape id="child" type="text" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
1544
|
+
<content fontSize="12"><p>A</p></content>
|
|
1545
|
+
</shape>
|
|
1546
|
+
</data>
|
|
1547
|
+
</slide>
|
|
1548
|
+
"""
|
|
1549
|
+
)
|
|
1550
|
+
|
|
1551
|
+
self_issue = self_only["slides"][0]["issues"][0]
|
|
1552
|
+
mixed_issue = with_overlapping_child["slides"][0]["issues"][0]
|
|
1553
|
+
self.assertEqual(
|
|
1554
|
+
mixed_issue["measurement"]["visible_content_area"],
|
|
1555
|
+
self_issue["measurement"]["visible_content_area"],
|
|
1556
|
+
)
|
|
1557
|
+
self.assertEqual(mixed_issue["measurement"]["content_element_count"], 2)
|
|
1558
|
+
|
|
1559
|
+
def test_extract_density_elements_reads_nested_font_size_from_rect_content(self) -> None:
|
|
1560
|
+
elements = xml_text_overlap_lint.extract_density_elements(
|
|
1561
|
+
"""
|
|
1562
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1563
|
+
<data>
|
|
1564
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
1565
|
+
<content fontSize="12"><p><span fontSize="36">32.0 t</span></p></content>
|
|
1566
|
+
</shape>
|
|
1567
|
+
</data>
|
|
1568
|
+
</slide>
|
|
1569
|
+
"""
|
|
1570
|
+
)
|
|
1571
|
+
|
|
1572
|
+
self.assertEqual(elements[0]["fontSize"], 36)
|
|
1573
|
+
|
|
1574
|
+
def test_extract_density_elements_does_not_attach_following_text_to_self_closing_rect(self) -> None:
|
|
1575
|
+
elements = xml_text_overlap_lint.extract_density_elements(
|
|
1576
|
+
"""
|
|
1577
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1578
|
+
<data>
|
|
1579
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184"/>
|
|
1580
|
+
<shape id="title" type="text" topLeftX="80" topLeftY="160" width="180" height="30">
|
|
1581
|
+
<content fontSize="18"><p>Following title</p></content>
|
|
1582
|
+
</shape>
|
|
1583
|
+
</data>
|
|
1584
|
+
</slide>
|
|
1585
|
+
"""
|
|
1586
|
+
)
|
|
1587
|
+
|
|
1588
|
+
self.assertEqual(elements[0]["text"], "")
|
|
1589
|
+
self.assertEqual(elements[1]["text"], "Following title")
|
|
1590
|
+
|
|
1591
|
+
def test_lint_xml_allows_container_with_large_visual_child(self) -> None:
|
|
1592
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1593
|
+
"""
|
|
1594
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1595
|
+
<data>
|
|
1596
|
+
<shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
|
|
1597
|
+
<chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220"/>
|
|
1598
|
+
</data>
|
|
1599
|
+
</slide>
|
|
1600
|
+
"""
|
|
1601
|
+
)
|
|
1602
|
+
|
|
1603
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1604
|
+
|
|
1605
|
+
def test_lint_xml_does_not_let_transparent_visual_child_suppress_sparse_warning(self) -> None:
|
|
1606
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1607
|
+
"""
|
|
1608
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1609
|
+
<data>
|
|
1610
|
+
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
|
|
1611
|
+
<content fontSize="20"><p>Section title</p></content>
|
|
1612
|
+
</shape>
|
|
1613
|
+
<shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
|
|
1614
|
+
<chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220" alpha="0"/>
|
|
1615
|
+
</data>
|
|
1616
|
+
</slide>
|
|
1617
|
+
"""
|
|
1618
|
+
)
|
|
1619
|
+
|
|
1620
|
+
issue = next(
|
|
1621
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
1622
|
+
)
|
|
1623
|
+
self.assertEqual(issue["target"]["container_id"], "chart-card")
|
|
1624
|
+
|
|
1625
|
+
def test_lint_xml_warns_for_small_empty_visual_placeholder_cards(self) -> None:
|
|
1626
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1627
|
+
"""
|
|
1628
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1629
|
+
<data>
|
|
1630
|
+
<shape id="letter-placeholder" type="rect" topLeftX="520" topLeftY="180" width="200" height="200"/>
|
|
1631
|
+
<shape id="letter" type="text" topLeftX="540" topLeftY="250" width="160" height="70">
|
|
1632
|
+
<content fontSize="46"><p>Z</p></content>
|
|
1633
|
+
</shape>
|
|
1634
|
+
<shape id="empty-placeholder" type="rect" topLeftX="744" topLeftY="180" width="144" height="200"/>
|
|
1635
|
+
</data>
|
|
1636
|
+
</slide>
|
|
1637
|
+
"""
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
issues = result["slides"][0]["issues"]
|
|
1641
|
+
self.assertEqual(
|
|
1642
|
+
[issue["target"]["container_id"] for issue in issues],
|
|
1643
|
+
["letter-placeholder", "empty-placeholder"],
|
|
1644
|
+
)
|
|
1645
|
+
self.assertEqual(issues[1]["measurement"]["content_element_count"], 0)
|
|
1646
|
+
|
|
1647
|
+
def test_lint_xml_applies_global_threshold_to_normal_text_card(self) -> None:
|
|
1648
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1649
|
+
"""
|
|
1650
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1651
|
+
<data>
|
|
1652
|
+
<shape id="card" type="rect" topLeftX="70" topLeftY="184" width="260" height="288"/>
|
|
1653
|
+
<shape id="title" type="text" topLeftX="90" topLeftY="215" width="220" height="30">
|
|
1654
|
+
<content fontSize="18"><p>梦境与现实</p></content>
|
|
1655
|
+
</shape>
|
|
1656
|
+
<shape id="copy" type="text" topLeftX="90" topLeftY="330" width="220" height="70">
|
|
1657
|
+
<content fontSize="13"><p>边界溶解,逻辑失效。观众被拽入潜意识的迷宫。</p></content>
|
|
1658
|
+
</shape>
|
|
1659
|
+
</data>
|
|
1660
|
+
</slide>
|
|
1661
|
+
"""
|
|
1662
|
+
)
|
|
1663
|
+
|
|
1664
|
+
issue = result["slides"][0]["issues"][0]
|
|
1665
|
+
self.assertEqual(issue["target"]["container_id"], "card")
|
|
1666
|
+
self.assertEqual(issue["rule"]["threshold"], 0.15)
|
|
1667
|
+
|
|
1668
|
+
def test_lint_xml_allows_image_overlay_rect(self) -> None:
|
|
1669
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1670
|
+
"""
|
|
1671
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1672
|
+
<data>
|
|
1673
|
+
<img id="hero" topLeftX="560" topLeftY="0" width="400" height="540"/>
|
|
1674
|
+
<shape id="tint" type="rect" topLeftX="560" topLeftY="0" width="400" height="540"/>
|
|
1675
|
+
</data>
|
|
1676
|
+
</slide>
|
|
1677
|
+
"""
|
|
1678
|
+
)
|
|
1679
|
+
|
|
1680
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1681
|
+
|
|
1682
|
+
def test_lint_xml_does_not_let_transparent_image_overlay_suppress_sparse_warning(self) -> None:
|
|
1683
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1684
|
+
"""
|
|
1685
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1686
|
+
<data>
|
|
1687
|
+
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
|
|
1688
|
+
<content fontSize="20"><p>Section title</p></content>
|
|
1689
|
+
</shape>
|
|
1690
|
+
<shape id="card" type="rect" topLeftX="330" topLeftY="120" width="300" height="300"/>
|
|
1691
|
+
<img id="ghost-overlay" topLeftX="330" topLeftY="120" width="300" height="300" alpha="0"/>
|
|
1692
|
+
</data>
|
|
1693
|
+
</slide>
|
|
1694
|
+
"""
|
|
1695
|
+
)
|
|
1696
|
+
|
|
1697
|
+
issue = next(
|
|
1698
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
1699
|
+
)
|
|
1700
|
+
self.assertEqual(issue["target"]["container_id"], "card")
|
|
1701
|
+
|
|
1702
|
+
def test_lint_xml_allows_edge_spanning_layout_panel_and_nested_decoration(self) -> None:
|
|
1703
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1704
|
+
"""
|
|
1705
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1706
|
+
<data>
|
|
1707
|
+
<shape id="panel" type="rect" topLeftX="600" topLeftY="0" width="360" height="540"/>
|
|
1708
|
+
<shape id="decoration" type="rect" topLeftX="660" topLeftY="150" width="240" height="240"/>
|
|
1709
|
+
</data>
|
|
1710
|
+
</slide>
|
|
1711
|
+
"""
|
|
1712
|
+
)
|
|
1713
|
+
|
|
1714
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1715
|
+
|
|
1716
|
+
def test_lint_xml_counts_icons_as_visible_content(self) -> None:
|
|
1717
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1718
|
+
"""
|
|
1719
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1720
|
+
<data>
|
|
1721
|
+
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="320" height="240"/>
|
|
1722
|
+
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="180" height="180">
|
|
1723
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
1724
|
+
</icon>
|
|
1725
|
+
</data>
|
|
1726
|
+
</slide>
|
|
1727
|
+
"""
|
|
1728
|
+
)
|
|
1729
|
+
|
|
1730
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1731
|
+
|
|
1732
|
+
def test_lint_xml_does_not_count_transparent_icon_as_visible_content(self) -> None:
|
|
1733
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1734
|
+
"""
|
|
1735
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1736
|
+
<data>
|
|
1737
|
+
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
|
|
1738
|
+
<content fontSize="20"><p>Section title</p></content>
|
|
1739
|
+
</shape>
|
|
1740
|
+
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="320" height="240"/>
|
|
1741
|
+
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="180" height="180" alpha="0">
|
|
1742
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
1743
|
+
</icon>
|
|
1744
|
+
</data>
|
|
1745
|
+
</slide>
|
|
1746
|
+
"""
|
|
1747
|
+
)
|
|
1748
|
+
|
|
1749
|
+
issue = next(
|
|
1750
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
1751
|
+
)
|
|
1752
|
+
self.assertEqual(issue["target"]["container_id"], "card")
|
|
1753
|
+
self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0)
|
|
1754
|
+
|
|
1755
|
+
def test_lint_xml_warns_when_coverage_is_below_global_threshold(self) -> None:
|
|
1756
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1757
|
+
"""
|
|
1758
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1759
|
+
<data>
|
|
1760
|
+
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="200" height="200"/>
|
|
1761
|
+
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="70" height="70">
|
|
1762
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
1763
|
+
</icon>
|
|
1764
|
+
</data>
|
|
1765
|
+
</slide>
|
|
1766
|
+
"""
|
|
1767
|
+
)
|
|
1768
|
+
|
|
1769
|
+
issue = result["slides"][0]["issues"][0]
|
|
1770
|
+
self.assertEqual(issue["target"]["container_id"], "card")
|
|
1771
|
+
self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.122)
|
|
1772
|
+
self.assertEqual(issue["rule"]["threshold"], 0.15)
|
|
1773
|
+
|
|
1774
|
+
def test_lint_xml_allows_quarter_coverage_under_lower_threshold(self) -> None:
|
|
1775
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1776
|
+
"""
|
|
1777
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1778
|
+
<data>
|
|
1779
|
+
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="200" height="200"/>
|
|
1780
|
+
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="100" height="100">
|
|
1781
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
1782
|
+
</icon>
|
|
1783
|
+
</data>
|
|
1784
|
+
</slide>
|
|
1785
|
+
"""
|
|
1786
|
+
)
|
|
1787
|
+
|
|
1788
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
1789
|
+
|
|
1790
|
+
def test_lint_xml_allows_large_metric_card_above_lower_threshold(self) -> None:
|
|
1791
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1792
|
+
"""
|
|
1793
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1794
|
+
<data>
|
|
1795
|
+
<shape id="metric-card" type="rect" topLeftX="80" topLeftY="140" width="360" height="300"/>
|
|
1796
|
+
<shape id="metric" type="text" topLeftX="104" topLeftY="190" width="340" height="90">
|
|
1797
|
+
<content fontSize="12.4"><p><strong><span fontSize="62">400</span></strong>+ 项</p></content>
|
|
1798
|
+
</shape>
|
|
1799
|
+
</data>
|
|
1800
|
+
</slide>
|
|
1801
|
+
"""
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
1805
|
+
|
|
1806
|
+
def test_lint_xml_does_not_report_blank_slide_for_line_only_content(self) -> None:
|
|
1807
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1808
|
+
"""
|
|
1809
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1810
|
+
<data>
|
|
1811
|
+
<line id="l1" startX="100" startY="100" endX="800" endY="100"/>
|
|
1812
|
+
<line id="l2" startX="100" startY="200" endX="800" endY="200"/>
|
|
1813
|
+
<line id="l3" startX="100" startY="300" endX="800" endY="300"/>
|
|
1814
|
+
<line id="l4" startX="100" startY="400" endX="800" endY="400"/>
|
|
1815
|
+
</data>
|
|
1816
|
+
</slide>
|
|
1817
|
+
"""
|
|
1818
|
+
)
|
|
1819
|
+
|
|
1820
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1821
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
1822
|
+
self.assertNotIn("blank_slide", codes)
|
|
1823
|
+
|
|
1824
|
+
def test_lint_xml_reports_bbox_overlap_measurement_from_decision_time_visual_bbox(self) -> None:
|
|
1825
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1826
|
+
"""
|
|
1827
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1828
|
+
<data>
|
|
1829
|
+
<shape id="left" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
1830
|
+
<content fontSize="14"><p>overlap text <span fontSize="96">big</span></p></content>
|
|
1831
|
+
</shape>
|
|
1832
|
+
<shape id="right" type="text" topLeftX="80" topLeftY="80" width="300" height="80">
|
|
1833
|
+
<content fontSize="14"><p>other overlap text</p></content>
|
|
1834
|
+
</shape>
|
|
1835
|
+
</data>
|
|
1836
|
+
</slide>
|
|
1837
|
+
"""
|
|
1838
|
+
)
|
|
1839
|
+
|
|
1840
|
+
issue = result["slides"][0]["issues"][0]
|
|
1841
|
+
self.assertEqual(issue["code"], "bbox_overlap")
|
|
1842
|
+
# Must match the visual bbox that should_flag_overlap actually decided with (fontSize=14
|
|
1843
|
+
# from extract_elements), not the fontSize=96 max-descendant value that
|
|
1844
|
+
# extract_density_elements computes for the same "left" element id.
|
|
1845
|
+
self.assertEqual(issue["measurement"]["intersection_width"], 117.04)
|
|
1846
|
+
self.assertEqual(issue["measurement"]["intersection_height"], 6.8)
|
|
1847
|
+
self.assertEqual(issue["measurement"]["intersection_area"], 795.872)
|
|
1848
|
+
|
|
1849
|
+
def test_has_similar_short_card_peer_excludes_the_element_itself(self) -> None:
|
|
1850
|
+
card_a = {"kind": "shape", "type": "rect", "x": 0, "y": 0, "width": 300, "height": 100}
|
|
1851
|
+
card_b = {"kind": "shape", "type": "rect", "x": 400, "y": 0, "width": 300, "height": 100}
|
|
1852
|
+
card_c = {"kind": "shape", "type": "rect", "x": 0, "y": 200, "width": 300, "height": 100}
|
|
1853
|
+
|
|
1854
|
+
self.assertFalse(
|
|
1855
|
+
xml_text_overlap_lint.has_similar_short_card_peer(card_a, [card_a, card_b])
|
|
1856
|
+
)
|
|
1857
|
+
self.assertTrue(
|
|
1858
|
+
xml_text_overlap_lint.has_similar_short_card_peer(card_a, [card_a, card_b, card_c])
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
def test_lint_xml_reports_schema_version_2_for_sparse_issues(self) -> None:
|
|
1862
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1863
|
+
"""
|
|
1864
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1865
|
+
<data>
|
|
1866
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184"/>
|
|
1867
|
+
</data>
|
|
1868
|
+
</slide>
|
|
1869
|
+
"""
|
|
1870
|
+
)
|
|
1871
|
+
|
|
1872
|
+
issue = next(
|
|
1873
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
1874
|
+
)
|
|
1875
|
+
self.assertEqual(issue["schema_version"], "2.0")
|
|
1876
|
+
|
|
1877
|
+
def test_lint_xml_does_not_report_blank_slide_for_textless_decorative_shapes(self) -> None:
|
|
1878
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1879
|
+
"""
|
|
1880
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1881
|
+
<data>
|
|
1882
|
+
<shape id="deco1" type="ellipse" topLeftX="60" topLeftY="60" width="300" height="300">
|
|
1883
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
1884
|
+
</shape>
|
|
1885
|
+
<shape id="deco2" type="triangle" topLeftX="500" topLeftY="200" width="200" height="200">
|
|
1886
|
+
<fill><fillColor color="rgba(220, 38, 38, 1)"/></fill>
|
|
1887
|
+
</shape>
|
|
1888
|
+
</data>
|
|
1889
|
+
</slide>
|
|
1890
|
+
"""
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1225
1893
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1894
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
1895
|
+
self.assertNotIn("blank_slide", codes)
|
|
1896
|
+
|
|
1897
|
+
def test_lint_xml_still_warns_for_sparse_slide_content_despite_full_bleed_background(self) -> None:
|
|
1898
|
+
# A plain textless shape now counts as "not blank" (see the test above), but a
|
|
1899
|
+
# full-bleed background rect must still NOT count toward sparse_slide_content's
|
|
1900
|
+
# meaningful-content coverage ratio -- otherwise every slide with a background would
|
|
1901
|
+
# trivially "pass" that density check.
|
|
1902
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1903
|
+
"""
|
|
1904
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1905
|
+
<data>
|
|
1906
|
+
<shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540"/>
|
|
1907
|
+
<shape id="text-1" type="text" topLeftX="60" topLeftY="80" width="200" height="30">
|
|
1908
|
+
<content fontSize="14"><p>One short line</p></content>
|
|
1909
|
+
</shape>
|
|
1910
|
+
<shape id="text-2" type="text" topLeftX="500" topLeftY="180" width="200" height="30">
|
|
1911
|
+
<content fontSize="14"><p>Another line</p></content>
|
|
1912
|
+
</shape>
|
|
1913
|
+
<shape id="text-3" type="text" topLeftX="60" topLeftY="310" width="200" height="30">
|
|
1914
|
+
<content fontSize="14"><p>Third line</p></content>
|
|
1915
|
+
</shape>
|
|
1916
|
+
<shape id="text-4" type="text" topLeftX="500" topLeftY="410" width="200" height="30">
|
|
1917
|
+
<content fontSize="14"><p>Fourth line</p></content>
|
|
1918
|
+
</shape>
|
|
1919
|
+
</data>
|
|
1920
|
+
</slide>
|
|
1921
|
+
"""
|
|
1922
|
+
)
|
|
1923
|
+
|
|
1924
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
1925
|
+
self.assertIn("sparse_slide_content", codes)
|
|
1926
|
+
|
|
1927
|
+
def test_lint_xml_accepts_whitespace_around_attribute_equals_sign(self) -> None:
|
|
1928
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1929
|
+
"""
|
|
1930
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1931
|
+
<data>
|
|
1932
|
+
<shape id="visible" type="text" topLeftX = "80" topLeftY = "80" width = "300" height = "60">
|
|
1933
|
+
<content><p>hello</p></content>
|
|
1934
|
+
</shape>
|
|
1935
|
+
</data>
|
|
1936
|
+
</slide>
|
|
1937
|
+
"""
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
self.assertEqual(result["slides"][0]["element_count"], 1)
|
|
1941
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
1942
|
+
self.assertNotIn("blank_slide", codes)
|
|
1943
|
+
|
|
1944
|
+
def test_lint_xml_reports_blank_slide_for_full_canvas_background_only(self) -> None:
|
|
1945
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1946
|
+
"""
|
|
1947
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1948
|
+
<data>
|
|
1949
|
+
<shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540">
|
|
1950
|
+
<fill><fillColor color="rgba(240, 235, 220, 1)"/></fill>
|
|
1951
|
+
</shape>
|
|
1952
|
+
</data>
|
|
1953
|
+
</slide>
|
|
1954
|
+
"""
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
1958
|
+
self.assertIn("blank_slide", codes)
|
|
1959
|
+
|
|
1960
|
+
def test_has_similar_short_card_peer_ignores_invisible_peers(self) -> None:
|
|
1961
|
+
visible_card = {"kind": "shape", "type": "rect", "x": 0, "y": 0, "width": 300, "height": 100}
|
|
1962
|
+
ghost_1 = {
|
|
1963
|
+
"kind": "shape", "type": "rect", "x": 400, "y": 0, "width": 300, "height": 100, "alpha": 0,
|
|
1964
|
+
}
|
|
1965
|
+
ghost_2 = {
|
|
1966
|
+
"kind": "shape", "type": "rect", "x": 800, "y": 0, "width": 300, "height": 100, "alpha": 0,
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
self.assertFalse(
|
|
1970
|
+
xml_text_overlap_lint.has_similar_short_card_peer(
|
|
1971
|
+
visible_card, [visible_card, ghost_1, ghost_2]
|
|
1972
|
+
)
|
|
1973
|
+
)
|
|
1226
1974
|
|
|
1227
1975
|
|
|
1228
1976
|
if __name__ == "__main__":
|