@amaster.ai/pi-lark 0.1.2-beta.44 → 0.1.2-beta.45

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 (122) hide show
  1. package/package.json +2 -2
  2. package/skills/lark-apps/SKILL.md +23 -12
  3. package/skills/lark-apps/creative-design/agents/assets/vision-probe.png +0 -0
  4. package/skills/lark-apps/creative-design/agents/fork-verifier-agent.md +71 -0
  5. package/skills/lark-apps/creative-design/agents/vision-probe-agent.md +41 -0
  6. package/skills/lark-apps/creative-design/assets/index.html +27 -0
  7. package/skills/lark-apps/creative-design/creative-design.md +239 -0
  8. package/skills/lark-apps/creative-design/references/aily.md +39 -0
  9. package/skills/lark-apps/creative-design/references/animated-video.md +34 -0
  10. package/skills/lark-apps/creative-design/references/charts.md +165 -0
  11. package/skills/lark-apps/creative-design/references/claude.md +36 -0
  12. package/skills/lark-apps/creative-design/references/codex.md +32 -0
  13. package/skills/lark-apps/creative-design/references/data-report.md +108 -0
  14. package/skills/lark-apps/creative-design/references/frontend-design.md +71 -0
  15. package/skills/lark-apps/creative-design/references/hi-fi-design.md +32 -0
  16. package/skills/lark-apps/creative-design/references/interactive-prototype.md +24 -0
  17. package/skills/lark-apps/creative-design/references/make-a-deck.md +133 -0
  18. package/skills/lark-apps/creative-design/references/visual-exposure.md +82 -0
  19. package/skills/lark-apps/creative-design/references/wireframe.md +14 -0
  20. package/skills/lark-apps/creative-design/starter-components/android-frame.jsx +188 -0
  21. package/skills/lark-apps/creative-design/starter-components/animations.jsx +773 -0
  22. package/skills/lark-apps/creative-design/starter-components/browser-window.jsx +122 -0
  23. package/skills/lark-apps/creative-design/starter-components/deck-stage.js +2483 -0
  24. package/skills/lark-apps/creative-design/starter-components/design-canvas.jsx +1432 -0
  25. package/skills/lark-apps/creative-design/starter-components/ios-frame.jsx +270 -0
  26. package/skills/lark-apps/creative-design/starter-components/macos-window.jsx +197 -0
  27. package/skills/lark-apps/creative-design/starter-components/tweaks-panel.jsx +752 -0
  28. package/skills/lark-apps/references/lark-apps-automation.md +80 -2
  29. package/skills/lark-apps/references/lark-apps-cloud-dev.md +0 -1
  30. package/skills/lark-apps/references/lark-apps-create.md +1 -2
  31. package/skills/lark-apps/references/lark-apps-db.md +1 -1
  32. package/skills/lark-apps/references/lark-apps-env-pull.md +1 -1
  33. package/skills/lark-apps/references/lark-apps-file.md +1 -1
  34. package/skills/lark-apps/references/lark-apps-git-credential.md +1 -1
  35. package/skills/lark-apps/references/lark-apps-html-publish.md +4 -8
  36. package/skills/lark-apps/references/lark-apps-init.md +1 -1
  37. package/skills/lark-apps/references/lark-apps-list.md +1 -1
  38. package/skills/lark-apps/references/lark-apps-local-dev.md +54 -11
  39. package/skills/lark-apps/references/lark-apps-openapi-key.md +1 -1
  40. package/skills/lark-apps/references/lark-apps-release-create.md +2 -2
  41. package/skills/lark-apps/references/lark-apps-release-get.md +3 -3
  42. package/skills/lark-base/SKILL.md +1 -2
  43. package/skills/lark-base/references/lark-base-cell-value.md +3 -3
  44. package/skills/lark-base/references/lark-base-field-create.md +4 -0
  45. package/skills/lark-base/references/lark-base-field-json.md +4 -4
  46. package/skills/lark-base/references/lark-base-field-update.md +17 -1
  47. package/skills/lark-base/references/lark-base-record-batch-create.md +12 -10
  48. package/skills/lark-base/references/lark-base-record-batch-update.md +11 -9
  49. package/skills/lark-base/references/lark-base-record-upsert.md +1 -1
  50. package/skills/lark-calendar/references/lark-calendar-create.md +1 -0
  51. package/skills/lark-calendar/references/lark-calendar-update.md +3 -0
  52. package/skills/lark-doc/references/lark-doc-fetch.md +10 -2
  53. package/skills/lark-doc/references/lark-doc-whiteboard.md +9 -8
  54. package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +41 -0
  55. package/skills/lark-doc/references/lark-doc-xml.md +3 -2
  56. package/skills/lark-drive/SKILL.md +4 -1
  57. package/skills/lark-drive/references/lark-drive-comment-location.md +2 -2
  58. package/skills/lark-drive/references/lark-drive-upload.md +1 -0
  59. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-execute.md +273 -0
  60. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-recall.md +202 -0
  61. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-resolve-verify.md +231 -0
  62. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-review-plan.md +248 -0
  63. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-setup.md +174 -0
  64. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector.md +202 -0
  65. package/skills/lark-drive/references/lark-drive-workflow.md +5 -4
  66. package/skills/lark-event/SKILL.md +1 -0
  67. package/skills/lark-event/references/lark-event-application.md +38 -0
  68. package/skills/lark-im/SKILL.md +1 -1
  69. package/skills/lark-im/references/lark-im-flag-list.md +8 -7
  70. package/skills/lark-okr/SKILL.md +71 -26
  71. package/skills/lark-okr/references/lark-okr-batch-create.md +19 -18
  72. package/skills/lark-okr/references/lark-okr-create.md +173 -0
  73. package/skills/lark-okr/references/lark-okr-cycle-list.md +17 -7
  74. package/skills/lark-okr/references/lark-okr-entities.md +1 -0
  75. package/skills/lark-okr/references/lark-okr-indicator-update.md +3 -1
  76. package/skills/lark-okr/references/lark-okr-indicators.md +61 -12
  77. package/skills/lark-okr/references/lark-okr-progress-list.md +21 -9
  78. package/skills/lark-slides/SKILL.md +103 -46
  79. package/skills/lark-slides/references/asset-planning.md +6 -4
  80. package/skills/lark-slides/references/iconpark.md +2 -2
  81. package/skills/lark-slides/references/lark-slides-create.md +2 -3
  82. package/skills/lark-slides/references/lark-slides-history.md +132 -0
  83. package/skills/lark-slides/references/lark-slides-media-upload.md +1 -2
  84. package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +7 -11
  85. package/skills/lark-slides/references/lark-slides-replace-slide.md +0 -3
  86. package/skills/lark-slides/references/lark-slides-screenshot.md +1 -1
  87. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-create.md +219 -0
  88. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +6 -5
  89. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +2 -2
  90. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +2 -3
  91. package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +65 -30
  92. package/skills/lark-slides/references/planning-layer.md +11 -10
  93. package/skills/lark-slides/references/slides_chart_demo.xml +1416 -1
  94. package/skills/lark-slides/references/slides_xml_schema_definition.xml +1 -45
  95. package/skills/lark-slides/references/troubleshooting.md +25 -7
  96. package/skills/lark-slides/references/validation-checklist.md +33 -13
  97. package/skills/lark-slides/references/visual-planning.md +25 -22
  98. package/skills/lark-slides/references/xml-schema-quick-ref.md +225 -46
  99. package/skills/lark-slides/scripts/xml_text_overlap_lint.py +223 -22
  100. package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +183 -124
  101. package/skills/lark-whiteboard/SKILL.md +13 -12
  102. package/skills/lark-whiteboard/elements/layout.md +1 -1
  103. package/skills/lark-whiteboard/elements/schema.md +2 -2
  104. package/skills/lark-whiteboard/references/{lark-whiteboard-query.md → lark-whiteboard-export.md} +15 -15
  105. package/skills/lark-whiteboard/references/lark-whiteboard-update.md +3 -3
  106. package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +7 -17
  107. package/skills/lark-whiteboard/routes/dsl.md +3 -3
  108. package/skills/lark-whiteboard/routes/mermaid.md +2 -2
  109. package/skills/lark-whiteboard/routes/svg-edit.md +4 -4
  110. package/skills/lark-whiteboard/routes/svg.md +11 -6
  111. package/skills/lark-whiteboard/scenes/bar-chart.md +1 -1
  112. package/skills/lark-whiteboard/scenes/fishbone.md +1 -1
  113. package/skills/lark-whiteboard/scenes/flywheel.md +1 -1
  114. package/skills/lark-whiteboard/scenes/line-chart.md +1 -1
  115. package/skills/lark-whiteboard/scenes/treemap.md +1 -1
  116. package/skills/lark-wiki/SKILL.md +1 -0
  117. package/skills/lark-slides/references/examples.md +0 -91
  118. package/skills/lark-slides/references/lark-slides-whiteboard.md +0 -331
  119. package/skills/lark-slides/references/lark-slides-xml-get.md +0 -100
  120. package/skills/lark-slides/references/slide-templates.md +0 -201
  121. package/skills/lark-slides/references/slides_demo.xml +0 -226
  122. package/skills/lark-slides/references/xml-format-guide.md +0 -433
@@ -38,6 +38,10 @@ SXSD_ATTR_ALIASES = {
38
38
  "fontColor": "color",
39
39
  }
40
40
  SERVER_FILLED_SXSD_ATTRS = {"id"}
41
+ ROUNDTRIP_SXSD_ATTRS = {
42
+ ("chart", "updated"),
43
+ ("chartData", "isStaticData"),
44
+ }
41
45
  DEFAULT_TABLE_COLUMN_WIDTH = 110
42
46
  DEFAULT_TABLE_ROW_HEIGHT = 37
43
47
  _SXSD_TAG_ATTRIBUTES_CACHE: dict[str, set[str]] | None = None
@@ -62,7 +66,7 @@ def parse_args(argv: list[str]) -> dict[str, Any]:
62
66
  while index < len(argv):
63
67
  token = argv[index]
64
68
  if not token.startswith("--"):
65
- fail(f"unexpected argument: {token}")
69
+ fail(f"unexpected argument: {token}, need --input")
66
70
  key = token[2:]
67
71
  next_token = argv[index + 1] if index + 1 < len(argv) else None
68
72
  if next_token is None or next_token.startswith("--"):
@@ -168,8 +172,10 @@ def solve_weighted_min_layout(
168
172
  return {"final_sizes": final_sizes, "actual_size": sum_sizes(final_sizes), "ratio": ratio}
169
173
 
170
174
 
171
- def strip_xml(value: str) -> str:
175
+ def strip_xml(value: str, preserve_line_breaks: bool = False) -> str:
172
176
  stripped = re.sub(r"<!\[CDATA\[([\s\S]*?)\]\]>", r"\1", value)
177
+ if preserve_line_breaks:
178
+ stripped = re.sub(r"<br\b[^>]*>", "\n", stripped)
173
179
  stripped = re.sub(r"<[^>]+>", " ", stripped)
174
180
  stripped = stripped.replace("&nbsp;", " ")
175
181
  stripped = stripped.replace("&amp;", "&")
@@ -177,14 +183,46 @@ def strip_xml(value: str) -> str:
177
183
  stripped = stripped.replace("&gt;", ">")
178
184
  stripped = stripped.replace("&quot;", '"')
179
185
  stripped = stripped.replace("&#39;", "'")
186
+ if preserve_line_breaks:
187
+ return "\n".join(re.sub(r"\s+", " ", line).strip() for line in stripped.split("\n"))
180
188
  return re.sub(r"\s+", " ", stripped).strip()
181
189
 
182
190
 
183
191
  def strip_xml_paragraphs(value: str) -> str:
184
192
  paragraphs = re.findall(r"<p\b[^>]*>([\s\S]*?)</p\s*>", value)
185
193
  if paragraphs:
186
- return "\n".join(strip_xml(paragraph) for paragraph in paragraphs)
187
- return strip_xml(value)
194
+ return "\n".join(strip_xml(paragraph, preserve_line_breaks=True) for paragraph in paragraphs)
195
+ return strip_xml(value, preserve_line_breaks=True)
196
+
197
+
198
+ def extract_text_paragraphs(value: str, default_font_size: int | float) -> list[dict[str, Any]]:
199
+ paragraphs = []
200
+ for attrs, body in re.findall(r"<p\b([^>]*)>([\s\S]*?)</p\s*>", value):
201
+ paragraphs.append(
202
+ {
203
+ "text": strip_xml(body, preserve_line_breaks=True),
204
+ "fontSize": extract_max_span_font_size(body, default_font_size),
205
+ "textAlign": extract_attribute(attrs, "textAlign"),
206
+ "lineSpacing": extract_attribute(attrs, "lineSpacing"),
207
+ "beforeLineSpacing": extract_attribute(attrs, "beforeLineSpacing"),
208
+ "afterLineSpacing": extract_attribute(attrs, "afterLineSpacing"),
209
+ }
210
+ )
211
+ return paragraphs
212
+
213
+
214
+ def extract_max_span_font_size(value: str, default_font_size: int | float) -> int | float:
215
+ font_sizes = [
216
+ font_size
217
+ for attrs in re.findall(r"<span\b([^>]*)>", value)
218
+ if (font_size := extract_numeric_attribute(attrs, "fontSize")) is not None
219
+ ]
220
+ return max([default_font_size, *font_sizes])
221
+
222
+
223
+ def extract_tag_attributes(value: str, tag: str) -> str:
224
+ match = re.search(fr"<{re.escape(tag)}\b([^>]*)>", value)
225
+ return match.group(1) if match else ""
188
226
 
189
227
 
190
228
  def xml_local_name(tag: str) -> str:
@@ -319,8 +357,8 @@ def should_skip_sxsd_subtree(element: ET.Element, ancestors: list[str]) -> bool:
319
357
  return "whiteboard" in ancestors and xml_namespace(element.tag) == SVG_NS
320
358
 
321
359
 
322
- def should_skip_sxsd_attribute(attr_name: str) -> bool:
323
- return attr_name in SERVER_FILLED_SXSD_ATTRS
360
+ def should_skip_sxsd_attribute(tag_name: str, attr_name: str) -> bool:
361
+ return attr_name in SERVER_FILLED_SXSD_ATTRS or (tag_name, attr_name) in ROUNDTRIP_SXSD_ATTRS
324
362
 
325
363
 
326
364
  def validate_sxsd_tag_attributes(root: ET.Element) -> list[dict[str, Any]]:
@@ -352,7 +390,7 @@ def validate_sxsd_tag_attributes(root: ET.Element) -> list[dict[str, Any]]:
352
390
  if raw_attr_name.startswith(XML_NS):
353
391
  continue
354
392
  attr_name = xml_local_name(raw_attr_name)
355
- if should_skip_sxsd_attribute(attr_name):
393
+ if should_skip_sxsd_attribute(tag_name, attr_name):
356
394
  continue
357
395
  if attr_name in allowed_attrs:
358
396
  continue
@@ -606,6 +644,7 @@ def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
606
644
  width = extract_numeric_attribute(attrs, "width")
607
645
  height = extract_numeric_attribute(attrs, "height")
608
646
  rotation = extract_numeric_attribute(attrs, "rotation") or 0
647
+ alpha = extract_numeric_attribute(attrs, "alpha")
609
648
  table_layouts: dict[str, dict[str, Any] | None] = {}
610
649
  if kind == "table":
611
650
  width, table_layouts["width"] = resolve_table_dimension(
@@ -624,6 +663,7 @@ def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
624
663
  "width": width,
625
664
  "height": height,
626
665
  "rotation": rotation,
666
+ "alpha": alpha if alpha is not None else 1,
627
667
  "order": len(elements),
628
668
  }
629
669
  if kind == "table":
@@ -635,15 +675,28 @@ def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
635
675
  }
636
676
  )
637
677
  if kind == "shape":
678
+ content_attrs = extract_tag_attributes(content, "content")
679
+ font_size = extract_numeric_attribute(content_attrs, "fontSize")
680
+ if font_size is None:
681
+ font_size = extract_numeric_attribute(attrs, "fontSize")
638
682
  element.update(
639
683
  {
640
- "textType": extract_attribute(content, "textType"),
641
- "textAlign": extract_attribute(content, "textAlign"),
642
- "autoFit": extract_attribute(content, "autoFit"),
643
- "fontSize": float(
644
- extract_attribute(content, "fontSize") or extract_attribute(attrs, "fontSize") or 16
645
- ),
684
+ "textType": extract_attribute(content_attrs, "textType"),
685
+ "textAlign": extract_attribute(content_attrs, "textAlign"),
686
+ "verticalAlign": extract_attribute(content_attrs, "verticalAlign") or "middle",
687
+ "vert": extract_attribute(attrs, "vert") or "horz",
688
+ "autoFit": extract_attribute(content_attrs, "autoFit"),
689
+ "wrap": extract_attribute(content_attrs, "wrap"),
690
+ "lineSpacing": extract_attribute(content_attrs, "lineSpacing"),
691
+ "beforeLineSpacing": extract_attribute(content_attrs, "beforeLineSpacing"),
692
+ "afterLineSpacing": extract_attribute(content_attrs, "afterLineSpacing"),
693
+ "paddingTop": extract_numeric_attribute(content_attrs, "paddingTop") or 0,
694
+ "paddingRight": extract_numeric_attribute(content_attrs, "paddingRight") or 0,
695
+ "paddingBottom": extract_numeric_attribute(content_attrs, "paddingBottom") or 0,
696
+ "paddingLeft": extract_numeric_attribute(content_attrs, "paddingLeft") or 0,
697
+ "fontSize": font_size if font_size is not None else 16,
646
698
  "text": strip_xml_paragraphs(content),
699
+ "paragraphs": extract_text_paragraphs(content, font_size if font_size is not None else 16),
647
700
  }
648
701
  )
649
702
  elements.append(element)
@@ -671,6 +724,40 @@ def has_text_content(element: dict[str, Any]) -> bool:
671
724
  return bool(element.get("text"))
672
725
 
673
726
 
727
+ def is_vertical_text(element: dict[str, Any]) -> bool:
728
+ return element.get("vert") in {"vert", "vert270", "word-art-vert", "word-art-vert-rtl", "ea-vert"}
729
+
730
+
731
+ def detect_image_text_occlusions(elements: list[dict[str, Any]]) -> list[dict[str, Any]]:
732
+ issues: list[dict[str, Any]] = []
733
+ text_elements = [element for element in elements if is_text_element(element) and has_text_content(element)]
734
+ image_elements = [element for element in elements if element["kind"] == "img" and element["alpha"] > 0]
735
+ for text_element in text_elements:
736
+ for image_element in image_elements:
737
+ if image_element["order"] <= text_element["order"]:
738
+ continue
739
+ if is_vertical_text(text_element):
740
+ if intersects(image_element, text_element):
741
+ issues.append({
742
+ "level": "info",
743
+ "code": "image_may_cover_vertical_text",
744
+ "elements": [image_element["id"], text_element["id"]],
745
+ "message": f'image {image_element["id"]} may cover vertical text shape {text_element["id"]}',
746
+ "hint": "Inspect the rendered slide because vertical text layout is not statically modeled.",
747
+ })
748
+ continue
749
+ text_visual_bbox = estimate_text_visual_bbox(text_element)
750
+ if text_visual_bbox is not None and intersects(image_element, text_visual_bbox):
751
+ issues.append({
752
+ "level": "error",
753
+ "code": "image_covers_text",
754
+ "elements": [image_element["id"], text_element["id"]],
755
+ "message": f'image {image_element["id"]} covers text shape {text_element["id"]}',
756
+ "hint": "Move the image before the text shape in XML order, or adjust the image and text shape coordinates or dimensions.",
757
+ })
758
+ return issues
759
+
760
+
674
761
  def is_decorative_text(element: dict[str, Any]) -> bool:
675
762
  text = element.get("text") or ""
676
763
  return bool(text) and re.search(r"[A-Za-z0-9\u4e00-\u9fff]", text) is None
@@ -708,27 +795,135 @@ def is_similar_text_overlay(left: dict[str, Any], right: dict[str, Any]) -> bool
708
795
  return SequenceMatcher(None, left_text, right_text).ratio() >= 0.75
709
796
 
710
797
 
711
- def estimate_text_line_count(element: dict[str, Any]) -> int:
798
+ def estimate_text_line_count_for_text(element: dict[str, Any], text: str) -> int:
712
799
  font_size = element["fontSize"] if isinstance(element["fontSize"], (int, float)) else 16
713
- paragraphs = [paragraph for paragraph in re.split(r"\n+", element["text"]) if paragraph]
800
+ hard_lines = text.split("\n")
801
+ if not text:
802
+ return 0
714
803
  line_count = 0
715
- for paragraph in paragraphs:
716
- logical_width = max(estimate_text_width(paragraph, font_size), 1)
804
+ for hard_line in hard_lines:
805
+ if element.get("wrap") in {"false", "0"}:
806
+ line_count += 1
807
+ continue
808
+ logical_width = max(estimate_text_width(hard_line, font_size), 1)
717
809
  line_count += max(1, math.ceil(logical_width / max(element["width"], 1)))
718
- return max(line_count, 1)
810
+ return line_count
811
+
812
+
813
+ def estimate_text_line_count(element: dict[str, Any]) -> int:
814
+ return max(estimate_text_line_count_for_text(element, element["text"]), 1)
815
+
816
+
817
+ def estimate_text_line_height(element: dict[str, Any], line_spacing: str | None = None) -> int | float | None:
818
+ font_size = element["fontSize"] if isinstance(element["fontSize"], (int, float)) else 16
819
+ line_spacing = line_spacing or "multiple:1.5"
820
+ match = re.fullmatch(r"(multiple|fixed):([0-9]+(?:\.[0-9]+)?)", line_spacing)
821
+ if match is None:
822
+ return None
823
+ spacing_type, value = match.groups()
824
+ return font_size * float(value) if spacing_type == "multiple" else float(value)
825
+
826
+
827
+ def detect_text_may_overflow_shapes(elements: list[dict[str, Any]]) -> list[dict[str, Any]]:
828
+ issues: list[dict[str, Any]] = []
829
+ for element in elements:
830
+ if not is_text_element(element) or not has_text_content(element):
831
+ continue
832
+ if element.get("autoFit") in {"normal-auto-fit", "shape-auto-fit"}:
833
+ continue
834
+
835
+ font_size = element["fontSize"] if isinstance(element["fontSize"], (int, float)) else 16
836
+ paragraphs = element.get("paragraphs") or [
837
+ {
838
+ "text": element["text"],
839
+ "lineSpacing": None,
840
+ "beforeLineSpacing": None,
841
+ "afterLineSpacing": None,
842
+ }
843
+ ]
844
+ line_count = 0
845
+ estimated_height = 0.0
846
+ line_heights: list[int | float] = []
847
+ for paragraph in paragraphs:
848
+ paragraph_line_count = estimate_text_line_count_for_text(element, paragraph["text"])
849
+ if paragraph_line_count == 0:
850
+ continue
851
+ line_height = estimate_text_line_height(element, paragraph["lineSpacing"] or element["lineSpacing"])
852
+ before_spacing = estimate_text_line_height(
853
+ element, paragraph["beforeLineSpacing"] or element["beforeLineSpacing"] or "fixed:0"
854
+ )
855
+ after_spacing = estimate_text_line_height(
856
+ element, paragraph["afterLineSpacing"] or element["afterLineSpacing"] or "fixed:0"
857
+ )
858
+ if line_height is None or before_spacing is None or after_spacing is None:
859
+ line_count = 0
860
+ break
861
+ first_line_height = font_size if line_count == 0 else line_height
862
+ line_count += paragraph_line_count
863
+ line_heights.append(line_height)
864
+ estimated_height += (
865
+ before_spacing + first_line_height + max(paragraph_line_count - 1, 0) * line_height + after_spacing
866
+ )
867
+ if line_count == 0:
868
+ continue
869
+ available_height = max(element["height"] - element["paddingTop"] - element["paddingBottom"], 0)
870
+ overflow = estimated_height - available_height
871
+ if overflow <= 0:
872
+ continue
873
+
874
+ issues.append(
875
+ {
876
+ "level": "warning",
877
+ "code": "text_may_overflow_shape",
878
+ "elements": [element["id"]],
879
+ "line_count": line_count,
880
+ "line_height": max(line_heights),
881
+ "estimated_height": estimated_height,
882
+ "available_height": available_height,
883
+ "overflow": overflow,
884
+ "message": (
885
+ f'text shape {element["id"]} may overflow its own content box '
886
+ f'(estimated {estimated_height:g}px, available {available_height:g}px); '
887
+ 'consider setting content wrap="true" autoFit="normal-auto-fit"'
888
+ ),
889
+ "hint": (
890
+ "Increase shape.height, reduce the text, or set content wrap=\"true\" "
891
+ "autoFit=\"normal-auto-fit\". "
892
+ "This is an estimate based on font size, line spacing, and wrapped line count."
893
+ ),
894
+ }
895
+ )
896
+ return issues
719
897
 
720
898
 
721
899
  def estimate_text_visual_bbox(element: dict[str, Any]) -> dict[str, int | float] | None:
722
900
  if not is_text_element(element) or not has_text_content(element) or is_decorative_text(element):
723
901
  return None
724
902
 
903
+ padding_left = element.get("paddingLeft", 0)
904
+ padding_right = element.get("paddingRight", 0)
905
+ padding_top = element.get("paddingTop", 0)
906
+ padding_bottom = element.get("paddingBottom", 0)
907
+ content_width = max(element["width"] - padding_left - padding_right, 0)
908
+ content_height = max(element["height"] - padding_top - padding_bottom, 0)
725
909
  font_size = element["fontSize"] if isinstance(element["fontSize"], (int, float)) else 16
726
910
  line_count = estimate_text_line_count(element)
727
- visual_width = min(element["width"], max(1, estimate_text_max_line_width(element)))
728
- visual_height = min(element["height"], max(1, line_count * font_size * 1.2))
911
+ estimated_width = max(1, estimate_text_max_line_width(element))
912
+ visual_width = estimated_width if element.get("wrap") in {"false", "0"} else min(content_width, estimated_width)
913
+ visual_height = min(content_height, max(1, line_count * font_size * 1.2))
914
+ x = element["x"] + padding_left
915
+ if element.get("textAlign") == "center":
916
+ x += (content_width - visual_width) / 2
917
+ elif element.get("textAlign") == "right":
918
+ x += content_width - visual_width
919
+ y = element["y"] + padding_top
920
+ if element.get("verticalAlign") == "middle":
921
+ y += (content_height - visual_height) / 2
922
+ elif element.get("verticalAlign") == "bottom":
923
+ y += content_height - visual_height
729
924
  return {
730
- "x": element["x"],
731
- "y": element["y"],
925
+ "x": x,
926
+ "y": y,
732
927
  "width": visual_width,
733
928
  "height": visual_height,
734
929
  }
@@ -818,6 +1013,10 @@ def should_flag_horizontal_text_overflow(left: dict[str, Any], right: dict[str,
818
1013
  source, target = sorted([left, right], key=lambda element: element["x"])
819
1014
  if source["x"] == target["x"]:
820
1015
  return False
1016
+ wrap_enabled = source.get("wrap") not in {"false", "0"}
1017
+ has_horizontal_gap = source["x"] + source["width"] <= target["x"]
1018
+ if wrap_enabled and has_horizontal_gap:
1019
+ return False
821
1020
  if source.get("autoFit") == "normal-auto-fit":
822
1021
  return False
823
1022
  if source.get("textAlign") in {"center", "right"}:
@@ -1116,6 +1315,8 @@ def lint_slide(
1116
1315
  *detect_whiteboard_external_overlaps(elements, slide_width, slide_height),
1117
1316
  *detect_elements_out_of_canvas(elements, slide_width, slide_height),
1118
1317
  *detect_table_layout_size_mismatches(elements),
1318
+ *detect_text_may_overflow_shapes(elements),
1319
+ *detect_image_text_occlusions(elements),
1119
1320
  ]
1120
1321
 
1121
1322
  for index, left in enumerate(elements):