@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.
- package/package.json +2 -2
- package/skills/lark-apps/SKILL.md +23 -12
- package/skills/lark-apps/creative-design/agents/assets/vision-probe.png +0 -0
- package/skills/lark-apps/creative-design/agents/fork-verifier-agent.md +71 -0
- package/skills/lark-apps/creative-design/agents/vision-probe-agent.md +41 -0
- package/skills/lark-apps/creative-design/assets/index.html +27 -0
- package/skills/lark-apps/creative-design/creative-design.md +239 -0
- package/skills/lark-apps/creative-design/references/aily.md +39 -0
- package/skills/lark-apps/creative-design/references/animated-video.md +34 -0
- package/skills/lark-apps/creative-design/references/charts.md +165 -0
- package/skills/lark-apps/creative-design/references/claude.md +36 -0
- package/skills/lark-apps/creative-design/references/codex.md +32 -0
- package/skills/lark-apps/creative-design/references/data-report.md +108 -0
- package/skills/lark-apps/creative-design/references/frontend-design.md +71 -0
- package/skills/lark-apps/creative-design/references/hi-fi-design.md +32 -0
- package/skills/lark-apps/creative-design/references/interactive-prototype.md +24 -0
- package/skills/lark-apps/creative-design/references/make-a-deck.md +133 -0
- package/skills/lark-apps/creative-design/references/visual-exposure.md +82 -0
- package/skills/lark-apps/creative-design/references/wireframe.md +14 -0
- package/skills/lark-apps/creative-design/starter-components/android-frame.jsx +188 -0
- package/skills/lark-apps/creative-design/starter-components/animations.jsx +773 -0
- package/skills/lark-apps/creative-design/starter-components/browser-window.jsx +122 -0
- package/skills/lark-apps/creative-design/starter-components/deck-stage.js +2483 -0
- package/skills/lark-apps/creative-design/starter-components/design-canvas.jsx +1432 -0
- package/skills/lark-apps/creative-design/starter-components/ios-frame.jsx +270 -0
- package/skills/lark-apps/creative-design/starter-components/macos-window.jsx +197 -0
- package/skills/lark-apps/creative-design/starter-components/tweaks-panel.jsx +752 -0
- package/skills/lark-apps/references/lark-apps-automation.md +80 -2
- package/skills/lark-apps/references/lark-apps-cloud-dev.md +0 -1
- package/skills/lark-apps/references/lark-apps-create.md +1 -2
- package/skills/lark-apps/references/lark-apps-db.md +1 -1
- package/skills/lark-apps/references/lark-apps-env-pull.md +1 -1
- package/skills/lark-apps/references/lark-apps-file.md +1 -1
- package/skills/lark-apps/references/lark-apps-git-credential.md +1 -1
- package/skills/lark-apps/references/lark-apps-html-publish.md +4 -8
- package/skills/lark-apps/references/lark-apps-init.md +1 -1
- package/skills/lark-apps/references/lark-apps-list.md +1 -1
- package/skills/lark-apps/references/lark-apps-local-dev.md +54 -11
- package/skills/lark-apps/references/lark-apps-openapi-key.md +1 -1
- package/skills/lark-apps/references/lark-apps-release-create.md +2 -2
- package/skills/lark-apps/references/lark-apps-release-get.md +3 -3
- package/skills/lark-base/SKILL.md +1 -2
- package/skills/lark-base/references/lark-base-cell-value.md +3 -3
- package/skills/lark-base/references/lark-base-field-create.md +4 -0
- package/skills/lark-base/references/lark-base-field-json.md +4 -4
- package/skills/lark-base/references/lark-base-field-update.md +17 -1
- package/skills/lark-base/references/lark-base-record-batch-create.md +12 -10
- package/skills/lark-base/references/lark-base-record-batch-update.md +11 -9
- package/skills/lark-base/references/lark-base-record-upsert.md +1 -1
- package/skills/lark-calendar/references/lark-calendar-create.md +1 -0
- package/skills/lark-calendar/references/lark-calendar-update.md +3 -0
- package/skills/lark-doc/references/lark-doc-fetch.md +10 -2
- package/skills/lark-doc/references/lark-doc-whiteboard.md +9 -8
- package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +41 -0
- package/skills/lark-doc/references/lark-doc-xml.md +3 -2
- package/skills/lark-drive/SKILL.md +4 -1
- package/skills/lark-drive/references/lark-drive-comment-location.md +2 -2
- package/skills/lark-drive/references/lark-drive-upload.md +1 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-execute.md +273 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-recall.md +202 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-resolve-verify.md +231 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-review-plan.md +248 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-setup.md +174 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector.md +202 -0
- package/skills/lark-drive/references/lark-drive-workflow.md +5 -4
- package/skills/lark-event/SKILL.md +1 -0
- package/skills/lark-event/references/lark-event-application.md +38 -0
- package/skills/lark-im/SKILL.md +1 -1
- package/skills/lark-im/references/lark-im-flag-list.md +8 -7
- package/skills/lark-okr/SKILL.md +71 -26
- package/skills/lark-okr/references/lark-okr-batch-create.md +19 -18
- package/skills/lark-okr/references/lark-okr-create.md +173 -0
- package/skills/lark-okr/references/lark-okr-cycle-list.md +17 -7
- package/skills/lark-okr/references/lark-okr-entities.md +1 -0
- package/skills/lark-okr/references/lark-okr-indicator-update.md +3 -1
- package/skills/lark-okr/references/lark-okr-indicators.md +61 -12
- package/skills/lark-okr/references/lark-okr-progress-list.md +21 -9
- package/skills/lark-slides/SKILL.md +103 -46
- package/skills/lark-slides/references/asset-planning.md +6 -4
- package/skills/lark-slides/references/iconpark.md +2 -2
- package/skills/lark-slides/references/lark-slides-create.md +2 -3
- package/skills/lark-slides/references/lark-slides-history.md +132 -0
- package/skills/lark-slides/references/lark-slides-media-upload.md +1 -2
- package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +7 -11
- package/skills/lark-slides/references/lark-slides-replace-slide.md +0 -3
- package/skills/lark-slides/references/lark-slides-screenshot.md +1 -1
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-create.md +219 -0
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +6 -5
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +2 -2
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +2 -3
- package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +65 -30
- package/skills/lark-slides/references/planning-layer.md +11 -10
- package/skills/lark-slides/references/slides_chart_demo.xml +1416 -1
- package/skills/lark-slides/references/slides_xml_schema_definition.xml +1 -45
- package/skills/lark-slides/references/troubleshooting.md +25 -7
- package/skills/lark-slides/references/validation-checklist.md +33 -13
- package/skills/lark-slides/references/visual-planning.md +25 -22
- package/skills/lark-slides/references/xml-schema-quick-ref.md +225 -46
- package/skills/lark-slides/scripts/xml_text_overlap_lint.py +223 -22
- package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +183 -124
- package/skills/lark-whiteboard/SKILL.md +13 -12
- package/skills/lark-whiteboard/elements/layout.md +1 -1
- package/skills/lark-whiteboard/elements/schema.md +2 -2
- package/skills/lark-whiteboard/references/{lark-whiteboard-query.md → lark-whiteboard-export.md} +15 -15
- package/skills/lark-whiteboard/references/lark-whiteboard-update.md +3 -3
- package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +7 -17
- package/skills/lark-whiteboard/routes/dsl.md +3 -3
- package/skills/lark-whiteboard/routes/mermaid.md +2 -2
- package/skills/lark-whiteboard/routes/svg-edit.md +4 -4
- package/skills/lark-whiteboard/routes/svg.md +11 -6
- package/skills/lark-whiteboard/scenes/bar-chart.md +1 -1
- package/skills/lark-whiteboard/scenes/fishbone.md +1 -1
- package/skills/lark-whiteboard/scenes/flywheel.md +1 -1
- package/skills/lark-whiteboard/scenes/line-chart.md +1 -1
- package/skills/lark-whiteboard/scenes/treemap.md +1 -1
- package/skills/lark-wiki/SKILL.md +1 -0
- package/skills/lark-slides/references/examples.md +0 -91
- package/skills/lark-slides/references/lark-slides-whiteboard.md +0 -331
- package/skills/lark-slides/references/lark-slides-xml-get.md +0 -100
- package/skills/lark-slides/references/slide-templates.md +0 -201
- package/skills/lark-slides/references/slides_demo.xml +0 -226
- 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(" ", " ")
|
|
175
181
|
stripped = stripped.replace("&", "&")
|
|
@@ -177,14 +183,46 @@ def strip_xml(value: str) -> str:
|
|
|
177
183
|
stripped = stripped.replace(">", ">")
|
|
178
184
|
stripped = stripped.replace(""", '"')
|
|
179
185
|
stripped = stripped.replace("'", "'")
|
|
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(
|
|
641
|
-
"textAlign": extract_attribute(
|
|
642
|
-
"
|
|
643
|
-
"
|
|
644
|
-
|
|
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
|
|
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
|
-
|
|
800
|
+
hard_lines = text.split("\n")
|
|
801
|
+
if not text:
|
|
802
|
+
return 0
|
|
714
803
|
line_count = 0
|
|
715
|
-
for
|
|
716
|
-
|
|
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
|
|
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
|
-
|
|
728
|
-
|
|
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":
|
|
731
|
-
"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):
|