@amaster.ai/pi-lark 0.1.2-beta.43 → 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-approval/references/lark-approval-initiate.md +2 -5
- package/skills/lark-approval/references/lark-approval-instances-initiated.md +6 -0
- package/skills/lark-approval/references/lark-approval-tasks-query.md +9 -0
- package/skills/lark-approval/references/lark-approval-tasks-rollback.md +8 -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 +6 -3
- 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-base/references/lark-base-view-set-filter.md +3 -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 +3 -1
- package/skills/lark-event/references/lark-event-application.md +38 -0
- package/skills/lark-event/references/lark-event-approval.md +170 -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 +487 -28
- package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +477 -76
- package/skills/lark-vc/SKILL.md +6 -3
- package/skills/lark-vc/references/vc-domain-boundaries.md +9 -1
- package/skills/lark-vc-agent/SKILL.md +1 -1
- 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 -438
|
@@ -38,6 +38,12 @@ 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
|
+
}
|
|
45
|
+
DEFAULT_TABLE_COLUMN_WIDTH = 110
|
|
46
|
+
DEFAULT_TABLE_ROW_HEIGHT = 37
|
|
41
47
|
_SXSD_TAG_ATTRIBUTES_CACHE: dict[str, set[str]] | None = None
|
|
42
48
|
_ICONPARK_ICON_TYPES_CACHE: set[str] | None = None
|
|
43
49
|
|
|
@@ -60,7 +66,7 @@ def parse_args(argv: list[str]) -> dict[str, Any]:
|
|
|
60
66
|
while index < len(argv):
|
|
61
67
|
token = argv[index]
|
|
62
68
|
if not token.startswith("--"):
|
|
63
|
-
fail(f"unexpected argument: {token}")
|
|
69
|
+
fail(f"unexpected argument: {token}, need --input")
|
|
64
70
|
key = token[2:]
|
|
65
71
|
next_token = argv[index + 1] if index + 1 < len(argv) else None
|
|
66
72
|
if next_token is None or next_token.startswith("--"):
|
|
@@ -88,8 +94,88 @@ def extract_numeric_attribute(tag_source: str, name: str) -> int | float | None:
|
|
|
88
94
|
return int(value) if value.is_integer() else value
|
|
89
95
|
|
|
90
96
|
|
|
91
|
-
def
|
|
97
|
+
def sum_sizes(sizes: list[int | float]) -> int | float:
|
|
98
|
+
return sum(sizes)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_filled_size(size: int | float | None) -> bool:
|
|
102
|
+
return isinstance(size, (int, float)) and math.isfinite(size) and size > 0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def fill_last_size_gap(sizes: list[int | float], target_size: int | float) -> list[int | float]:
|
|
106
|
+
if not sizes:
|
|
107
|
+
return sizes
|
|
108
|
+
final_sizes = [
|
|
109
|
+
size if index == len(sizes) - 1 else max(1, math.floor(size + 0.5))
|
|
110
|
+
for index, size in enumerate(sizes)
|
|
111
|
+
]
|
|
112
|
+
remaining_size = target_size - sum_sizes(final_sizes[:-1])
|
|
113
|
+
if remaining_size >= 1:
|
|
114
|
+
final_sizes[-1] = remaining_size
|
|
115
|
+
return final_sizes
|
|
116
|
+
|
|
117
|
+
size_to_redistribute = 1 - remaining_size
|
|
118
|
+
for index in range(len(final_sizes) - 2, -1, -1):
|
|
119
|
+
reduction = min(final_sizes[index] - 1, size_to_redistribute)
|
|
120
|
+
final_sizes[index] -= reduction
|
|
121
|
+
size_to_redistribute -= reduction
|
|
122
|
+
if size_to_redistribute == 0:
|
|
123
|
+
final_sizes[-1] = 1
|
|
124
|
+
return final_sizes
|
|
125
|
+
|
|
126
|
+
final_sizes[-1] = 1
|
|
127
|
+
return final_sizes
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def solve_weighted_min_layout(
|
|
131
|
+
input_sizes: list[int | float | None], default_size: int | float, target_min_size: int | float | None
|
|
132
|
+
) -> dict[str, Any]:
|
|
133
|
+
filled_indexes: list[int] = []
|
|
134
|
+
empty_indexes: list[int] = []
|
|
135
|
+
base_sizes: list[int | float] = []
|
|
136
|
+
for index, size in enumerate(input_sizes):
|
|
137
|
+
if is_filled_size(size):
|
|
138
|
+
filled_indexes.append(index)
|
|
139
|
+
base_sizes.append(size)
|
|
140
|
+
else:
|
|
141
|
+
empty_indexes.append(index)
|
|
142
|
+
base_sizes.append(0)
|
|
143
|
+
filled_sum = sum_sizes(base_sizes)
|
|
144
|
+
|
|
145
|
+
if target_min_size is None:
|
|
146
|
+
final_sizes = [default_size if index in empty_indexes else size for index, size in enumerate(base_sizes)]
|
|
147
|
+
return {"final_sizes": final_sizes, "actual_size": sum_sizes(final_sizes), "ratio": 1}
|
|
148
|
+
|
|
149
|
+
if not filled_indexes:
|
|
150
|
+
average_size = target_min_size / len(input_sizes)
|
|
151
|
+
final_sizes = fill_last_size_gap([average_size] * len(input_sizes), target_min_size)
|
|
152
|
+
return {"final_sizes": final_sizes, "actual_size": sum_sizes(final_sizes), "ratio": 1}
|
|
153
|
+
|
|
154
|
+
if empty_indexes:
|
|
155
|
+
remaining_size = target_min_size - filled_sum
|
|
156
|
+
final_sizes = [*base_sizes]
|
|
157
|
+
if remaining_size > 0:
|
|
158
|
+
average_size = remaining_size / len(empty_indexes)
|
|
159
|
+
empty_sizes = fill_last_size_gap([average_size] * len(empty_indexes), remaining_size)
|
|
160
|
+
for index, empty_size in zip(empty_indexes, empty_sizes):
|
|
161
|
+
final_sizes[index] = empty_size
|
|
162
|
+
else:
|
|
163
|
+
for index in empty_indexes:
|
|
164
|
+
final_sizes[index] = default_size
|
|
165
|
+
return {"final_sizes": final_sizes, "actual_size": sum_sizes(final_sizes), "ratio": 1}
|
|
166
|
+
|
|
167
|
+
ratio = max(1, target_min_size / filled_sum)
|
|
168
|
+
actual_size = max(target_min_size, filled_sum)
|
|
169
|
+
if ratio == 1:
|
|
170
|
+
return {"final_sizes": [*base_sizes], "actual_size": actual_size, "ratio": ratio}
|
|
171
|
+
final_sizes = fill_last_size_gap([size * ratio for size in base_sizes], actual_size)
|
|
172
|
+
return {"final_sizes": final_sizes, "actual_size": sum_sizes(final_sizes), "ratio": ratio}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def strip_xml(value: str, preserve_line_breaks: bool = False) -> str:
|
|
92
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)
|
|
93
179
|
stripped = re.sub(r"<[^>]+>", " ", stripped)
|
|
94
180
|
stripped = stripped.replace(" ", " ")
|
|
95
181
|
stripped = stripped.replace("&", "&")
|
|
@@ -97,14 +183,46 @@ def strip_xml(value: str) -> str:
|
|
|
97
183
|
stripped = stripped.replace(">", ">")
|
|
98
184
|
stripped = stripped.replace(""", '"')
|
|
99
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"))
|
|
100
188
|
return re.sub(r"\s+", " ", stripped).strip()
|
|
101
189
|
|
|
102
190
|
|
|
103
191
|
def strip_xml_paragraphs(value: str) -> str:
|
|
104
192
|
paragraphs = re.findall(r"<p\b[^>]*>([\s\S]*?)</p\s*>", value)
|
|
105
193
|
if paragraphs:
|
|
106
|
-
return "\n".join(strip_xml(paragraph) for paragraph in paragraphs)
|
|
107
|
-
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 ""
|
|
108
226
|
|
|
109
227
|
|
|
110
228
|
def xml_local_name(tag: str) -> str:
|
|
@@ -239,8 +357,8 @@ def should_skip_sxsd_subtree(element: ET.Element, ancestors: list[str]) -> bool:
|
|
|
239
357
|
return "whiteboard" in ancestors and xml_namespace(element.tag) == SVG_NS
|
|
240
358
|
|
|
241
359
|
|
|
242
|
-
def should_skip_sxsd_attribute(attr_name: str) -> bool:
|
|
243
|
-
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
|
|
244
362
|
|
|
245
363
|
|
|
246
364
|
def validate_sxsd_tag_attributes(root: ET.Element) -> list[dict[str, Any]]:
|
|
@@ -272,7 +390,7 @@ def validate_sxsd_tag_attributes(root: ET.Element) -> list[dict[str, Any]]:
|
|
|
272
390
|
if raw_attr_name.startswith(XML_NS):
|
|
273
391
|
continue
|
|
274
392
|
attr_name = xml_local_name(raw_attr_name)
|
|
275
|
-
if should_skip_sxsd_attribute(attr_name):
|
|
393
|
+
if should_skip_sxsd_attribute(tag_name, attr_name):
|
|
276
394
|
continue
|
|
277
395
|
if attr_name in allowed_attrs:
|
|
278
396
|
continue
|
|
@@ -515,8 +633,8 @@ def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
|
|
|
515
633
|
for match in re.finditer(r"<(shape|img|table|chart|whiteboard)\b([^>]*)>", slide_xml):
|
|
516
634
|
kind, attrs = match.group(1), match.group(2)
|
|
517
635
|
content = ""
|
|
518
|
-
if kind
|
|
519
|
-
close_index = slide_xml.find("</
|
|
636
|
+
if kind in {"shape", "table"}:
|
|
637
|
+
close_index = slide_xml.find(f"</{kind}>", match.end())
|
|
520
638
|
if close_index != -1:
|
|
521
639
|
content = slide_xml[match.end() : close_index]
|
|
522
640
|
|
|
@@ -525,6 +643,16 @@ def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
|
|
|
525
643
|
y = extract_numeric_attribute(attrs, "topLeftY")
|
|
526
644
|
width = extract_numeric_attribute(attrs, "width")
|
|
527
645
|
height = extract_numeric_attribute(attrs, "height")
|
|
646
|
+
rotation = extract_numeric_attribute(attrs, "rotation") or 0
|
|
647
|
+
alpha = extract_numeric_attribute(attrs, "alpha")
|
|
648
|
+
table_layouts: dict[str, dict[str, Any] | None] = {}
|
|
649
|
+
if kind == "table":
|
|
650
|
+
width, table_layouts["width"] = resolve_table_dimension(
|
|
651
|
+
content, width, extract_table_column_sizes, DEFAULT_TABLE_COLUMN_WIDTH
|
|
652
|
+
)
|
|
653
|
+
height, table_layouts["height"] = resolve_table_dimension(
|
|
654
|
+
content, height, extract_table_row_sizes, DEFAULT_TABLE_ROW_HEIGHT
|
|
655
|
+
)
|
|
528
656
|
if all(value is not None for value in [x, y, width, height]):
|
|
529
657
|
element = {
|
|
530
658
|
"id": element_id,
|
|
@@ -534,18 +662,41 @@ def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
|
|
|
534
662
|
"y": y,
|
|
535
663
|
"width": width,
|
|
536
664
|
"height": height,
|
|
665
|
+
"rotation": rotation,
|
|
666
|
+
"alpha": alpha if alpha is not None else 1,
|
|
537
667
|
"order": len(elements),
|
|
538
668
|
}
|
|
669
|
+
if kind == "table":
|
|
670
|
+
element.update(
|
|
671
|
+
{
|
|
672
|
+
"declared_width": extract_numeric_attribute(attrs, "width"),
|
|
673
|
+
"declared_height": extract_numeric_attribute(attrs, "height"),
|
|
674
|
+
"table_layouts": table_layouts,
|
|
675
|
+
}
|
|
676
|
+
)
|
|
539
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")
|
|
540
682
|
element.update(
|
|
541
683
|
{
|
|
542
|
-
"textType": extract_attribute(
|
|
543
|
-
"textAlign": extract_attribute(
|
|
544
|
-
"
|
|
545
|
-
"
|
|
546
|
-
|
|
547
|
-
),
|
|
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,
|
|
548
698
|
"text": strip_xml_paragraphs(content),
|
|
699
|
+
"paragraphs": extract_text_paragraphs(content, font_size if font_size is not None else 16),
|
|
549
700
|
}
|
|
550
701
|
)
|
|
551
702
|
elements.append(element)
|
|
@@ -573,6 +724,40 @@ def has_text_content(element: dict[str, Any]) -> bool:
|
|
|
573
724
|
return bool(element.get("text"))
|
|
574
725
|
|
|
575
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
|
+
|
|
576
761
|
def is_decorative_text(element: dict[str, Any]) -> bool:
|
|
577
762
|
text = element.get("text") or ""
|
|
578
763
|
return bool(text) and re.search(r"[A-Za-z0-9\u4e00-\u9fff]", text) is None
|
|
@@ -610,27 +795,135 @@ def is_similar_text_overlay(left: dict[str, Any], right: dict[str, Any]) -> bool
|
|
|
610
795
|
return SequenceMatcher(None, left_text, right_text).ratio() >= 0.75
|
|
611
796
|
|
|
612
797
|
|
|
613
|
-
def
|
|
798
|
+
def estimate_text_line_count_for_text(element: dict[str, Any], text: str) -> int:
|
|
614
799
|
font_size = element["fontSize"] if isinstance(element["fontSize"], (int, float)) else 16
|
|
615
|
-
|
|
800
|
+
hard_lines = text.split("\n")
|
|
801
|
+
if not text:
|
|
802
|
+
return 0
|
|
616
803
|
line_count = 0
|
|
617
|
-
for
|
|
618
|
-
|
|
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)
|
|
619
809
|
line_count += max(1, math.ceil(logical_width / max(element["width"], 1)))
|
|
620
|
-
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
|
|
621
897
|
|
|
622
898
|
|
|
623
899
|
def estimate_text_visual_bbox(element: dict[str, Any]) -> dict[str, int | float] | None:
|
|
624
900
|
if not is_text_element(element) or not has_text_content(element) or is_decorative_text(element):
|
|
625
901
|
return None
|
|
626
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)
|
|
627
909
|
font_size = element["fontSize"] if isinstance(element["fontSize"], (int, float)) else 16
|
|
628
910
|
line_count = estimate_text_line_count(element)
|
|
629
|
-
|
|
630
|
-
|
|
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
|
|
631
924
|
return {
|
|
632
|
-
"x":
|
|
633
|
-
"y":
|
|
925
|
+
"x": x,
|
|
926
|
+
"y": y,
|
|
634
927
|
"width": visual_width,
|
|
635
928
|
"height": visual_height,
|
|
636
929
|
}
|
|
@@ -720,6 +1013,10 @@ def should_flag_horizontal_text_overflow(left: dict[str, Any], right: dict[str,
|
|
|
720
1013
|
source, target = sorted([left, right], key=lambda element: element["x"])
|
|
721
1014
|
if source["x"] == target["x"]:
|
|
722
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
|
|
723
1020
|
if source.get("autoFit") == "normal-auto-fit":
|
|
724
1021
|
return False
|
|
725
1022
|
if source.get("textAlign") in {"center", "right"}:
|
|
@@ -867,11 +1164,160 @@ def detect_whiteboard_external_overlaps(
|
|
|
867
1164
|
return issues
|
|
868
1165
|
|
|
869
1166
|
|
|
1167
|
+
def element_canvas_bbox(element: dict[str, Any]) -> dict[str, int | float]:
|
|
1168
|
+
bbox = {key: element[key] for key in ("x", "y", "width", "height")}
|
|
1169
|
+
if element["kind"] != "chart" and not (element["kind"] == "shape" and element["type"] == "text"):
|
|
1170
|
+
return bbox
|
|
1171
|
+
|
|
1172
|
+
rotation = element["rotation"]
|
|
1173
|
+
if not isinstance(rotation, (int, float)) or not math.isfinite(rotation):
|
|
1174
|
+
rotation = 0
|
|
1175
|
+
rotation %= 360
|
|
1176
|
+
if math.isclose(rotation, 0, abs_tol=1e-9):
|
|
1177
|
+
return bbox
|
|
1178
|
+
radians = math.radians(rotation)
|
|
1179
|
+
sine = abs(math.sin(radians))
|
|
1180
|
+
cosine = abs(math.cos(radians))
|
|
1181
|
+
sine = 0 if math.isclose(sine, 0, abs_tol=1e-12) else sine
|
|
1182
|
+
cosine = 0 if math.isclose(cosine, 0, abs_tol=1e-12) else cosine
|
|
1183
|
+
rotated_width = element["width"] * cosine + element["height"] * sine
|
|
1184
|
+
rotated_height = element["width"] * sine + element["height"] * cosine
|
|
1185
|
+
return {
|
|
1186
|
+
"x": element["x"] - (rotated_width - element["width"]) / 2,
|
|
1187
|
+
"y": element["y"] - (rotated_height - element["height"]) / 2,
|
|
1188
|
+
"width": rotated_width,
|
|
1189
|
+
"height": rotated_height,
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
def detect_elements_out_of_canvas(
|
|
1194
|
+
elements: list[dict[str, Any]], slide_width: int | float, slide_height: int | float
|
|
1195
|
+
) -> list[dict[str, Any]]:
|
|
1196
|
+
issues: list[dict[str, Any]] = []
|
|
1197
|
+
for element in (
|
|
1198
|
+
element
|
|
1199
|
+
for element in elements
|
|
1200
|
+
if element["kind"] in {"table", "chart"}
|
|
1201
|
+
or (element["kind"] == "shape" and element["type"] == "text")
|
|
1202
|
+
):
|
|
1203
|
+
bbox = element_canvas_bbox(element)
|
|
1204
|
+
overflow = {
|
|
1205
|
+
"left": max(-bbox["x"], 0),
|
|
1206
|
+
"top": max(-bbox["y"], 0),
|
|
1207
|
+
"right": max(bbox["x"] + bbox["width"] - slide_width, 0),
|
|
1208
|
+
"bottom": max(bbox["y"] + bbox["height"] - slide_height, 0),
|
|
1209
|
+
}
|
|
1210
|
+
overflow_details = [
|
|
1211
|
+
f"{side} by {amount:g}px" for side, amount in overflow.items() if amount > 0
|
|
1212
|
+
]
|
|
1213
|
+
if not overflow_details:
|
|
1214
|
+
continue
|
|
1215
|
+
issues.append(
|
|
1216
|
+
{
|
|
1217
|
+
"level": "error",
|
|
1218
|
+
"code": f'{element["kind"]}_out_of_canvas',
|
|
1219
|
+
"elements": [element["id"]],
|
|
1220
|
+
"canvas": {"width": slide_width, "height": slide_height},
|
|
1221
|
+
"bbox": bbox,
|
|
1222
|
+
"overflow": overflow,
|
|
1223
|
+
"message": (
|
|
1224
|
+
f'{element["kind"]} {element["id"]} exceeds the {slide_width:g}x{slide_height:g} canvas '
|
|
1225
|
+
f'({", ".join(overflow_details)})'
|
|
1226
|
+
),
|
|
1227
|
+
"hint": (
|
|
1228
|
+
"Move the table inside the canvas, reduce table.width/table.height, or split the table across "
|
|
1229
|
+
"slides."
|
|
1230
|
+
if element["kind"] == "table"
|
|
1231
|
+
else f'Move the {element["kind"]} inside the canvas or reduce its width/height.'
|
|
1232
|
+
),
|
|
1233
|
+
}
|
|
1234
|
+
)
|
|
1235
|
+
return issues
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def extract_table_column_sizes(table_xml: str) -> list[int | float | None]:
|
|
1239
|
+
sizes: list[int | float | None] = []
|
|
1240
|
+
for match in re.finditer(r"<col\b([^>]*)/?>", table_xml):
|
|
1241
|
+
attrs = match.group(1)
|
|
1242
|
+
span = extract_numeric_attribute(attrs, "span") or 1
|
|
1243
|
+
span_count = int(span) if math.isfinite(span) and span > 0 and float(span).is_integer() else 1
|
|
1244
|
+
sizes.extend([extract_numeric_attribute(attrs, "width")] * span_count)
|
|
1245
|
+
return sizes
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def extract_table_row_sizes(table_xml: str) -> list[int | float | None]:
|
|
1249
|
+
return [extract_numeric_attribute(match.group(1), "height") for match in re.finditer(r"<tr\b([^>]*)>", table_xml)]
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def resolve_table_dimension(
|
|
1253
|
+
table_xml: str,
|
|
1254
|
+
declared_size: int | float | None,
|
|
1255
|
+
extract_sizes: Any,
|
|
1256
|
+
default_size: int | float,
|
|
1257
|
+
) -> tuple[int | float | None, dict[str, Any] | None]:
|
|
1258
|
+
input_sizes = extract_sizes(table_xml)
|
|
1259
|
+
if not input_sizes:
|
|
1260
|
+
return declared_size, None
|
|
1261
|
+
layout = solve_weighted_min_layout(
|
|
1262
|
+
input_sizes, default_size, declared_size if is_filled_size(declared_size) else None
|
|
1263
|
+
)
|
|
1264
|
+
return layout["actual_size"], layout
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def format_size(size: int | float) -> str:
|
|
1268
|
+
return f"{size:g}"
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def detect_table_layout_size_mismatches(elements: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
1272
|
+
issues: list[dict[str, Any]] = []
|
|
1273
|
+
dimensions = {
|
|
1274
|
+
"width": ("col", "column widths"),
|
|
1275
|
+
"height": ("tr", "row heights"),
|
|
1276
|
+
}
|
|
1277
|
+
for table in (element for element in elements if element["kind"] == "table"):
|
|
1278
|
+
for dimension, (child_tag, child_description) in dimensions.items():
|
|
1279
|
+
target_size = table[f"declared_{dimension}"]
|
|
1280
|
+
if not is_filled_size(target_size):
|
|
1281
|
+
continue
|
|
1282
|
+
layout = table["table_layouts"][dimension]
|
|
1283
|
+
if layout is None:
|
|
1284
|
+
continue
|
|
1285
|
+
actual_size = layout["actual_size"]
|
|
1286
|
+
if math.isclose(actual_size, target_size, rel_tol=1e-9, abs_tol=1e-9):
|
|
1287
|
+
continue
|
|
1288
|
+
issues.append(
|
|
1289
|
+
{
|
|
1290
|
+
"level": "info",
|
|
1291
|
+
"code": "table_resolved_size_mismatch",
|
|
1292
|
+
"elements": [table["id"]],
|
|
1293
|
+
"dimension": dimension,
|
|
1294
|
+
"declared_size": target_size,
|
|
1295
|
+
"resolved_size": actual_size,
|
|
1296
|
+
"resolved_sizes": layout["final_sizes"],
|
|
1297
|
+
"message": (
|
|
1298
|
+
f'table {table["id"]} declares {dimension}={format_size(target_size)}px, but its '
|
|
1299
|
+
f"{child_description} resolve to {format_size(actual_size)}px"
|
|
1300
|
+
),
|
|
1301
|
+
"hint": (
|
|
1302
|
+
f"Set table.{dimension} to {format_size(actual_size)}px, or adjust <{child_tag}> sizes "
|
|
1303
|
+
f"so their resolved total matches {format_size(target_size)}px."
|
|
1304
|
+
),
|
|
1305
|
+
}
|
|
1306
|
+
)
|
|
1307
|
+
return issues
|
|
1308
|
+
|
|
1309
|
+
|
|
870
1310
|
def lint_slide(
|
|
871
1311
|
slide_xml: str, slide_number: int, slide_width: int | float = 960, slide_height: int | float = 540
|
|
872
1312
|
) -> dict[str, Any]:
|
|
873
1313
|
elements = extract_elements(slide_xml)
|
|
874
|
-
issues: list[dict[str, Any]] =
|
|
1314
|
+
issues: list[dict[str, Any]] = [
|
|
1315
|
+
*detect_whiteboard_external_overlaps(elements, slide_width, slide_height),
|
|
1316
|
+
*detect_elements_out_of_canvas(elements, slide_width, slide_height),
|
|
1317
|
+
*detect_table_layout_size_mismatches(elements),
|
|
1318
|
+
*detect_text_may_overflow_shapes(elements),
|
|
1319
|
+
*detect_image_text_occlusions(elements),
|
|
1320
|
+
]
|
|
875
1321
|
|
|
876
1322
|
for index, left in enumerate(elements):
|
|
877
1323
|
for right in elements[index + 1 :]:
|
|
@@ -896,7 +1342,7 @@ def lint_xml(xml: str, source_path: str | None = None) -> dict[str, Any]:
|
|
|
896
1342
|
return {
|
|
897
1343
|
"file": source_path,
|
|
898
1344
|
"slide_size": {"width": 960, "height": 540},
|
|
899
|
-
"summary": {"slide_count": 0, "error_count": 1, "warning_count": 0},
|
|
1345
|
+
"summary": {"slide_count": 0, "error_count": 1, "warning_count": 0, "info_count": 0},
|
|
900
1346
|
"issues": [xml_error],
|
|
901
1347
|
"slides": [],
|
|
902
1348
|
}
|
|
@@ -908,10 +1354,16 @@ def lint_xml(xml: str, source_path: str | None = None) -> dict[str, Any]:
|
|
|
908
1354
|
if namespace_issues:
|
|
909
1355
|
error_count = sum(1 for issue in top_level_issues if issue["level"] == "error")
|
|
910
1356
|
warning_count = sum(1 for issue in top_level_issues if issue["level"] == "warning")
|
|
1357
|
+
info_count = sum(1 for issue in top_level_issues if issue["level"] == "info")
|
|
911
1358
|
return {
|
|
912
1359
|
"file": source_path,
|
|
913
1360
|
"slide_size": {"width": 960, "height": 540},
|
|
914
|
-
"summary": {
|
|
1361
|
+
"summary": {
|
|
1362
|
+
"slide_count": 0,
|
|
1363
|
+
"error_count": error_count,
|
|
1364
|
+
"warning_count": warning_count,
|
|
1365
|
+
"info_count": info_count,
|
|
1366
|
+
},
|
|
915
1367
|
"issues": top_level_issues,
|
|
916
1368
|
"slides": [],
|
|
917
1369
|
}
|
|
@@ -924,10 +1376,17 @@ def lint_xml(xml: str, source_path: str | None = None) -> dict[str, Any]:
|
|
|
924
1376
|
error_count += sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "error")
|
|
925
1377
|
warning_count = sum(1 for issue in top_level_issues if issue["level"] == "warning")
|
|
926
1378
|
warning_count += sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "warning")
|
|
1379
|
+
info_count = sum(1 for issue in top_level_issues if issue["level"] == "info")
|
|
1380
|
+
info_count += sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "info")
|
|
927
1381
|
result = {
|
|
928
1382
|
"file": source_path,
|
|
929
1383
|
"slide_size": {"width": presentation["width"], "height": presentation["height"]},
|
|
930
|
-
"summary": {
|
|
1384
|
+
"summary": {
|
|
1385
|
+
"slide_count": len(slides),
|
|
1386
|
+
"error_count": error_count,
|
|
1387
|
+
"warning_count": warning_count,
|
|
1388
|
+
"info_count": info_count,
|
|
1389
|
+
},
|
|
931
1390
|
"slides": slides,
|
|
932
1391
|
}
|
|
933
1392
|
if top_level_issues:
|