@amaster.ai/pi-lark 0.1.2-beta.47 → 0.1.2-beta.48

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.
Files changed (37) hide show
  1. package/README.md +5 -1
  2. package/dist/config.d.ts +1 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +2 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/package.json +3 -3
  10. package/skills/lark-base/SKILL.md +5 -3
  11. package/skills/lark-base/references/lark-base-filter-condition.md +179 -0
  12. package/skills/lark-base/references/lark-base-form-questions-create.md +34 -4
  13. package/skills/lark-base/references/lark-base-form-questions-update.md +73 -20
  14. package/skills/lark-base/references/lark-base-view-set-filter.md +11 -137
  15. package/skills/lark-calendar/SKILL.md +12 -6
  16. package/skills/lark-calendar/references/lark-calendar-create.md +6 -6
  17. package/skills/lark-calendar/references/lark-calendar-room-find.md +2 -1
  18. package/skills/lark-calendar/references/lark-calendar-schedule-clear-time.md +1 -0
  19. package/skills/lark-calendar/references/lark-calendar-suggestion.md +1 -1
  20. package/skills/lark-calendar/references/lark-calendar-update.md +7 -4
  21. package/skills/lark-contact/SKILL.md +18 -2
  22. package/skills/lark-contact/references/lark-contact-search-bot.md +60 -0
  23. package/skills/lark-drive/SKILL.md +5 -1
  24. package/skills/lark-drive/references/lark-drive-member-list.md +65 -0
  25. package/skills/lark-drive/references/lark-drive-permission-get-setting.md +48 -0
  26. package/skills/lark-drive/references/lark-drive-search.md +6 -1
  27. package/skills/lark-drive/references/lark-drive-secure-label.md +1 -1
  28. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-commands.md +38 -8
  29. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-outputs.md +10 -10
  30. package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +22 -20
  31. package/skills/lark-slides/SKILL.md +16 -26
  32. package/skills/lark-slides/references/lark-slides-create.md +14 -5
  33. package/skills/lark-slides/references/slides_xml_schema_definition.xml +491 -31
  34. package/skills/lark-slides/references/xml-schema-quick-ref.md +39 -0
  35. package/skills/lark-slides/scripts/xml_text_overlap_lint.py +424 -32
  36. package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +840 -58
  37. package/skills/lark-task/references/lark-task-create.md +9 -0
@@ -343,6 +343,26 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
343
343
  self.assertEqual(result["summary"]["error_count"], 0)
344
344
  self.assertNotIn("issues", result)
345
345
 
346
+ def test_lint_xml_ignores_chart_parsed_values_roundtrip_tag(self) -> None:
347
+ result = xml_text_overlap_lint.lint_xml(
348
+ """
349
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
350
+ <data>
351
+ <chart topLeftX="80" topLeftY="80" width="300" height="160">
352
+ <chartData>
353
+ <chartField>
354
+ <chartParsedValues>Africa</chartParsedValues>
355
+ </chartField>
356
+ </chartData>
357
+ </chart>
358
+ </data>
359
+ </slide>
360
+ """
361
+ )
362
+
363
+ self.assertEqual(result["summary"]["error_count"], 0)
364
+ self.assertNotIn("issues", result)
365
+
346
366
  def test_lint_xml_limits_chart_roundtrip_attrs_to_matching_tags(self) -> None:
347
367
  result = xml_text_overlap_lint.lint_xml(
348
368
  """
@@ -637,9 +657,10 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
637
657
  </slide>
638
658
  """
639
659
  )
640
- self.assertEqual(result["summary"]["error_count"], 0)
641
- self.assertEqual(result["summary"]["warning_count"], 1)
660
+ self.assertEqual(result["summary"]["error_count"], 1)
661
+ self.assertEqual(result["summary"]["warning_count"], 0)
642
662
  self.assertEqual(result["slides"][0]["issues"][0]["code"], "text_may_overflow_shape")
663
+ self.assertEqual(result["slides"][0]["issues"][0]["level"], "error")
643
664
  self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["source"])
644
665
 
645
666
  def test_lint_xml_reports_text_out_of_canvas_and_warns_for_text_height(self) -> None:
@@ -660,8 +681,8 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
660
681
  """
661
682
  )
662
683
  issue = result["slides"][0]["issues"][0]
663
- self.assertEqual(result["summary"]["error_count"], 1)
664
- self.assertEqual(result["summary"]["warning_count"], 1)
684
+ self.assertEqual(result["summary"]["error_count"], 2)
685
+ self.assertEqual(result["summary"]["warning_count"], 0)
665
686
  self.assertEqual(issue["code"], "shape_out_of_canvas")
666
687
  self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 160, "bottom": 40})
667
688
 
@@ -671,7 +692,7 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
671
692
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
672
693
  <data>
673
694
  <shape id="overflowing" type="text" topLeftX="80" topLeftY="80" width="360" height="80">
674
- <content fontSize="20" lineSpacing="multiple:1.5">
695
+ <content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit">
675
696
  <p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
676
697
  </content>
677
698
  </shape>
@@ -685,20 +706,29 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
685
706
  <p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
686
707
  </content>
687
708
  </shape>
709
+ <shape id="shape-auto-fit" type="text" topLeftX="480" topLeftY="240" width="360" height="30">
710
+ <content fontSize="20" lineSpacing="multiple:1.5" autoFit="shape-auto-fit">
711
+ <p>第一段</p><p>第二段</p><p>第三段</p>
712
+ </content>
713
+ </shape>
688
714
  </data>
689
715
  </slide>
690
716
  """
691
717
  )
692
718
  issues = result["slides"][0]["issues"]
693
- self.assertEqual(result["summary"]["error_count"], 0)
694
- self.assertEqual(result["summary"]["warning_count"], 1)
695
- self.assertEqual(issues[0]["code"], "text_may_overflow_shape")
696
- self.assertEqual(issues[0]["elements"], ["overflowing"])
697
- self.assertEqual(issues[0]["line_count"], 4)
698
- self.assertEqual(issues[0]["estimated_height"], 110)
699
- self.assertEqual(issues[0]["available_height"], 80)
700
- self.assertEqual(issues[0]["overflow"], 30)
701
- self.assertIn('wrap="true" autoFit="normal-auto-fit"', issues[0]["message"])
719
+ overflow_issues = [issue for issue in issues if issue["code"] == "text_may_overflow_shape"]
720
+ self.assertEqual(result["summary"]["error_count"], 1)
721
+ overflow_ids = {issue["elements"][0] for issue in overflow_issues}
722
+ self.assertIn("overflowing", overflow_ids)
723
+ self.assertNotIn("auto-fit", overflow_ids)
724
+ self.assertNotIn("shape-auto-fit", overflow_ids)
725
+ self.assertNotIn("fitting", overflow_ids)
726
+ overflowing_issue = next(issue for issue in overflow_issues if issue["elements"] == ["overflowing"])
727
+ self.assertEqual(overflowing_issue["line_count"], 4)
728
+ self.assertEqual(overflowing_issue["estimated_height"], 110)
729
+ self.assertEqual(overflowing_issue["available_height"], 80)
730
+ self.assertEqual(overflowing_issue["overflow"], 30)
731
+ self.assertIn('wrap="true" autoFit="normal-auto-fit"', overflowing_issue["message"])
702
732
 
703
733
  def test_lint_xml_uses_fixed_line_spacing_for_text_height_warning(self) -> None:
704
734
  result = xml_text_overlap_lint.lint_xml(
@@ -706,7 +736,7 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
706
736
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
707
737
  <data>
708
738
  <shape id="fixed-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="50">
709
- <content fontSize="20" lineSpacing="fixed:20">
739
+ <content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
710
740
  <p>第一段</p><p>第二段</p><p>第三段</p>
711
741
  </content>
712
742
  </shape>
@@ -716,17 +746,525 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
716
746
  )
717
747
  issue = result["slides"][0]["issues"][0]
718
748
  self.assertEqual(result["summary"]["warning_count"], 1)
749
+ self.assertEqual(result["summary"]["error_count"], 0)
750
+ self.assertEqual(issue["level"], "warning")
719
751
  self.assertEqual(issue["line_height"], 20)
720
752
  self.assertEqual(issue["estimated_height"], 60)
721
753
  self.assertEqual(issue["overflow"], 10)
722
754
 
755
+ def test_lint_xml_ignores_subpixel_text_height_overflow_tolerance(self) -> None:
756
+ result = xml_text_overlap_lint.lint_xml(
757
+ """
758
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
759
+ <data>
760
+ <shape id="minor-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="39.8">
761
+ <content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
762
+ <p>第一段</p><p>第二段</p>
763
+ </content>
764
+ </shape>
765
+ </data>
766
+ </slide>
767
+ """
768
+ )
769
+ overflow_issues = [
770
+ issue
771
+ for issue in result["slides"][0]["issues"]
772
+ if issue["code"] == "text_may_overflow_shape"
773
+ ]
774
+ self.assertEqual(overflow_issues, [])
775
+
776
+ def test_lint_xml_allows_single_line_width_estimation_jitter(self) -> None:
777
+ result = xml_text_overlap_lint.lint_xml(
778
+ """
779
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
780
+ <data>
781
+ <shape id="metric" type="text" topLeftX="80" topLeftY="80" width="152" height="54">
782
+ <content fontSize="36" lineSpacing="multiple:1.2" autoFit="no-auto-fit"><p>4.16万亿</p></content>
783
+ </shape>
784
+ </data>
785
+ </slide>
786
+ """
787
+ )
788
+ overflow_issues = [
789
+ issue
790
+ for issue in result["slides"][0]["issues"]
791
+ if issue["code"] == "text_may_overflow_shape"
792
+ ]
793
+ self.assertEqual(overflow_issues, [])
794
+
795
+ def test_lint_xml_allows_short_metric_text_with_separators_as_single_line(self) -> None:
796
+ result = xml_text_overlap_lint.lint_xml(
797
+ """
798
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
799
+ <data>
800
+ <shape id="metric" type="text" topLeftX="80" topLeftY="80" width="150" height="50">
801
+ <content textType="title" fontSize="36" autoFit="no-auto-fit"><p>4.16万亿</p></content>
802
+ </shape>
803
+ <shape id="table-number" type="text" topLeftX="80" topLeftY="160" width="25" height="20">
804
+ <content fontSize="10" textAlign="center" autoFit="no-auto-fit"><p>1,380</p></content>
805
+ </shape>
806
+ </data>
807
+ </slide>
808
+ """
809
+ )
810
+ overflow_issues = [
811
+ issue
812
+ for issue in result["slides"][0]["issues"]
813
+ if issue["code"] == "text_may_overflow_shape"
814
+ ]
815
+ self.assertEqual(overflow_issues, [])
816
+
817
+ def test_lint_xml_reports_plain_short_metric_when_it_wraps(self) -> None:
818
+ result = xml_text_overlap_lint.lint_xml(
819
+ """
820
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
821
+ <data>
822
+ <shape id="plain-age" type="text" topLeftX="80" topLeftY="80" width="50" height="80">
823
+ <content textType="title" fontSize="36" bold="true" autoFit="no-auto-fit"><p>82岁</p></content>
824
+ </shape>
825
+ </data>
826
+ </slide>
827
+ """
828
+ )
829
+ overflow_issues = [
830
+ issue
831
+ for issue in result["slides"][0]["issues"]
832
+ if issue["code"] == "text_may_overflow_shape"
833
+ ]
834
+ self.assertEqual(len(overflow_issues), 1)
835
+ self.assertEqual(overflow_issues[0]["elements"], ["plain-age"])
836
+
837
+ def test_lint_xml_allows_centered_short_label_near_fit_as_single_line(self) -> None:
838
+ result = xml_text_overlap_lint.lint_xml(
839
+ """
840
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
841
+ <data>
842
+ <shape id="centered-label" type="text" topLeftX="80" topLeftY="80" width="200" height="30">
843
+ <content fontSize="14" bold="true" textAlign="center" autoFit="no-auto-fit">
844
+ <p>参数服务器 (Parameter Server)</p>
845
+ </content>
846
+ </shape>
847
+ </data>
848
+ </slide>
849
+ """
850
+ )
851
+ overflow_issues = [
852
+ issue
853
+ for issue in result["slides"][0]["issues"]
854
+ if issue["code"] == "text_may_overflow_shape"
855
+ ]
856
+ self.assertEqual(overflow_issues, [])
857
+
858
+ def test_lint_xml_allows_headline_near_fit_as_single_line(self) -> None:
859
+ result = xml_text_overlap_lint.lint_xml(
860
+ """
861
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
862
+ <data>
863
+ <shape id="headline" type="text" topLeftX="80" topLeftY="80" width="700" height="50">
864
+ <content textType="headline" fontSize="26" bold="true" lineSpacing="multiple:1.3" autoFit="no-auto-fit">
865
+ <p>全球半导体市场规模持续高速增长,AI驱动新一轮景气周期</p>
866
+ </content>
867
+ </shape>
868
+ </data>
869
+ </slide>
870
+ """
871
+ )
872
+ overflow_issues = [
873
+ issue
874
+ for issue in result["slides"][0]["issues"]
875
+ if issue["code"] == "text_may_overflow_shape"
876
+ ]
877
+ self.assertEqual(overflow_issues, [])
878
+
879
+ def test_lint_xml_allows_dense_body_line_spacing_estimation_slack(self) -> None:
880
+ result = xml_text_overlap_lint.lint_xml(
881
+ """
882
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
883
+ <data>
884
+ <shape id="dense-body" type="text" topLeftX="80" topLeftY="80" width="360" height="140">
885
+ <content fontSize="13" bold="true" lineSpacing="multiple:1.7" autoFit="no-auto-fit">
886
+ <p>总体目标:</p>
887
+ <p>建立深度神经网络高效训练的统一理论框架,实现训练效率与模型性能的协同优化。</p>
888
+ <p>具体目标:</p>
889
+ <p>提出自适应优化算法,收敛速度提升 2-3 倍</p>
890
+ <p>实现结构化压缩方法,模型体积减少 10 倍以上</p>
891
+ <p>构建分布式训练策略,64 GPU 加速比 &gt; 50x</p>
892
+ </content>
893
+ </shape>
894
+ </data>
895
+ </slide>
896
+ """
897
+ )
898
+ overflow_issues = [
899
+ issue
900
+ for issue in result["slides"][0]["issues"]
901
+ if issue["code"] == "text_may_overflow_shape"
902
+ ]
903
+ self.assertEqual(overflow_issues, [])
904
+
905
+ def test_lint_xml_reports_dense_body_when_adjusted_height_still_overflows(self) -> None:
906
+ result = xml_text_overlap_lint.lint_xml(
907
+ """
908
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
909
+ <data>
910
+ <shape id="dense-body" type="text" topLeftX="80" topLeftY="80" width="360" height="100">
911
+ <content fontSize="13" bold="true" lineSpacing="multiple:1.7" autoFit="no-auto-fit">
912
+ <p>总体目标:</p>
913
+ <p>建立深度神经网络高效训练的统一理论框架,实现训练效率与模型性能的协同优化。</p>
914
+ <p>具体目标:</p>
915
+ <p>提出自适应优化算法,收敛速度提升 2-3 倍</p>
916
+ <p>实现结构化压缩方法,模型体积减少 10 倍以上</p>
917
+ <p>构建分布式训练策略,64 GPU 加速比 &gt; 50x</p>
918
+ </content>
919
+ </shape>
920
+ </data>
921
+ </slide>
922
+ """
923
+ )
924
+ overflow_issues = [
925
+ issue
926
+ for issue in result["slides"][0]["issues"]
927
+ if issue["code"] == "text_may_overflow_shape"
928
+ ]
929
+ self.assertEqual(len(overflow_issues), 1)
930
+ self.assertEqual(overflow_issues[0]["elements"], ["dense-body"])
931
+
932
+ def test_lint_xml_reports_letter_spaced_caption_near_fit(self) -> None:
933
+ result = xml_text_overlap_lint.lint_xml(
934
+ """
935
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
936
+ <data>
937
+ <shape id="caption" type="text" topLeftX="80" topLeftY="80" width="120" height="20">
938
+ <content textType="caption" fontSize="11" letterSpacing="1" autoFit="no-auto-fit">
939
+ <p>RISKS &amp; CHALLENGES</p>
940
+ </content>
941
+ </shape>
942
+ </data>
943
+ </slide>
944
+ """
945
+ )
946
+ overflow_issues = [
947
+ issue
948
+ for issue in result["slides"][0]["issues"]
949
+ if issue["code"] == "text_may_overflow_shape"
950
+ ]
951
+ self.assertEqual(len(overflow_issues), 1)
952
+ self.assertEqual(overflow_issues[0]["elements"], ["caption"])
953
+
954
+ def test_lint_xml_reports_micro_caption_when_wrapping_overflows(self) -> None:
955
+ result = xml_text_overlap_lint.lint_xml(
956
+ """
957
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
958
+ <data>
959
+ <shape id="micro-caption" type="text" topLeftX="80" topLeftY="60" width="200" height="16">
960
+ <content textType="caption" fontSize="3" lineSpacing="multiple:1.3" letterSpacing="160" autoFit="no-auto-fit">
961
+ <p>MARKET INSIGHT · 市场洞察</p>
962
+ </content>
963
+ </shape>
964
+ </data>
965
+ </slide>
966
+ """
967
+ )
968
+ overflow_issues = [
969
+ issue
970
+ for issue in result["slides"][0]["issues"]
971
+ if issue["code"] == "text_may_overflow_shape"
972
+ ]
973
+ self.assertEqual(len(overflow_issues), 1)
974
+ self.assertEqual(overflow_issues[0]["elements"], ["micro-caption"])
975
+
976
+ def test_lint_xml_text_may_overflow_shape_upgrades_to_error_above_threshold(self) -> None:
977
+ result = xml_text_overlap_lint.lint_xml(
978
+ """
979
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
980
+ <data>
981
+ <shape id="just-warning" type="text" topLeftX="80" topLeftY="80" width="360" height="50">
982
+ <content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
983
+ <p>第一段</p><p>第二段</p><p>第三段</p>
984
+ </content>
985
+ </shape>
986
+ <shape id="error-overflow" type="text" topLeftX="80" topLeftY="200" width="360" height="30">
987
+ <content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
988
+ <p>第一段</p><p>第二段</p><p>第三段</p>
989
+ </content>
990
+ </shape>
991
+ </data>
992
+ </slide>
993
+ """
994
+ )
995
+ issues = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
996
+ self.assertEqual(issues["just-warning"]["level"], "warning")
997
+ self.assertEqual(issues["just-warning"]["overflow"], 10)
998
+ self.assertEqual(issues["error-overflow"]["level"], "error")
999
+ self.assertEqual(issues["error-overflow"]["overflow"], 30)
1000
+ self.assertEqual(result["summary"]["error_count"], 1)
1001
+ self.assertEqual(result["summary"]["warning_count"], 1)
1002
+
1003
+ def test_lint_xml_text_may_overflow_shape_downgrades_background_decoration_to_info(self) -> None:
1004
+ result = xml_text_overlap_lint.lint_xml(
1005
+ """
1006
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1007
+ <data>
1008
+ <shape id="bg-deco" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
1009
+ <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
1010
+ </shape>
1011
+ <shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
1012
+ <content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
1013
+ </shape>
1014
+ </data>
1015
+ </slide>
1016
+ """
1017
+ )
1018
+ issues = {
1019
+ issue["elements"][0]: issue
1020
+ for issue in result["slides"][0]["issues"]
1021
+ if issue["code"] == "text_may_overflow_shape"
1022
+ }
1023
+ self.assertEqual(issues["bg-deco"]["level"], "info")
1024
+ self.assertEqual(result["summary"]["warning_count"], 0)
1025
+ self.assertEqual(result["summary"]["info_count"], 1)
1026
+ self.assertEqual(result["slides"][0]["infos"], [issues["bg-deco"]])
1027
+ self.assertIn("background decoration", issues["bg-deco"]["message"])
1028
+
1029
+ def test_lint_xml_reports_shape_alpha_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
1030
+ result = xml_text_overlap_lint.lint_xml(
1031
+ """
1032
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1033
+ <data>
1034
+ <shape id="ghost-number" type="text" topLeftX="-60" topLeftY="30" width="360" height="180" alpha="0.2">
1035
+ <content fontSize="160" lineSpacing="fixed:160" wrap="false"><p>01</p></content>
1036
+ </shape>
1037
+ <shape id="title" type="text" topLeftX="80" topLeftY="80" width="360" height="80">
1038
+ <content fontSize="30" lineSpacing="fixed:36"><p>Annual Review</p></content>
1039
+ </shape>
1040
+ </data>
1041
+ </slide>
1042
+ """
1043
+ )
1044
+ codes = [issue["code"] for issue in result["slides"][0]["issues"]]
1045
+ self.assertEqual(result["summary"]["error_count"], 1)
1046
+ self.assertIn("shape_out_of_canvas", codes)
1047
+ self.assertNotIn("bbox_overlap", codes)
1048
+
1049
+ def test_lint_xml_reports_content_color_alpha_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
1050
+ result = xml_text_overlap_lint.lint_xml(
1051
+ """
1052
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1053
+ <data>
1054
+ <shape id="ghost-year" type="text" topLeftX="760" topLeftY="20" width="260" height="160">
1055
+ <content fontSize="140" color="rgba(0,0,0,0.2)" lineSpacing="fixed:140" wrap="false"><p>2026</p></content>
1056
+ </shape>
1057
+ <shape id="headline" type="text" topLeftX="700" topLeftY="70" width="220" height="80">
1058
+ <content fontSize="28" lineSpacing="fixed:34"><p>Forecast</p></content>
1059
+ </shape>
1060
+ </data>
1061
+ </slide>
1062
+ """
1063
+ )
1064
+ codes = [issue["code"] for issue in result["slides"][0]["issues"]]
1065
+ self.assertEqual(result["summary"]["error_count"], 1)
1066
+ self.assertIn("shape_out_of_canvas", codes)
1067
+ self.assertNotIn("bbox_overlap", codes)
1068
+
1069
+ def test_lint_xml_reports_faint_medium_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
1070
+ result = xml_text_overlap_lint.lint_xml(
1071
+ """
1072
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1073
+ <data>
1074
+ <shape id="medium-ghost" type="text" topLeftX="820" topLeftY="300" width="270" height="72" alpha="0.32">
1075
+ <content fontSize="40" lineSpacing="fixed:40" wrap="false"><p>OFF EDGE</p></content>
1076
+ </shape>
1077
+ <shape id="caption" type="text" topLeftX="760" topLeftY="315" width="180" height="36">
1078
+ <content fontSize="16" lineSpacing="fixed:20"><p>Readable caption</p></content>
1079
+ </shape>
1080
+ </data>
1081
+ </slide>
1082
+ """
1083
+ )
1084
+ codes = [issue["code"] for issue in result["slides"][0]["issues"]]
1085
+ self.assertEqual(result["summary"]["error_count"], 1)
1086
+ self.assertIn("shape_out_of_canvas", codes)
1087
+ self.assertNotIn("bbox_overlap", codes)
1088
+
1089
+ def test_lint_xml_allows_ghost_text_image_overlap(self) -> None:
1090
+ result = xml_text_overlap_lint.lint_xml(
1091
+ """
1092
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1093
+ <data>
1094
+ <shape id="ghost-label" type="text" topLeftX="100" topLeftY="40" width="560" height="160" alpha="0.2">
1095
+ <content fontSize="120" lineSpacing="fixed:120" wrap="false"><p>2026</p></content>
1096
+ </shape>
1097
+ <img id="photo" src="token" topLeftX="160" topLeftY="70" width="260" height="160"/>
1098
+ <shape id="title" type="text" topLeftX="610" topLeftY="95" width="320" height="60">
1099
+ <content fontSize="28" lineSpacing="fixed:34"><p>Annual Review</p></content>
1100
+ </shape>
1101
+ </data>
1102
+ </slide>
1103
+ """
1104
+ )
1105
+ codes = [issue["code"] for issue in result["slides"][0]["issues"]]
1106
+ self.assertNotIn("image_covers_text", codes)
1107
+ self.assertNotIn("bbox_overlap", codes)
1108
+
1109
+ def test_lint_slide_allows_ghost_text_whiteboard_overlap(self) -> None:
1110
+ result = xml_text_overlap_lint.lint_slide(
1111
+ """
1112
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1113
+ <data>
1114
+ <whiteboard id="board" topLeftX="180" topLeftY="70" width="420" height="300"/>
1115
+ <shape id="ghost-label" type="text" topLeftX="100" topLeftY="40" width="560" height="160" alpha="0.2">
1116
+ <content fontSize="120" lineSpacing="fixed:120" wrap="false"><p>2026</p></content>
1117
+ </shape>
1118
+ <shape id="title" type="text" topLeftX="610" topLeftY="95" width="220" height="60">
1119
+ <content fontSize="28" lineSpacing="fixed:34"><p>Annual Review</p></content>
1120
+ </shape>
1121
+ </data>
1122
+ </slide>
1123
+ """,
1124
+ 1,
1125
+ )
1126
+ codes = [issue["code"] for issue in result["issues"]]
1127
+ self.assertNotIn("whiteboard_external_overlap", codes)
1128
+
1129
+ def test_lint_xml_reports_faint_ghost_text_out_of_canvas_without_area_threshold(self) -> None:
1130
+ result = xml_text_overlap_lint.lint_xml(
1131
+ """
1132
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1133
+ <data>
1134
+ <shape id="small-ghost" type="text" topLeftX="940" topLeftY="300" width="40" height="40" alpha="0.32">
1135
+ <content fontSize="36" lineSpacing="fixed:36" wrap="false"><p>土</p></content>
1136
+ </shape>
1137
+ </data>
1138
+ </slide>
1139
+ """
1140
+ )
1141
+ self.assertEqual(result["summary"]["error_count"], 1)
1142
+ self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
1143
+
1144
+ def test_lint_xml_keeps_out_of_canvas_error_for_medium_text_without_faint_alpha(self) -> None:
1145
+ result = xml_text_overlap_lint.lint_xml(
1146
+ """
1147
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1148
+ <data>
1149
+ <shape id="medium-not-ghost" type="text" topLeftX="820" topLeftY="300" width="270" height="72" alpha="0.36">
1150
+ <content fontSize="54" lineSpacing="fixed:54" wrap="false"><p>OFF EDGE</p></content>
1151
+ </shape>
1152
+ </data>
1153
+ </slide>
1154
+ """
1155
+ )
1156
+ self.assertEqual(result["summary"]["error_count"], 1)
1157
+ self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
1158
+ self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["medium-not-ghost"])
1159
+
1160
+ def test_lint_xml_keeps_out_of_canvas_error_for_half_alpha_large_text(self) -> None:
1161
+ result = xml_text_overlap_lint.lint_xml(
1162
+ """
1163
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1164
+ <data>
1165
+ <shape id="half-alpha" type="text" topLeftX="760" topLeftY="20" width="260" height="160">
1166
+ <content fontSize="140" color="rgba(0,0,0,0.5)" lineSpacing="fixed:140" wrap="false"><p>2026</p></content>
1167
+ </shape>
1168
+ </data>
1169
+ </slide>
1170
+ """
1171
+ )
1172
+ self.assertEqual(result["summary"]["error_count"], 1)
1173
+ self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
1174
+ self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["half-alpha"])
1175
+
1176
+ def test_lint_xml_text_may_overflow_shape_keeps_error_when_alpha_not_low(self) -> None:
1177
+ result = xml_text_overlap_lint.lint_xml(
1178
+ """
1179
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1180
+ <data>
1181
+ <shape id="opaque-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.9">
1182
+ <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
1183
+ </shape>
1184
+ <shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
1185
+ <content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
1186
+ </shape>
1187
+ </data>
1188
+ </slide>
1189
+ """
1190
+ )
1191
+ issue = next(
1192
+ issue
1193
+ for issue in result["slides"][0]["issues"]
1194
+ if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["opaque-big"]
1195
+ )
1196
+ self.assertEqual(issue["level"], "error")
1197
+
1198
+ def test_lint_xml_text_may_overflow_shape_keeps_error_when_no_foreground_text(self) -> None:
1199
+ result = xml_text_overlap_lint.lint_xml(
1200
+ """
1201
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1202
+ <data>
1203
+ <shape id="lonely-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
1204
+ <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
1205
+ </shape>
1206
+ </data>
1207
+ </slide>
1208
+ """
1209
+ )
1210
+ issue = next(
1211
+ issue
1212
+ for issue in result["slides"][0]["issues"]
1213
+ if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["lonely-big"]
1214
+ )
1215
+ self.assertEqual(issue["level"], "error")
1216
+
1217
+ def test_lint_xml_text_may_overflow_shape_keeps_error_when_foreground_alpha_zero(self) -> None:
1218
+ result = xml_text_overlap_lint.lint_xml(
1219
+ """
1220
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1221
+ <data>
1222
+ <shape id="bg-deco" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
1223
+ <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
1224
+ </shape>
1225
+ <shape id="transparent-foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60" alpha="0">
1226
+ <content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
1227
+ </shape>
1228
+ </data>
1229
+ </slide>
1230
+ """
1231
+ )
1232
+ issue = next(
1233
+ issue
1234
+ for issue in result["slides"][0]["issues"]
1235
+ if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["bg-deco"]
1236
+ )
1237
+ self.assertEqual(issue["level"], "error")
1238
+
1239
+ def test_lint_xml_text_may_overflow_shape_keeps_error_when_foreground_is_below_in_order(self) -> None:
1240
+ result = xml_text_overlap_lint.lint_xml(
1241
+ """
1242
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1243
+ <data>
1244
+ <shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
1245
+ <content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
1246
+ </shape>
1247
+ <shape id="top-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
1248
+ <content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
1249
+ </shape>
1250
+ </data>
1251
+ </slide>
1252
+ """
1253
+ )
1254
+ issue = next(
1255
+ issue
1256
+ for issue in result["slides"][0]["issues"]
1257
+ if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["top-big"]
1258
+ )
1259
+ self.assertEqual(issue["level"], "error")
1260
+
723
1261
  def test_lint_xml_uses_paragraph_spacing_overrides_for_text_height_warning(self) -> None:
724
1262
  result = xml_text_overlap_lint.lint_xml(
725
1263
  """
726
1264
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
727
1265
  <data>
728
- <shape id="paragraph-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="35">
729
- <content fontSize="20" lineSpacing="multiple:1.5">
1266
+ <shape id="paragraph-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="30">
1267
+ <content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit">
730
1268
  <p lineSpacing="fixed:10" beforeLineSpacing="fixed:5" afterLineSpacing="fixed:5">第一行<br/>第二行</p>
731
1269
  </content>
732
1270
  </shape>
@@ -745,7 +1283,36 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
745
1283
  self.assertEqual(issues[0]["line_count"], 2)
746
1284
  self.assertEqual(issues[0]["line_height"], 10)
747
1285
  self.assertEqual(issues[0]["estimated_height"], 40)
748
- self.assertEqual(issues[0]["overflow"], 5)
1286
+ self.assertEqual(issues[0]["overflow"], 10)
1287
+
1288
+ def test_lint_xml_uses_letter_spacing_for_text_overflow_warning(self) -> None:
1289
+ result = xml_text_overlap_lint.lint_xml(
1290
+ """
1291
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1292
+ <data>
1293
+ <shape id="baseline" type="text" topLeftX="0" topLeftY="0" width="120" height="30">
1294
+ <content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit"><p>一二三四五六</p></content>
1295
+ </shape>
1296
+ <shape id="content-spaced" type="text" topLeftX="200" topLeftY="0" width="120" height="30">
1297
+ <content fontSize="20" lineSpacing="multiple:1.5" letterSpacing="2" autoFit="no-auto-fit"><p>一二三四五六</p></content>
1298
+ </shape>
1299
+ <shape id="paragraph-spaced" type="text" topLeftX="400" topLeftY="0" width="120" height="30">
1300
+ <content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit"><p letterSpacing="2">一二三四五六</p></content>
1301
+ </shape>
1302
+ </data>
1303
+ </slide>
1304
+ """
1305
+ )
1306
+ issues = result["slides"][0]["issues"]
1307
+ overflow_ids = [issue["elements"][0] for issue in issues if issue["code"] == "text_may_overflow_shape"]
1308
+ self.assertNotIn("baseline", overflow_ids)
1309
+ self.assertIn("content-spaced", overflow_ids)
1310
+ self.assertIn("paragraph-spaced", overflow_ids)
1311
+ by_id = {issue["elements"][0]: issue for issue in issues if issue["code"] == "text_may_overflow_shape"}
1312
+ self.assertEqual(by_id["content-spaced"]["line_count"], 2)
1313
+ self.assertEqual(by_id["content-spaced"]["estimated_height"], 50)
1314
+ self.assertEqual(by_id["content-spaced"]["overflow"], 20)
1315
+ self.assertEqual(by_id["paragraph-spaced"]["line_count"], 2)
749
1316
 
750
1317
  def test_strip_xml_paragraphs_preserves_br_as_hard_line_break(self) -> None:
751
1318
  self.assertEqual(
@@ -753,7 +1320,7 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
753
1320
  "第一行\n第二行\n第三行",
754
1321
  )
755
1322
 
756
- def test_lint_xml_blocks_template_style_bleed_outside_canvas(self) -> None:
1323
+ def test_lint_xml_allows_template_style_images_outside_canvas(self) -> None:
757
1324
  result = xml_text_overlap_lint.lint_xml(
758
1325
  """
759
1326
  <presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
@@ -771,9 +1338,8 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
771
1338
  </presentation>
772
1339
  """
773
1340
  )
774
- self.assertEqual(result["summary"]["error_count"], 1)
1341
+ self.assertEqual(result["summary"]["error_count"], 0)
775
1342
  self.assertEqual(result["summary"]["warning_count"], 0)
776
- self.assertEqual(result["slides"][0]["errors"][0]["code"], "img_out_of_canvas")
777
1343
 
778
1344
  def test_extract_elements_preserves_supported_element_geometry_order_and_text_metadata(self) -> None:
779
1345
  elements = xml_text_overlap_lint.extract_elements(
@@ -807,7 +1373,7 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
807
1373
  self.assertEqual(elements[1]["fontSize"], 28)
808
1374
  self.assertEqual(elements[1]["text"], "Growth & scale\nFocused execution")
809
1375
 
810
- def test_lint_xml_blocks_small_out_of_bounds_images(self) -> None:
1376
+ def test_lint_xml_ignores_small_out_of_bounds_images(self) -> None:
811
1377
  result = xml_text_overlap_lint.lint_xml(
812
1378
  """
813
1379
  <presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
@@ -819,10 +1385,9 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
819
1385
  </presentation>
820
1386
  """
821
1387
  )
822
- self.assertEqual(result["summary"]["error_count"], 1)
823
- self.assertEqual(result["slides"][0]["errors"][0]["code"], "img_out_of_canvas")
1388
+ self.assertEqual(result["summary"]["error_count"], 0)
824
1389
 
825
- def test_lint_xml_blocks_out_of_canvas_images(self) -> None:
1390
+ def test_lint_xml_ignores_out_of_canvas_images(self) -> None:
826
1391
  result = xml_text_overlap_lint.lint_xml(
827
1392
  """
828
1393
  <presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
@@ -835,13 +1400,9 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
835
1400
  </presentation>
836
1401
  """
837
1402
  )
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
- )
1403
+ self.assertEqual(result["summary"]["error_count"], 0)
843
1404
 
844
- def test_lint_xml_blocks_full_bleed_images_outside_canvas(self) -> None:
1405
+ def test_lint_xml_ignores_full_bleed_images_outside_canvas(self) -> None:
845
1406
  result = xml_text_overlap_lint.lint_xml(
846
1407
  """
847
1408
  <presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
@@ -853,10 +1414,9 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
853
1414
  </presentation>
854
1415
  """
855
1416
  )
856
- self.assertEqual(result["summary"]["error_count"], 1)
857
- self.assertEqual(result["slides"][0]["errors"][0]["code"], "img_out_of_canvas")
1417
+ self.assertEqual(result["summary"]["error_count"], 0)
858
1418
 
859
- def test_lint_xml_reports_text_and_chart_out_of_canvas(self) -> None:
1419
+ def test_lint_xml_reports_text_and_chart_but_not_image_out_of_canvas(self) -> None:
860
1420
  result = xml_text_overlap_lint.lint_xml(
861
1421
  """
862
1422
  <presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
@@ -871,17 +1431,16 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
871
1431
  """
872
1432
  )
873
1433
  issues = result["slides"][0]["issues"]
874
- self.assertEqual(result["summary"]["error_count"], 3)
1434
+ self.assertEqual(result["summary"]["error_count"], 2)
875
1435
  self.assertEqual(
876
1436
  [(issue["code"], issue["elements"], issue["overflow"]) for issue in issues],
877
1437
  [
878
1438
  ("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}),
880
1439
  ("chart_out_of_canvas", ["outside-chart"], {"left": 0, "top": 0, "right": 40, "bottom": 0}),
881
1440
  ],
882
1441
  )
883
1442
 
884
- def test_lint_xml_reports_line_out_of_canvas_with_structured_geometry(self) -> None:
1443
+ def test_lint_xml_ignores_line_out_of_canvas(self) -> None:
885
1444
  result = xml_text_overlap_lint.lint_xml(
886
1445
  """
887
1446
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
@@ -895,11 +1454,189 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
895
1454
  """
896
1455
  )
897
1456
 
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")
1457
+ self.assertEqual(result["summary"]["error_count"], 0)
1458
+ self.assertEqual(result["slides"][0]["issues"], [])
1459
+
1460
+ def test_lint_xml_reports_horizontal_line_crossing_headline_glyphs(self) -> None:
1461
+ result = xml_text_overlap_lint.lint_xml(
1462
+ """
1463
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1464
+ <data>
1465
+ <shape id="title" type="text" topLeftX="80" topLeftY="200" width="500" height="90">
1466
+ <content fontSize="60"><p>测试文字 ABC</p></content>
1467
+ </shape>
1468
+ <line id="strike" startX="80" startY="245" endX="560" endY="245">
1469
+ <border color="rgb(255, 0, 0)" width="4"/>
1470
+ </line>
1471
+ </data>
1472
+ </slide>
1473
+ """
1474
+ )
1475
+ crossing = [
1476
+ issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"strike", "title"}
1477
+ ]
1478
+ self.assertEqual(len(crossing), 1)
1479
+ self.assertEqual(crossing[0]["code"], "bbox_overlap")
1480
+
1481
+ def test_lint_xml_reports_vertical_line_crossing_multiline_text(self) -> None:
1482
+ result = xml_text_overlap_lint.lint_xml(
1483
+ """
1484
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1485
+ <data>
1486
+ <shape id="col" type="text" topLeftX="700" topLeftY="180" width="240" height="180">
1487
+ <content fontSize="20"><p>第一行文字内容</p><p>第二行文字内容</p><p>第三行文字内容</p></content>
1488
+ </shape>
1489
+ <line id="vbar" startX="740" startY="170" endX="740" endY="360">
1490
+ <border color="rgb(0, 0, 255)" width="3"/>
1491
+ </line>
1492
+ </data>
1493
+ </slide>
1494
+ """
1495
+ )
1496
+ crossing = [
1497
+ issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"vbar", "col"}
1498
+ ]
1499
+ self.assertEqual(len(crossing), 1)
1500
+
1501
+ def test_lint_xml_reports_diagonal_line_crossing_text_block(self) -> None:
1502
+ result = xml_text_overlap_lint.lint_xml(
1503
+ """
1504
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1505
+ <data>
1506
+ <shape id="para" type="text" topLeftX="80" topLeftY="400" width="420" height="140">
1507
+ <content fontSize="18"><p>这是一段测试文字用于验证线条穿过</p></content>
1508
+ </shape>
1509
+ <line id="diag" startX="80" startY="410" endX="500" endY="530">
1510
+ <border color="rgb(255, 0, 0)" width="3"/>
1511
+ </line>
1512
+ </data>
1513
+ </slide>
1514
+ """
1515
+ )
1516
+ crossing = [
1517
+ issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"diag", "para"}
1518
+ ]
1519
+ self.assertEqual(len(crossing), 1)
1520
+
1521
+ def test_lint_xml_ignores_diagonal_line_whose_bbox_but_not_segment_crosses_text(self) -> None:
1522
+ # The diagonal's axis-aligned bounding box overlaps the text, but the segment itself passes
1523
+ # through empty space in the opposite corner -- a naive bbox test would false-positive here.
1524
+ result = xml_text_overlap_lint.lint_xml(
1525
+ """
1526
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1527
+ <data>
1528
+ <shape id="corner-text" type="text" topLeftX="80" topLeftY="80" width="120" height="40">
1529
+ <content fontSize="18"><p>corner</p></content>
1530
+ </shape>
1531
+ <line id="far-diag" startX="700" startY="80" endX="90" endY="500">
1532
+ <border color="rgb(255, 0, 0)" width="3"/>
1533
+ </line>
1534
+ </data>
1535
+ </slide>
1536
+ """
1537
+ )
1538
+ crossing = [
1539
+ issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"far-diag", "corner-text"}
1540
+ ]
1541
+ self.assertEqual(crossing, [])
1542
+
1543
+ def test_lint_xml_ignores_line_touching_text_frame_but_not_glyphs(self) -> None:
1544
+ result = xml_text_overlap_lint.lint_xml(
1545
+ """
1546
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1547
+ <data>
1548
+ <shape id="lbl" type="text" topLeftX="80" topLeftY="80" width="300" height="200">
1549
+ <content fontSize="18" verticalAlign="top"><p>短标签</p></content>
1550
+ </shape>
1551
+ <line id="below" startX="80" startY="270" endX="380" endY="270">
1552
+ <border color="rgb(255, 0, 0)" width="2"/>
1553
+ </line>
1554
+ </data>
1555
+ </slide>
1556
+ """
1557
+ )
1558
+ self.assertEqual(result["summary"]["error_count"], 0)
1559
+
1560
+ def test_lint_xml_ignores_invisible_line_crossing_text(self) -> None:
1561
+ result = xml_text_overlap_lint.lint_xml(
1562
+ """
1563
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1564
+ <data>
1565
+ <shape id="title" type="text" topLeftX="80" topLeftY="200" width="500" height="90">
1566
+ <content fontSize="60"><p>测试文字 ABC</p></content>
1567
+ </shape>
1568
+ <line id="ghost-line" startX="80" startY="245" endX="560" endY="245">
1569
+ <border color="rgba(255, 0, 0, 0.03)" width="4"/>
1570
+ </line>
1571
+ </data>
1572
+ </slide>
1573
+ """
1574
+ )
1575
+ self.assertEqual(result["summary"]["error_count"], 0)
1576
+
1577
+ def test_lint_xml_ignores_vertical_line_grazing_text_left_edge(self) -> None:
1578
+ # Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 4: a vertical line sitting on the text
1579
+ # frame's left edge renders before the first glyph, so it must not be flagged.
1580
+ result = xml_text_overlap_lint.lint_xml(
1581
+ """
1582
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1583
+ <data>
1584
+ <shape width="240" height="60" topLeftX="120" topLeftY="100" type="text" id="bmm">
1585
+ <content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
1586
+ <p>Vertical edge graze</p>
1587
+ </content>
1588
+ </shape>
1589
+ <line id="bmX" startX="120.00000000000001" startY="90" endX="120.00000000000001" endY="150.00833275470998">
1590
+ <border color="rgba(0, 0, 0, 1)"/>
1591
+ </line>
1592
+ </data>
1593
+ </slide>
1594
+ """
1595
+ )
1596
+ self.assertEqual(result["summary"]["error_count"], 0)
1597
+
1598
+ def test_lint_xml_ignores_polyline_crossing_text(self) -> None:
1599
+ # Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 6: the crossing check is scoped to
1600
+ # <line> only, so a <polyline> over text is not flagged.
1601
+ result = xml_text_overlap_lint.lint_xml(
1602
+ """
1603
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1604
+ <data>
1605
+ <shape width="240" height="60" topLeftX="120" topLeftY="100" type="text" id="bmr">
1606
+ <content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
1607
+ <p>Polyline target</p>
1608
+ </content>
1609
+ </shape>
1610
+ <polyline id="bmH" width="270" height="55" topLeftX="110" topLeftY="95">
1611
+ <border color="rgba(0, 0, 0, 1)"/>
1612
+ </polyline>
1613
+ </data>
1614
+ </slide>
1615
+ """
1616
+ )
1617
+ self.assertEqual(result["summary"]["error_count"], 0)
1618
+
1619
+ def test_lint_xml_ignores_line_below_visual_glyph_height(self) -> None:
1620
+ # Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 7: the shape frame is 80px tall but the
1621
+ # single 20px line of glyphs occupies only its top; a line at the frame's lower region grazes
1622
+ # under the visual glyph box (underline look) and must not be flagged.
1623
+ result = xml_text_overlap_lint.lint_xml(
1624
+ """
1625
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
1626
+ <data>
1627
+ <shape width="240" height="80" topLeftX="120" topLeftY="100" type="text" id="bmB">
1628
+ <content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
1629
+ <p>Visual height target</p>
1630
+ </content>
1631
+ </shape>
1632
+ <line id="bmQ" startX="110" startY="150" endX="380.00185184550116" endY="150">
1633
+ <border color="rgba(0, 0, 0, 1)"/>
1634
+ </line>
1635
+ </data>
1636
+ </slide>
1637
+ """
1638
+ )
1639
+ self.assertEqual(result["summary"]["error_count"], 0)
903
1640
 
904
1641
  def test_lint_xml_uses_rotated_text_and_chart_bounds_for_canvas_validation(self) -> None:
905
1642
  result = xml_text_overlap_lint.lint_xml(
@@ -922,13 +1659,13 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
922
1659
  self.assertEqual(issues_by_element["rotated-chart"]["code"], "chart_out_of_canvas")
923
1660
  self.assertAlmostEqual(issues_by_element["rotated-chart"]["overflow"]["right"], 20.710678, places=5)
924
1661
 
925
- def test_lint_xml_uses_rotated_bounds_for_rect_and_image_canvas_validation(self) -> None:
1662
+ def test_lint_xml_uses_declared_bounds_for_rect_and_ignores_images(self) -> None:
926
1663
  result = xml_text_overlap_lint.lint_xml(
927
1664
  """
928
1665
  <presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
929
1666
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
930
1667
  <data>
931
- <shape id="rotated-rect" type="rect" topLeftX="0" topLeftY="0" width="100" height="100" rotation="45"/>
1668
+ <shape id="rotated-rect" type="rect" topLeftX="900" topLeftY="0" width="100" height="100" rotation="45"/>
932
1669
  <img id="rotated-image" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45"/>
933
1670
  </data>
934
1671
  </slide>
@@ -936,12 +1673,54 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
936
1673
  """
937
1674
  )
938
1675
  issues_by_element = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
939
- self.assertEqual(result["summary"]["error_count"], 2)
1676
+ self.assertEqual(result["summary"]["error_count"], 1)
940
1677
  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)
1678
+ self.assertEqual(issues_by_element["rotated-rect"]["overflow"], {"left": 0, "top": 0, "right": 40, "bottom": 0})
1679
+ self.assertNotIn("rotated-image", issues_by_element)
1680
+
1681
+ def test_detect_elements_out_of_canvas_limits_detection_to_whitelist(self) -> None:
1682
+ issues = xml_text_overlap_lint.detect_elements_out_of_canvas(
1683
+ [
1684
+ {"id": "table", "kind": "table", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 45},
1685
+ {"id": "chart", "kind": "chart", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 0},
1686
+ {
1687
+ "id": "text",
1688
+ "kind": "shape",
1689
+ "type": "text",
1690
+ "x": 95,
1691
+ "y": 0,
1692
+ "width": 10,
1693
+ "height": 10,
1694
+ "rotation": 0,
1695
+ },
1696
+ {
1697
+ "id": "rect",
1698
+ "kind": "shape",
1699
+ "type": "rect",
1700
+ "x": 95,
1701
+ "y": 0,
1702
+ "width": 10,
1703
+ "height": 10,
1704
+ "rotation": 45,
1705
+ },
1706
+ {"id": "image", "kind": "img", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 0},
1707
+ {
1708
+ "id": "ellipse",
1709
+ "kind": "shape",
1710
+ "type": "ellipse",
1711
+ "x": 95,
1712
+ "y": 0,
1713
+ "width": 10,
1714
+ "height": 10,
1715
+ "rotation": 0,
1716
+ },
1717
+ ],
1718
+ 100,
1719
+ 100,
1720
+ )
1721
+
1722
+ self.assertEqual([issue["elements"] for issue in issues], [["table"], ["chart"], ["text"], ["rect"]])
1723
+ self.assertEqual(issues[-1]["bbox"], {"x": 95, "y": 0, "width": 10, "height": 10})
945
1724
 
946
1725
  def test_lint_xml_treats_non_finite_rotations_as_zero(self) -> None:
947
1726
  result = xml_text_overlap_lint.lint_xml(
@@ -1107,8 +1886,9 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1107
1886
  )
1108
1887
  issues_by_dimension = {issue["dimension"]: issue for issue in result["slides"][0]["issues"]}
1109
1888
  self.assertEqual(result["summary"]["error_count"], 0)
1110
- self.assertEqual(result["summary"]["warning_count"], 2)
1111
- self.assertEqual(issues_by_dimension["width"]["level"], "warning")
1889
+ self.assertEqual(result["summary"]["warning_count"], 0)
1890
+ self.assertEqual(result["summary"]["info_count"], 2)
1891
+ self.assertEqual(issues_by_dimension["width"]["level"], "info")
1112
1892
  self.assertEqual(issues_by_dimension["width"]["code"], "table_resolved_size_mismatch")
1113
1893
  self.assertEqual(issues_by_dimension["width"]["resolved_sizes"], [100, 100, 50])
1114
1894
  self.assertEqual(issues_by_dimension["width"]["resolved_size"], 250)
@@ -1201,7 +1981,7 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1201
1981
  }
1202
1982
  script_path = Path(xml_text_overlap_lint.__file__).resolve()
1203
1983
  with tempfile.TemporaryDirectory() as temp_dir:
1204
- for name, (table_xml, expected_warning_count) in cases.items():
1984
+ for name, (table_xml, expected_info_count) in cases.items():
1205
1985
  with self.subTest(case=name):
1206
1986
  input_path = Path(temp_dir) / f"{name}.xml"
1207
1987
  input_path.write_text(
@@ -1221,9 +2001,10 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1221
2001
  result = json.loads(completed.stdout)
1222
2002
  self.assertEqual(completed.returncode, 0, completed.stderr)
1223
2003
  self.assertEqual(result["summary"]["error_count"], 0)
1224
- self.assertEqual(result["summary"]["warning_count"], expected_warning_count)
2004
+ self.assertEqual(result["summary"]["warning_count"], 0)
2005
+ self.assertEqual(result["summary"]["info_count"], expected_info_count)
1225
2006
  self.assertTrue(
1226
- all(issue["level"] == "warning" for issue in result["slides"][0]["issues"]),
2007
+ all(issue["level"] == "info" for issue in result["slides"][0]["issues"]),
1227
2008
  result["slides"][0]["issues"],
1228
2009
  )
1229
2010
 
@@ -1268,8 +2049,9 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1268
2049
  """
1269
2050
  )
1270
2051
  issue = next(issue for issue in result["slides"][0]["issues"] if issue["code"] == "image_may_cover_vertical_text")
1271
- self.assertEqual(issue["level"], "warning")
2052
+ self.assertEqual(issue["level"], "info")
1272
2053
  self.assertEqual(result["summary"]["error_count"], 0)
2054
+ self.assertEqual(result["summary"]["info_count"], 1)
1273
2055
 
1274
2056
 
1275
2057
  class XmlTextOverlapLintDensityTest(unittest.TestCase):
@@ -1357,7 +2139,7 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
1357
2139
  "id": "sparse_container_content",
1358
2140
  })
1359
2141
  self.assertEqual(issue["measurement"]["container_area"], 151700)
1360
- self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.032)
2142
+ self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.03)
1361
2143
  self.assertEqual(issue["elements"], ["trend-card", "trend-title", "trend-copy"])
1362
2144
  self.assertEqual(issue["element_ids"], ["trend-card", "trend-title", "trend-copy"])
1363
2145
  self.assertEqual(
@@ -1842,9 +2624,9 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
1842
2624
  # Must match the visual bbox that should_flag_overlap actually decided with (fontSize=14
1843
2625
  # from extract_elements), not the fontSize=96 max-descendant value that
1844
2626
  # extract_density_elements computes for the same "left" element id.
1845
- self.assertEqual(issue["measurement"]["intersection_width"], 117.04)
2627
+ self.assertEqual(issue["measurement"]["intersection_width"], 109.2)
1846
2628
  self.assertEqual(issue["measurement"]["intersection_height"], 6.8)
1847
- self.assertEqual(issue["measurement"]["intersection_area"], 795.872)
2629
+ self.assertEqual(issue["measurement"]["intersection_area"], 742.56)
1848
2630
 
1849
2631
  def test_has_similar_short_card_peer_excludes_the_element_itself(self) -> None:
1850
2632
  card_a = {"kind": "shape", "type": "rect", "x": 0, "y": 0, "width": 300, "height": 100}