@amaster.ai/pi-lark 0.1.4 → 0.1.6

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 (416) hide show
  1. package/package.json +3 -3
  2. package/skills/lark-approval/SKILL.md +99 -0
  3. package/skills/lark-approval/references/lark-approval-approvals-get.md +128 -0
  4. package/skills/lark-approval/references/lark-approval-approvals-search.md +103 -0
  5. package/skills/lark-approval/references/lark-approval-initiate.md +221 -0
  6. package/skills/lark-approval/references/lark-approval-instance-form-control-parameters.md +606 -0
  7. package/skills/lark-approval/references/lark-approval-instance-value-sourcing.md +108 -0
  8. package/skills/lark-approval/references/lark-approval-instances-cancel.md +78 -0
  9. package/skills/lark-approval/references/lark-approval-instances-cc.md +105 -0
  10. package/skills/lark-approval/references/lark-approval-instances-get.md +145 -0
  11. package/skills/lark-approval/references/lark-approval-instances-initiated.md +128 -0
  12. package/skills/lark-approval/references/lark-approval-tasks-add-sign.md +120 -0
  13. package/skills/lark-approval/references/lark-approval-tasks-approve.md +81 -0
  14. package/skills/lark-approval/references/lark-approval-tasks-query.md +85 -0
  15. package/skills/lark-approval/references/lark-approval-tasks-reject.md +73 -0
  16. package/skills/lark-approval/references/lark-approval-tasks-remind.md +82 -0
  17. package/skills/lark-approval/references/lark-approval-tasks-rollback.md +89 -0
  18. package/skills/lark-approval/references/lark-approval-tasks-transfer.md +91 -0
  19. package/skills/lark-apps/SKILL.md +110 -0
  20. package/skills/lark-apps/references/lark-apps-access-scope-get.md +28 -0
  21. package/skills/lark-apps/references/lark-apps-access-scope-set.md +40 -0
  22. package/skills/lark-apps/references/lark-apps-automation.md +164 -0
  23. package/skills/lark-apps/references/lark-apps-cloud-dev.md +120 -0
  24. package/skills/lark-apps/references/lark-apps-create.md +40 -0
  25. package/skills/lark-apps/references/lark-apps-db-execute.md +228 -0
  26. package/skills/lark-apps/references/lark-apps-db.md +162 -0
  27. package/skills/lark-apps/references/lark-apps-env-pull.md +37 -0
  28. package/skills/lark-apps/references/lark-apps-env.md +48 -0
  29. package/skills/lark-apps/references/lark-apps-file.md +96 -0
  30. package/skills/lark-apps/references/lark-apps-get.md +43 -0
  31. package/skills/lark-apps/references/lark-apps-git-credential.md +37 -0
  32. package/skills/lark-apps/references/lark-apps-html-publish.md +62 -0
  33. package/skills/lark-apps/references/lark-apps-init.md +36 -0
  34. package/skills/lark-apps/references/lark-apps-list.md +37 -0
  35. package/skills/lark-apps/references/lark-apps-local-dev.md +78 -0
  36. package/skills/lark-apps/references/lark-apps-observability.md +48 -0
  37. package/skills/lark-apps/references/lark-apps-openapi-key.md +79 -0
  38. package/skills/lark-apps/references/lark-apps-plugin-install.md +36 -0
  39. package/skills/lark-apps/references/lark-apps-plugin-list.md +23 -0
  40. package/skills/lark-apps/references/lark-apps-plugin-uninstall.md +25 -0
  41. package/skills/lark-apps/references/lark-apps-release-create.md +32 -0
  42. package/skills/lark-apps/references/lark-apps-release-get.md +28 -0
  43. package/skills/lark-apps/references/lark-apps-release-list.md +31 -0
  44. package/skills/lark-apps/references/lark-apps-role.md +133 -0
  45. package/skills/lark-apps/references/lark-apps-session-messages-list.md +53 -0
  46. package/skills/lark-apps/references/lark-apps-update.md +30 -0
  47. package/skills/lark-attendance/SKILL.md +57 -0
  48. package/skills/lark-base/SKILL.md +161 -0
  49. package/skills/lark-base/references/dashboard-block-data-config.md +376 -0
  50. package/skills/lark-base/references/formula-field-guide.md +737 -0
  51. package/skills/lark-base/references/lark-base-cell-value.md +158 -0
  52. package/skills/lark-base/references/lark-base-dashboard-block-get-data.md +717 -0
  53. package/skills/lark-base/references/lark-base-dashboard.md +247 -0
  54. package/skills/lark-base/references/lark-base-data-analysis-sop.md +210 -0
  55. package/skills/lark-base/references/lark-base-data-query-guide.md +61 -0
  56. package/skills/lark-base/references/lark-base-data-query.md +454 -0
  57. package/skills/lark-base/references/lark-base-field-create.md +105 -0
  58. package/skills/lark-base/references/lark-base-field-json.md +527 -0
  59. package/skills/lark-base/references/lark-base-field-update.md +173 -0
  60. package/skills/lark-base/references/lark-base-form-detail.md +71 -0
  61. package/skills/lark-base/references/lark-base-form-questions-create.md +118 -0
  62. package/skills/lark-base/references/lark-base-form-questions-update.md +92 -0
  63. package/skills/lark-base/references/lark-base-form-submit.md +170 -0
  64. package/skills/lark-base/references/lark-base-record-batch-create.md +57 -0
  65. package/skills/lark-base/references/lark-base-record-batch-update.md +52 -0
  66. package/skills/lark-base/references/lark-base-record-history-list.md +43 -0
  67. package/skills/lark-base/references/lark-base-record-upsert.md +63 -0
  68. package/skills/lark-base/references/lark-base-role-guide.md +65 -0
  69. package/skills/lark-base/references/lark-base-view-set-filter.md +191 -0
  70. package/skills/lark-base/references/lark-base-workflow-guide.md +830 -0
  71. package/skills/lark-base/references/lark-base-workflow-schema.md +1071 -0
  72. package/skills/lark-base/references/lookup-field-guide.md +512 -0
  73. package/skills/lark-base/references/role-config.md +549 -0
  74. package/skills/lark-calendar/SKILL.md +198 -0
  75. package/skills/lark-calendar/references/lark-calendar-create.md +83 -0
  76. package/skills/lark-calendar/references/lark-calendar-meeting.md +40 -0
  77. package/skills/lark-calendar/references/lark-calendar-recurring.md +90 -0
  78. package/skills/lark-calendar/references/lark-calendar-room-find.md +109 -0
  79. package/skills/lark-calendar/references/lark-calendar-rsvp.md +38 -0
  80. package/skills/lark-calendar/references/lark-calendar-schedule-clear-time.md +59 -0
  81. package/skills/lark-calendar/references/lark-calendar-schedule-fuzzy-time.md +88 -0
  82. package/skills/lark-calendar/references/lark-calendar-schedule-meeting.md +122 -0
  83. package/skills/lark-calendar/references/lark-calendar-suggestion.md +121 -0
  84. package/skills/lark-calendar/references/lark-calendar-update.md +100 -0
  85. package/skills/lark-contact/SKILL.md +55 -0
  86. package/skills/lark-contact/references/lark-contact-get-user.md +19 -0
  87. package/skills/lark-contact/references/lark-contact-search-user.md +123 -0
  88. package/skills/lark-doc/SKILL.md +84 -0
  89. package/skills/lark-doc/references/lark-doc-create.md +80 -0
  90. package/skills/lark-doc/references/lark-doc-fetch.md +140 -0
  91. package/skills/lark-doc/references/lark-doc-history.md +107 -0
  92. package/skills/lark-doc/references/lark-doc-md.md +76 -0
  93. package/skills/lark-doc/references/lark-doc-media-download.md +50 -0
  94. package/skills/lark-doc/references/lark-doc-media-insert.md +114 -0
  95. package/skills/lark-doc/references/lark-doc-media-preview.md +41 -0
  96. package/skills/lark-doc/references/lark-doc-mindnote.md +128 -0
  97. package/skills/lark-doc/references/lark-doc-resource-cover.md +70 -0
  98. package/skills/lark-doc/references/lark-doc-update.md +260 -0
  99. package/skills/lark-doc/references/lark-doc-whiteboard.md +158 -0
  100. package/skills/lark-doc/references/lark-doc-word-stat.md +93 -0
  101. package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +35 -0
  102. package/skills/lark-doc/references/lark-doc-xml.md +182 -0
  103. package/skills/lark-doc/references/style/lark-doc-create-workflow.md +47 -0
  104. package/skills/lark-doc/references/style/lark-doc-style.md +68 -0
  105. package/skills/lark-doc/references/style/lark-doc-update-workflow.md +48 -0
  106. package/skills/lark-doc/scripts/doc_word_stat.py +1243 -0
  107. package/skills/lark-drive/SKILL.md +232 -0
  108. package/skills/lark-drive/references/lark-drive-add-comment.md +193 -0
  109. package/skills/lark-drive/references/lark-drive-apply-permission.md +77 -0
  110. package/skills/lark-drive/references/lark-drive-comment-location.md +205 -0
  111. package/skills/lark-drive/references/lark-drive-comments-guide.md +80 -0
  112. package/skills/lark-drive/references/lark-drive-cover.md +79 -0
  113. package/skills/lark-drive/references/lark-drive-create-folder.md +73 -0
  114. package/skills/lark-drive/references/lark-drive-create-shortcut.md +103 -0
  115. package/skills/lark-drive/references/lark-drive-delete.md +103 -0
  116. package/skills/lark-drive/references/lark-drive-download.md +31 -0
  117. package/skills/lark-drive/references/lark-drive-export-download.md +50 -0
  118. package/skills/lark-drive/references/lark-drive-export.md +174 -0
  119. package/skills/lark-drive/references/lark-drive-files-list.md +183 -0
  120. package/skills/lark-drive/references/lark-drive-import.md +178 -0
  121. package/skills/lark-drive/references/lark-drive-inspect.md +52 -0
  122. package/skills/lark-drive/references/lark-drive-list-comments.md +125 -0
  123. package/skills/lark-drive/references/lark-drive-member-add.md +66 -0
  124. package/skills/lark-drive/references/lark-drive-move.md +122 -0
  125. package/skills/lark-drive/references/lark-drive-permission-guide.md +53 -0
  126. package/skills/lark-drive/references/lark-drive-preview.md +87 -0
  127. package/skills/lark-drive/references/lark-drive-pull.md +137 -0
  128. package/skills/lark-drive/references/lark-drive-push.md +189 -0
  129. package/skills/lark-drive/references/lark-drive-reactions.md +113 -0
  130. package/skills/lark-drive/references/lark-drive-search.md +273 -0
  131. package/skills/lark-drive/references/lark-drive-secure-label.md +52 -0
  132. package/skills/lark-drive/references/lark-drive-status.md +135 -0
  133. package/skills/lark-drive/references/lark-drive-task-result.md +355 -0
  134. package/skills/lark-drive/references/lark-drive-upload.md +101 -0
  135. package/skills/lark-drive/references/lark-drive-version-delete.md +38 -0
  136. package/skills/lark-drive/references/lark-drive-version-get.md +71 -0
  137. package/skills/lark-drive/references/lark-drive-version-history.md +73 -0
  138. package/skills/lark-drive/references/lark-drive-version-revert.md +35 -0
  139. package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-analysis.md +249 -0
  140. package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-discovery.md +253 -0
  141. package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-execution.md +200 -0
  142. package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-planning.md +336 -0
  143. package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-rollback.md +308 -0
  144. package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize.md +232 -0
  145. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-commands.md +168 -0
  146. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-outputs.md +424 -0
  147. package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +207 -0
  148. package/skills/lark-drive/references/lark-drive-workflow.md +131 -0
  149. package/skills/lark-event/SKILL.md +155 -0
  150. package/skills/lark-event/references/lark-event-approval.md +170 -0
  151. package/skills/lark-event/references/lark-event-im.md +87 -0
  152. package/skills/lark-event/references/lark-event-minutes.md +54 -0
  153. package/skills/lark-event/references/lark-event-task.md +78 -0
  154. package/skills/lark-event/references/lark-event-vc.md +106 -0
  155. package/skills/lark-event/references/lark-event-whiteboard.md +67 -0
  156. package/skills/lark-im/SKILL.md +249 -0
  157. package/skills/lark-im/references/card/card-2.0-schema.md +107 -0
  158. package/skills/lark-im/references/card/components/button.md +63 -0
  159. package/skills/lark-im/references/card/components/chart.md +57 -0
  160. package/skills/lark-im/references/card/components/checker.md +38 -0
  161. package/skills/lark-im/references/card/components/collapsible_panel.md +46 -0
  162. package/skills/lark-im/references/card/components/column_set.md +53 -0
  163. package/skills/lark-im/references/card/components/date_picker.md +34 -0
  164. package/skills/lark-im/references/card/components/div.md +36 -0
  165. package/skills/lark-im/references/card/components/form.md +51 -0
  166. package/skills/lark-im/references/card/components/header.md +34 -0
  167. package/skills/lark-im/references/card/components/hr.md +17 -0
  168. package/skills/lark-im/references/card/components/img.md +34 -0
  169. package/skills/lark-im/references/card/components/img_combination.md +30 -0
  170. package/skills/lark-im/references/card/components/input.md +43 -0
  171. package/skills/lark-im/references/card/components/interactive_container.md +46 -0
  172. package/skills/lark-im/references/card/components/markdown.md +56 -0
  173. package/skills/lark-im/references/card/components/multi_select_person.md +40 -0
  174. package/skills/lark-im/references/card/components/multi_select_static.md +40 -0
  175. package/skills/lark-im/references/card/components/overflow.md +36 -0
  176. package/skills/lark-im/references/card/components/person.md +30 -0
  177. package/skills/lark-im/references/card/components/person_list.md +31 -0
  178. package/skills/lark-im/references/card/components/picker_datetime.md +34 -0
  179. package/skills/lark-im/references/card/components/picker_time.md +34 -0
  180. package/skills/lark-im/references/card/components/recycling_container.md +35 -0
  181. package/skills/lark-im/references/card/components/select_img.md +42 -0
  182. package/skills/lark-im/references/card/components/select_person.md +39 -0
  183. package/skills/lark-im/references/card/components/select_static.md +43 -0
  184. package/skills/lark-im/references/card/components/table.md +53 -0
  185. package/skills/lark-im/references/card/lark-im-card-create.md +180 -0
  186. package/skills/lark-im/references/card/lark-im-card-style.md +281 -0
  187. package/skills/lark-im/references/card/resource/colors.md +34 -0
  188. package/skills/lark-im/references/card/resource/icons.md +38 -0
  189. package/skills/lark-im/references/lark-im-card-action-reply.md +175 -0
  190. package/skills/lark-im/references/lark-im-chat-create.md +162 -0
  191. package/skills/lark-im/references/lark-im-chat-identity.md +55 -0
  192. package/skills/lark-im/references/lark-im-chat-list.md +166 -0
  193. package/skills/lark-im/references/lark-im-chat-members-list.md +83 -0
  194. package/skills/lark-im/references/lark-im-chat-messages-list.md +157 -0
  195. package/skills/lark-im/references/lark-im-chat-search.md +142 -0
  196. package/skills/lark-im/references/lark-im-chat-update.md +84 -0
  197. package/skills/lark-im/references/lark-im-feed-group-list-item.md +68 -0
  198. package/skills/lark-im/references/lark-im-feed-group-list.md +65 -0
  199. package/skills/lark-im/references/lark-im-feed-group-query-item.md +44 -0
  200. package/skills/lark-im/references/lark-im-feed-groups.md +452 -0
  201. package/skills/lark-im/references/lark-im-feed-shortcut-create.md +97 -0
  202. package/skills/lark-im/references/lark-im-feed-shortcut-list.md +103 -0
  203. package/skills/lark-im/references/lark-im-feed-shortcut-remove.md +48 -0
  204. package/skills/lark-im/references/lark-im-flag-cancel.md +67 -0
  205. package/skills/lark-im/references/lark-im-flag-create.md +67 -0
  206. package/skills/lark-im/references/lark-im-flag-list.md +100 -0
  207. package/skills/lark-im/references/lark-im-message-enrichment.md +54 -0
  208. package/skills/lark-im/references/lark-im-messages-mget.md +99 -0
  209. package/skills/lark-im/references/lark-im-messages-reply.md +277 -0
  210. package/skills/lark-im/references/lark-im-messages-resources-download.md +94 -0
  211. package/skills/lark-im/references/lark-im-messages-search.md +234 -0
  212. package/skills/lark-im/references/lark-im-messages-send.md +279 -0
  213. package/skills/lark-im/references/lark-im-reactions.md +299 -0
  214. package/skills/lark-im/references/lark-im-threads-messages-list.md +115 -0
  215. package/skills/lark-mail/SKILL.md +290 -0
  216. package/skills/lark-mail/assets/templates/job-application--resume.html +33 -0
  217. package/skills/lark-mail/assets/templates/newsletter--weekly-brief.html +50 -0
  218. package/skills/lark-mail/assets/templates/research--market-report.html +256 -0
  219. package/skills/lark-mail/assets/templates/weekly--personal-report.html +43 -0
  220. package/skills/lark-mail/assets/templates/weekly--team-report.html +9 -0
  221. package/skills/lark-mail/references/lark-mail-calendar-invite.md +36 -0
  222. package/skills/lark-mail/references/lark-mail-decline-receipt.md +115 -0
  223. package/skills/lark-mail/references/lark-mail-draft-create.md +127 -0
  224. package/skills/lark-mail/references/lark-mail-draft-edit.md +404 -0
  225. package/skills/lark-mail/references/lark-mail-forward.md +239 -0
  226. package/skills/lark-mail/references/lark-mail-html.md +333 -0
  227. package/skills/lark-mail/references/lark-mail-lint-html.md +243 -0
  228. package/skills/lark-mail/references/lark-mail-message-modify.md +48 -0
  229. package/skills/lark-mail/references/lark-mail-message-trash.md +41 -0
  230. package/skills/lark-mail/references/lark-mail-message.md +233 -0
  231. package/skills/lark-mail/references/lark-mail-messages.md +108 -0
  232. package/skills/lark-mail/references/lark-mail-recall.md +66 -0
  233. package/skills/lark-mail/references/lark-mail-recipient-search.md +59 -0
  234. package/skills/lark-mail/references/lark-mail-reply-all.md +213 -0
  235. package/skills/lark-mail/references/lark-mail-reply.md +249 -0
  236. package/skills/lark-mail/references/lark-mail-rules.md +31 -0
  237. package/skills/lark-mail/references/lark-mail-send-as.md +44 -0
  238. package/skills/lark-mail/references/lark-mail-send-receipt.md +120 -0
  239. package/skills/lark-mail/references/lark-mail-send-status.md +46 -0
  240. package/skills/lark-mail/references/lark-mail-send.md +222 -0
  241. package/skills/lark-mail/references/lark-mail-share-to-chat.md +87 -0
  242. package/skills/lark-mail/references/lark-mail-signature.md +98 -0
  243. package/skills/lark-mail/references/lark-mail-template-create.md +129 -0
  244. package/skills/lark-mail/references/lark-mail-template-update.md +150 -0
  245. package/skills/lark-mail/references/lark-mail-template.md +54 -0
  246. package/skills/lark-mail/references/lark-mail-thread.md +111 -0
  247. package/skills/lark-mail/references/lark-mail-triage.md +131 -0
  248. package/skills/lark-mail/references/lark-mail-watch.md +94 -0
  249. package/skills/lark-markdown/SKILL.md +70 -0
  250. package/skills/lark-markdown/references/lark-markdown-create.md +114 -0
  251. package/skills/lark-markdown/references/lark-markdown-diff.md +156 -0
  252. package/skills/lark-markdown/references/lark-markdown-fetch.md +79 -0
  253. package/skills/lark-markdown/references/lark-markdown-overwrite.md +85 -0
  254. package/skills/lark-markdown/references/lark-markdown-patch.md +160 -0
  255. package/skills/lark-minutes/SKILL.md +207 -0
  256. package/skills/lark-minutes/references/lark-minutes-detail.md +62 -0
  257. package/skills/lark-minutes/references/lark-minutes-download.md +135 -0
  258. package/skills/lark-minutes/references/lark-minutes-search.md +202 -0
  259. package/skills/lark-minutes/references/lark-minutes-speaker-replace.md +107 -0
  260. package/skills/lark-minutes/references/lark-minutes-summary.md +120 -0
  261. package/skills/lark-minutes/references/lark-minutes-todo.md +136 -0
  262. package/skills/lark-minutes/references/lark-minutes-update.md +39 -0
  263. package/skills/lark-minutes/references/lark-minutes-upload.md +104 -0
  264. package/skills/lark-note/SKILL.md +94 -0
  265. package/skills/lark-note/references/lark-note-detail.md +26 -0
  266. package/skills/lark-note/references/lark-note-transcript.md +23 -0
  267. package/skills/lark-okr/SKILL.md +122 -0
  268. package/skills/lark-okr/references/lark-okr-alignments.md +180 -0
  269. package/skills/lark-okr/references/lark-okr-batch-create.md +106 -0
  270. package/skills/lark-okr/references/lark-okr-contentblock.md +427 -0
  271. package/skills/lark-okr/references/lark-okr-cycle-detail.md +91 -0
  272. package/skills/lark-okr/references/lark-okr-cycle-list.md +93 -0
  273. package/skills/lark-okr/references/lark-okr-entities.md +329 -0
  274. package/skills/lark-okr/references/lark-okr-image-upload.md +116 -0
  275. package/skills/lark-okr/references/lark-okr-indicator-update.md +80 -0
  276. package/skills/lark-okr/references/lark-okr-indicators.md +223 -0
  277. package/skills/lark-okr/references/lark-okr-patch.md +104 -0
  278. package/skills/lark-okr/references/lark-okr-progress-create.md +85 -0
  279. package/skills/lark-okr/references/lark-okr-progress-delete.md +47 -0
  280. package/skills/lark-okr/references/lark-okr-progress-get.md +93 -0
  281. package/skills/lark-okr/references/lark-okr-progress-list.md +80 -0
  282. package/skills/lark-okr/references/lark-okr-progress-update.md +85 -0
  283. package/skills/lark-okr/references/lark-okr-reorder.md +81 -0
  284. package/skills/lark-okr/references/lark-okr-weight.md +96 -0
  285. package/skills/lark-openapi-explorer/SKILL.md +153 -0
  286. package/skills/lark-shared/SKILL.md +211 -0
  287. package/skills/lark-shared/references/lark-wiki-token-routing.md +42 -0
  288. package/skills/lark-sheets/SKILL.md +234 -0
  289. package/skills/lark-sheets/references/lark-sheets-batch-update.md +200 -0
  290. package/skills/lark-sheets/references/lark-sheets-changeset.md +105 -0
  291. package/skills/lark-sheets/references/lark-sheets-chart.md +332 -0
  292. package/skills/lark-sheets/references/lark-sheets-conditional-format.md +181 -0
  293. package/skills/lark-sheets/references/lark-sheets-filter-view.md +137 -0
  294. package/skills/lark-sheets/references/lark-sheets-filter.md +130 -0
  295. package/skills/lark-sheets/references/lark-sheets-float-image.md +159 -0
  296. package/skills/lark-sheets/references/lark-sheets-formula-translation.md +276 -0
  297. package/skills/lark-sheets/references/lark-sheets-formula-verify.md +77 -0
  298. package/skills/lark-sheets/references/lark-sheets-history.md +93 -0
  299. package/skills/lark-sheets/references/lark-sheets-pivot-table.md +171 -0
  300. package/skills/lark-sheets/references/lark-sheets-range-operations.md +297 -0
  301. package/skills/lark-sheets/references/lark-sheets-read-data.md +235 -0
  302. package/skills/lark-sheets/references/lark-sheets-search-replace.md +111 -0
  303. package/skills/lark-sheets/references/lark-sheets-sheet-structure.md +212 -0
  304. package/skills/lark-sheets/references/lark-sheets-sparkline.md +149 -0
  305. package/skills/lark-sheets/references/lark-sheets-visual-standards.md +205 -0
  306. package/skills/lark-sheets/references/lark-sheets-workbook.md +420 -0
  307. package/skills/lark-sheets/references/lark-sheets-write-cells.md +575 -0
  308. package/skills/lark-sheets/scripts/sheets_df.py +32 -0
  309. package/skills/lark-skill-maker/SKILL.md +85 -0
  310. package/skills/lark-slides/SKILL.md +285 -0
  311. package/skills/lark-slides/references/asset-planning.md +135 -0
  312. package/skills/lark-slides/references/examples.md +91 -0
  313. package/skills/lark-slides/references/iconpark-index.json +41901 -0
  314. package/skills/lark-slides/references/iconpark.md +46 -0
  315. package/skills/lark-slides/references/lark-slides-create.md +156 -0
  316. package/skills/lark-slides/references/lark-slides-edit-workflows.md +144 -0
  317. package/skills/lark-slides/references/lark-slides-media-upload.md +127 -0
  318. package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +89 -0
  319. package/skills/lark-slides/references/lark-slides-replace-pages.md +95 -0
  320. package/skills/lark-slides/references/lark-slides-replace-slide.md +240 -0
  321. package/skills/lark-slides/references/lark-slides-screenshot.md +97 -0
  322. package/skills/lark-slides/references/lark-slides-whiteboard.md +331 -0
  323. package/skills/lark-slides/references/lark-slides-xml-get.md +100 -0
  324. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +125 -0
  325. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +110 -0
  326. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +189 -0
  327. package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +99 -0
  328. package/skills/lark-slides/references/planning-layer.md +246 -0
  329. package/skills/lark-slides/references/slide-templates.md +201 -0
  330. package/skills/lark-slides/references/slides_chart_demo.xml +1 -0
  331. package/skills/lark-slides/references/slides_demo.xml +226 -0
  332. package/skills/lark-slides/references/slides_xml_schema_definition.xml +3054 -0
  333. package/skills/lark-slides/references/troubleshooting.md +45 -0
  334. package/skills/lark-slides/references/validation-checklist.md +119 -0
  335. package/skills/lark-slides/references/visual-planning.md +255 -0
  336. package/skills/lark-slides/references/xml-format-guide.md +433 -0
  337. package/skills/lark-slides/references/xml-schema-quick-ref.md +249 -0
  338. package/skills/lark-slides/scripts/iconpark_tool.py +362 -0
  339. package/skills/lark-slides/scripts/iconpark_tool_test.py +177 -0
  340. package/skills/lark-slides/scripts/xml_text_overlap_lint.py +1220 -0
  341. package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +1170 -0
  342. package/skills/lark-task/SKILL.md +168 -0
  343. package/skills/lark-task/references/lark-task-assign.md +38 -0
  344. package/skills/lark-task/references/lark-task-comment.md +28 -0
  345. package/skills/lark-task/references/lark-task-complete.md +27 -0
  346. package/skills/lark-task/references/lark-task-create.md +70 -0
  347. package/skills/lark-task/references/lark-task-followers.md +35 -0
  348. package/skills/lark-task/references/lark-task-get-my-tasks.md +61 -0
  349. package/skills/lark-task/references/lark-task-get-related-tasks.md +53 -0
  350. package/skills/lark-task/references/lark-task-reminder.md +36 -0
  351. package/skills/lark-task/references/lark-task-reopen.md +27 -0
  352. package/skills/lark-task/references/lark-task-search.md +41 -0
  353. package/skills/lark-task/references/lark-task-set-ancestor.md +32 -0
  354. package/skills/lark-task/references/lark-task-tasklist-create.md +35 -0
  355. package/skills/lark-task/references/lark-task-tasklist-members.md +36 -0
  356. package/skills/lark-task/references/lark-task-tasklist-search.md +38 -0
  357. package/skills/lark-task/references/lark-task-tasklist-task-add.md +38 -0
  358. package/skills/lark-task/references/lark-task-update.md +37 -0
  359. package/skills/lark-task/references/lark-task-upload-attachment.md +59 -0
  360. package/skills/lark-vc/SKILL.md +205 -0
  361. package/skills/lark-vc/references/lark-vc-detail.md +44 -0
  362. package/skills/lark-vc/references/lark-vc-recording.md +152 -0
  363. package/skills/lark-vc/references/lark-vc-search.md +163 -0
  364. package/skills/lark-vc/references/vc-domain-boundaries.md +196 -0
  365. package/skills/lark-vc-agent/SKILL.md +201 -0
  366. package/skills/lark-vc-agent/references/lark-vc-agent-meeting-events.md +315 -0
  367. package/skills/lark-vc-agent/references/lark-vc-agent-meeting-join.md +141 -0
  368. package/skills/lark-vc-agent/references/lark-vc-agent-meeting-leave.md +105 -0
  369. package/skills/lark-vc-agent/references/lark-vc-agent-meeting-list-active.md +91 -0
  370. package/skills/lark-vc-agent/references/lark-vc-agent-meeting-message-send.md +134 -0
  371. package/skills/lark-whiteboard/SKILL.md +47 -0
  372. package/skills/lark-whiteboard/elements/connectors.md +102 -0
  373. package/skills/lark-whiteboard/elements/content.md +40 -0
  374. package/skills/lark-whiteboard/elements/image.md +80 -0
  375. package/skills/lark-whiteboard/elements/layout.md +374 -0
  376. package/skills/lark-whiteboard/elements/schema.md +357 -0
  377. package/skills/lark-whiteboard/elements/style.md +318 -0
  378. package/skills/lark-whiteboard/elements/typography.md +73 -0
  379. package/skills/lark-whiteboard/references/lark-whiteboard-query.md +60 -0
  380. package/skills/lark-whiteboard/references/lark-whiteboard-update.md +122 -0
  381. package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +97 -0
  382. package/skills/lark-whiteboard/routes/dsl.md +107 -0
  383. package/skills/lark-whiteboard/routes/mermaid.md +27 -0
  384. package/skills/lark-whiteboard/routes/svg-edit.md +85 -0
  385. package/skills/lark-whiteboard/routes/svg.md +54 -0
  386. package/skills/lark-whiteboard/scenes/architecture.md +433 -0
  387. package/skills/lark-whiteboard/scenes/bar-chart.md +187 -0
  388. package/skills/lark-whiteboard/scenes/comparison.md +135 -0
  389. package/skills/lark-whiteboard/scenes/fishbone.md +238 -0
  390. package/skills/lark-whiteboard/scenes/flowchart.md +185 -0
  391. package/skills/lark-whiteboard/scenes/flywheel.md +195 -0
  392. package/skills/lark-whiteboard/scenes/funnel.md +101 -0
  393. package/skills/lark-whiteboard/scenes/line-chart.md +214 -0
  394. package/skills/lark-whiteboard/scenes/mermaid.md +130 -0
  395. package/skills/lark-whiteboard/scenes/milestone.md +139 -0
  396. package/skills/lark-whiteboard/scenes/organization.md +173 -0
  397. package/skills/lark-whiteboard/scenes/photo-showcase.md +126 -0
  398. package/skills/lark-whiteboard/scenes/pyramid.md +99 -0
  399. package/skills/lark-whiteboard/scenes/swimlane.md +371 -0
  400. package/skills/lark-whiteboard/scenes/treemap.md +216 -0
  401. package/skills/lark-wiki/SKILL.md +114 -0
  402. package/skills/lark-wiki/references/lark-wiki-delete-space.md +205 -0
  403. package/skills/lark-wiki/references/lark-wiki-member-add.md +67 -0
  404. package/skills/lark-wiki/references/lark-wiki-member-list.md +76 -0
  405. package/skills/lark-wiki/references/lark-wiki-member-remove.md +61 -0
  406. package/skills/lark-wiki/references/lark-wiki-move-to-drive.md +122 -0
  407. package/skills/lark-wiki/references/lark-wiki-move.md +185 -0
  408. package/skills/lark-wiki/references/lark-wiki-node-copy.md +72 -0
  409. package/skills/lark-wiki/references/lark-wiki-node-create.md +127 -0
  410. package/skills/lark-wiki/references/lark-wiki-node-delete.md +62 -0
  411. package/skills/lark-wiki/references/lark-wiki-node-get.md +57 -0
  412. package/skills/lark-wiki/references/lark-wiki-node-list.md +95 -0
  413. package/skills/lark-wiki/references/lark-wiki-space-create.md +46 -0
  414. package/skills/lark-wiki/references/lark-wiki-space-list.md +68 -0
  415. package/skills/lark-workflow-meeting-summary/SKILL.md +122 -0
  416. package/skills/lark-workflow-standup-report/SKILL.md +122 -0
@@ -0,0 +1,1220 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 Lark Technologies Pte. Ltd.
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import math
9
+ import re
10
+ import sys
11
+ import unicodedata
12
+ import xml.parsers.expat as expat
13
+ import xml.etree.ElementTree as ET
14
+ from difflib import SequenceMatcher, get_close_matches
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ XS_NS = "{http://www.w3.org/2001/XMLSchema}"
20
+ XML_NS = "{http://www.w3.org/XML/1998/namespace}"
21
+ SVG_NS = "{http://www.w3.org/2000/svg}"
22
+ SML_NAMESPACE = "http://www.larkoffice.com/sml/2.0"
23
+ SXSD_SCHEMA_PATH = Path(__file__).resolve().parents[1] / "references" / "slides_xml_schema_definition.xml"
24
+ ICONPARK_INDEX_PATH = Path(__file__).resolve().parents[1] / "references" / "iconpark-index.json"
25
+ SXSD_TAG_ALIASES = {
26
+ "textbox": "<shape type=\"text\">",
27
+ "textBox": "<shape type=\"text\">",
28
+ "image": "<img>",
29
+ "picture": "<img>",
30
+ }
31
+ SXSD_ATTR_ALIASES = {
32
+ "x": "topLeftX",
33
+ "left": "topLeftX",
34
+ "y": "topLeftY",
35
+ "top": "topLeftY",
36
+ "w": "width",
37
+ "h": "height",
38
+ "fontColor": "color",
39
+ }
40
+ SERVER_FILLED_SXSD_ATTRS = {"id"}
41
+ DEFAULT_TABLE_COLUMN_WIDTH = 110
42
+ DEFAULT_TABLE_ROW_HEIGHT = 37
43
+ _SXSD_TAG_ATTRIBUTES_CACHE: dict[str, set[str]] | None = None
44
+ _ICONPARK_ICON_TYPES_CACHE: set[str] | None = None
45
+
46
+
47
+ class XmlTextOverlapLintError(Exception):
48
+ pass
49
+
50
+
51
+ def fail(message: str) -> None:
52
+ raise XmlTextOverlapLintError(message)
53
+
54
+
55
+ def read_file(file_path: str | Path) -> str:
56
+ return Path(file_path).read_text(encoding="utf-8")
57
+
58
+
59
+ def parse_args(argv: list[str]) -> dict[str, Any]:
60
+ options: dict[str, Any] = {}
61
+ index = 0
62
+ while index < len(argv):
63
+ token = argv[index]
64
+ if not token.startswith("--"):
65
+ fail(f"unexpected argument: {token}")
66
+ key = token[2:]
67
+ next_token = argv[index + 1] if index + 1 < len(argv) else None
68
+ if next_token is None or next_token.startswith("--"):
69
+ options[key] = True
70
+ index += 1
71
+ continue
72
+ options[key] = next_token
73
+ index += 2
74
+ return options
75
+
76
+
77
+ def extract_attribute(tag_source: str, name: str) -> str | None:
78
+ match = re.search(fr'{re.escape(name)}="([^"]+)"', tag_source)
79
+ return match.group(1) if match else None
80
+
81
+
82
+ def extract_numeric_attribute(tag_source: str, name: str) -> int | float | None:
83
+ raw = extract_attribute(tag_source, name)
84
+ if raw is None:
85
+ return None
86
+ try:
87
+ value = float(raw)
88
+ except ValueError:
89
+ return None
90
+ return int(value) if value.is_integer() else value
91
+
92
+
93
+ def sum_sizes(sizes: list[int | float]) -> int | float:
94
+ return sum(sizes)
95
+
96
+
97
+ def is_filled_size(size: int | float | None) -> bool:
98
+ return isinstance(size, (int, float)) and math.isfinite(size) and size > 0
99
+
100
+
101
+ def fill_last_size_gap(sizes: list[int | float], target_size: int | float) -> list[int | float]:
102
+ if not sizes:
103
+ return sizes
104
+ final_sizes = [
105
+ size if index == len(sizes) - 1 else max(1, math.floor(size + 0.5))
106
+ for index, size in enumerate(sizes)
107
+ ]
108
+ remaining_size = target_size - sum_sizes(final_sizes[:-1])
109
+ if remaining_size >= 1:
110
+ final_sizes[-1] = remaining_size
111
+ return final_sizes
112
+
113
+ size_to_redistribute = 1 - remaining_size
114
+ for index in range(len(final_sizes) - 2, -1, -1):
115
+ reduction = min(final_sizes[index] - 1, size_to_redistribute)
116
+ final_sizes[index] -= reduction
117
+ size_to_redistribute -= reduction
118
+ if size_to_redistribute == 0:
119
+ final_sizes[-1] = 1
120
+ return final_sizes
121
+
122
+ final_sizes[-1] = 1
123
+ return final_sizes
124
+
125
+
126
+ def solve_weighted_min_layout(
127
+ input_sizes: list[int | float | None], default_size: int | float, target_min_size: int | float | None
128
+ ) -> dict[str, Any]:
129
+ filled_indexes: list[int] = []
130
+ empty_indexes: list[int] = []
131
+ base_sizes: list[int | float] = []
132
+ for index, size in enumerate(input_sizes):
133
+ if is_filled_size(size):
134
+ filled_indexes.append(index)
135
+ base_sizes.append(size)
136
+ else:
137
+ empty_indexes.append(index)
138
+ base_sizes.append(0)
139
+ filled_sum = sum_sizes(base_sizes)
140
+
141
+ if target_min_size is None:
142
+ final_sizes = [default_size if index in empty_indexes else size for index, size in enumerate(base_sizes)]
143
+ return {"final_sizes": final_sizes, "actual_size": sum_sizes(final_sizes), "ratio": 1}
144
+
145
+ if not filled_indexes:
146
+ average_size = target_min_size / len(input_sizes)
147
+ final_sizes = fill_last_size_gap([average_size] * len(input_sizes), target_min_size)
148
+ return {"final_sizes": final_sizes, "actual_size": sum_sizes(final_sizes), "ratio": 1}
149
+
150
+ if empty_indexes:
151
+ remaining_size = target_min_size - filled_sum
152
+ final_sizes = [*base_sizes]
153
+ if remaining_size > 0:
154
+ average_size = remaining_size / len(empty_indexes)
155
+ empty_sizes = fill_last_size_gap([average_size] * len(empty_indexes), remaining_size)
156
+ for index, empty_size in zip(empty_indexes, empty_sizes):
157
+ final_sizes[index] = empty_size
158
+ else:
159
+ for index in empty_indexes:
160
+ final_sizes[index] = default_size
161
+ return {"final_sizes": final_sizes, "actual_size": sum_sizes(final_sizes), "ratio": 1}
162
+
163
+ ratio = max(1, target_min_size / filled_sum)
164
+ actual_size = max(target_min_size, filled_sum)
165
+ if ratio == 1:
166
+ return {"final_sizes": [*base_sizes], "actual_size": actual_size, "ratio": ratio}
167
+ final_sizes = fill_last_size_gap([size * ratio for size in base_sizes], actual_size)
168
+ return {"final_sizes": final_sizes, "actual_size": sum_sizes(final_sizes), "ratio": ratio}
169
+
170
+
171
+ def strip_xml(value: str) -> str:
172
+ stripped = re.sub(r"<!\[CDATA\[([\s\S]*?)\]\]>", r"\1", value)
173
+ stripped = re.sub(r"<[^>]+>", " ", stripped)
174
+ stripped = stripped.replace("&nbsp;", " ")
175
+ stripped = stripped.replace("&amp;", "&")
176
+ stripped = stripped.replace("&lt;", "<")
177
+ stripped = stripped.replace("&gt;", ">")
178
+ stripped = stripped.replace("&quot;", '"')
179
+ stripped = stripped.replace("&#39;", "'")
180
+ return re.sub(r"\s+", " ", stripped).strip()
181
+
182
+
183
+ def strip_xml_paragraphs(value: str) -> str:
184
+ paragraphs = re.findall(r"<p\b[^>]*>([\s\S]*?)</p\s*>", value)
185
+ if paragraphs:
186
+ return "\n".join(strip_xml(paragraph) for paragraph in paragraphs)
187
+ return strip_xml(value)
188
+
189
+
190
+ def xml_local_name(tag: str) -> str:
191
+ return tag.rsplit("}", 1)[-1] if tag.startswith("{") else tag
192
+
193
+
194
+ def xml_namespace(tag: str) -> str | None:
195
+ return tag.split("}", 1)[0] + "}" if tag.startswith("{") else None
196
+
197
+
198
+ def strip_xsd_prefix(value: str | None) -> str | None:
199
+ if value is None:
200
+ return None
201
+ return value.rsplit(":", 1)[-1]
202
+
203
+
204
+ def iter_direct_xsd_children(element: ET.Element, local_name: str) -> list[ET.Element]:
205
+ return [child for child in element if child.tag == f"{XS_NS}{local_name}"]
206
+
207
+
208
+ def load_sxsd_tag_attributes() -> dict[str, set[str]]:
209
+ global _SXSD_TAG_ATTRIBUTES_CACHE
210
+ if _SXSD_TAG_ATTRIBUTES_CACHE is not None:
211
+ return _SXSD_TAG_ATTRIBUTES_CACHE
212
+
213
+ schema_root = ET.parse(SXSD_SCHEMA_PATH).getroot()
214
+ named_complex_types = {
215
+ complex_type.attrib["name"]: complex_type
216
+ for complex_type in schema_root.findall(f"{XS_NS}complexType")
217
+ if complex_type.attrib.get("name")
218
+ }
219
+ resolving: set[str] = set()
220
+
221
+ def attributes_for_complex_type(complex_type: ET.Element) -> set[str]:
222
+ attrs: set[str] = {
223
+ attribute.attrib["name"]
224
+ for attribute in iter_direct_xsd_children(complex_type, "attribute")
225
+ if attribute.attrib.get("name")
226
+ }
227
+ for content_name in ("simpleContent", "complexContent"):
228
+ for complex_content in iter_direct_xsd_children(complex_type, content_name):
229
+ for extension in iter_direct_xsd_children(complex_content, "extension"):
230
+ base_type = strip_xsd_prefix(extension.attrib.get("base"))
231
+ if base_type:
232
+ attrs.update(attributes_for_type(base_type))
233
+ attrs.update(
234
+ attribute.attrib["name"]
235
+ for attribute in iter_direct_xsd_children(extension, "attribute")
236
+ if attribute.attrib.get("name")
237
+ )
238
+ return attrs
239
+
240
+ def attributes_for_type(type_name: str) -> set[str]:
241
+ if type_name in resolving:
242
+ return set()
243
+ complex_type = named_complex_types.get(type_name)
244
+ if complex_type is None:
245
+ return set()
246
+ resolving.add(type_name)
247
+ try:
248
+ return attributes_for_complex_type(complex_type)
249
+ finally:
250
+ resolving.remove(type_name)
251
+
252
+ tag_attributes: dict[str, set[str]] = {}
253
+ for element in schema_root.iter(f"{XS_NS}element"):
254
+ tag_name = element.attrib.get("name")
255
+ if not tag_name:
256
+ continue
257
+
258
+ attrs: set[str] = set()
259
+ type_name = strip_xsd_prefix(element.attrib.get("type"))
260
+ if type_name:
261
+ attrs.update(attributes_for_type(type_name))
262
+ for complex_type in iter_direct_xsd_children(element, "complexType"):
263
+ attrs.update(attributes_for_complex_type(complex_type))
264
+
265
+ tag_attributes.setdefault(tag_name, set()).update(attrs)
266
+
267
+ _SXSD_TAG_ATTRIBUTES_CACHE = tag_attributes
268
+ return tag_attributes
269
+
270
+
271
+ def load_iconpark_icon_types() -> set[str]:
272
+ global _ICONPARK_ICON_TYPES_CACHE
273
+ if _ICONPARK_ICON_TYPES_CACHE is not None:
274
+ return _ICONPARK_ICON_TYPES_CACHE
275
+
276
+ try:
277
+ index_data = json.loads(ICONPARK_INDEX_PATH.read_text(encoding="utf-8"))
278
+ except json.JSONDecodeError as error:
279
+ fail(f"invalid iconpark index JSON: {error}")
280
+ icons = index_data.get("icons")
281
+ if not isinstance(icons, list):
282
+ fail("iconpark index must contain an icons array")
283
+
284
+ icon_types = {
285
+ icon["iconType"]
286
+ for icon in icons
287
+ if isinstance(icon, dict) and isinstance(icon.get("iconType"), str) and icon["iconType"]
288
+ }
289
+ _ICONPARK_ICON_TYPES_CACHE = icon_types
290
+ return icon_types
291
+
292
+
293
+ def build_sxsd_tag_hint(tag_name: str, supported_tags: set[str]) -> str:
294
+ alias = SXSD_TAG_ALIASES.get(tag_name)
295
+ if alias:
296
+ return f"Use {alias} instead of <{tag_name}>."
297
+ if tag_name == "svg":
298
+ return 'Inside <whiteboard>, write SVG as <svg xmlns="http://www.w3.org/2000/svg">...</svg>.'
299
+ close_matches = get_close_matches(tag_name, sorted(supported_tags), n=3, cutoff=0.72)
300
+ if close_matches:
301
+ return "Unsupported SXSD tag. Did you mean " + ", ".join(f"<{match}>" for match in close_matches) + "?"
302
+ return "Unsupported SXSD tag. Use only tags defined in slides_xml_schema_definition.xml."
303
+
304
+
305
+ def build_sxsd_attr_hint(tag_name: str, attr_name: str, allowed_attrs: set[str]) -> str:
306
+ alias = SXSD_ATTR_ALIASES.get(attr_name)
307
+ if alias and alias in allowed_attrs:
308
+ return f'Use "{alias}" on <{tag_name}> instead of "{attr_name}".'
309
+ close_matches = get_close_matches(attr_name, sorted(allowed_attrs), n=3, cutoff=0.68)
310
+ if close_matches:
311
+ return "Unsupported SXSD attribute. Did you mean " + ", ".join(f'"{match}"' for match in close_matches) + "?"
312
+ allowed_summary = ", ".join(sorted(allowed_attrs)[:8])
313
+ if len(allowed_attrs) > 8:
314
+ allowed_summary += ", ..."
315
+ return f"Unsupported SXSD attribute for <{tag_name}>. Allowed attributes include: {allowed_summary}."
316
+
317
+
318
+ def should_skip_sxsd_subtree(element: ET.Element, ancestors: list[str]) -> bool:
319
+ return "whiteboard" in ancestors and xml_namespace(element.tag) == SVG_NS
320
+
321
+
322
+ def should_skip_sxsd_attribute(attr_name: str) -> bool:
323
+ return attr_name in SERVER_FILLED_SXSD_ATTRS
324
+
325
+
326
+ def validate_sxsd_tag_attributes(root: ET.Element) -> list[dict[str, Any]]:
327
+ tag_attributes = load_sxsd_tag_attributes()
328
+ supported_tags = set(tag_attributes)
329
+ issues: list[dict[str, Any]] = []
330
+
331
+ def visit(element: ET.Element, ancestors: list[str], path: str) -> None:
332
+ if should_skip_sxsd_subtree(element, ancestors):
333
+ return
334
+
335
+ tag_name = xml_local_name(element.tag)
336
+ current_path = f"{path}/{tag_name}" if path else tag_name
337
+ if tag_name not in supported_tags:
338
+ issues.append(
339
+ {
340
+ "level": "error",
341
+ "code": "sxsd_unsupported_tag",
342
+ "tag": tag_name,
343
+ "path": current_path,
344
+ "message": f"unsupported SXSD tag <{tag_name}> at {current_path}",
345
+ "hint": build_sxsd_tag_hint(tag_name, supported_tags),
346
+ }
347
+ )
348
+ return
349
+ else:
350
+ allowed_attrs = tag_attributes[tag_name]
351
+ for raw_attr_name in element.attrib:
352
+ if raw_attr_name.startswith(XML_NS):
353
+ continue
354
+ attr_name = xml_local_name(raw_attr_name)
355
+ if should_skip_sxsd_attribute(attr_name):
356
+ continue
357
+ if attr_name in allowed_attrs:
358
+ continue
359
+ issues.append(
360
+ {
361
+ "level": "error",
362
+ "code": "sxsd_unsupported_attr",
363
+ "tag": tag_name,
364
+ "attr": attr_name,
365
+ "path": current_path,
366
+ "message": f'unsupported SXSD attribute "{attr_name}" on <{tag_name}> at {current_path}',
367
+ "hint": build_sxsd_attr_hint(tag_name, attr_name, allowed_attrs),
368
+ }
369
+ )
370
+
371
+ for child in element:
372
+ visit(child, [*ancestors, tag_name], current_path)
373
+
374
+ visit(root, [], "")
375
+ return issues
376
+
377
+
378
+ def build_iconpark_icon_type_hint(icon_type: str, supported_icon_types: set[str]) -> str:
379
+ close_matches = get_close_matches(icon_type, sorted(supported_icon_types), n=3, cutoff=0.58)
380
+ if close_matches:
381
+ return (
382
+ "iconType must exist in iconpark-index.json. Did you mean "
383
+ + ", ".join(f'"{match}"' for match in close_matches)
384
+ + "?"
385
+ )
386
+ return "iconType must exist in iconpark-index.json. Use scripts/iconpark_tool.py to search supported icons."
387
+
388
+
389
+ def validate_iconpark_icon_types(root: ET.Element) -> list[dict[str, Any]]:
390
+ supported_icon_types: set[str] | None = None
391
+ issues: list[dict[str, Any]] = []
392
+
393
+ def direct_child(element: ET.Element, local_name: str) -> ET.Element | None:
394
+ return next((child for child in element if xml_local_name(child.tag) == local_name), None)
395
+
396
+ def is_transparent_color(color: str) -> bool:
397
+ normalized = re.sub(r"\s+", "", color).lower()
398
+ if normalized == "transparent":
399
+ return True
400
+ rgba_match = re.fullmatch(r"rgba\([^,]+,[^,]+,[^,]+,([0-9.]+)\)", normalized)
401
+ if not rgba_match:
402
+ return False
403
+ try:
404
+ return float(rgba_match.group(1)) <= 0
405
+ except ValueError:
406
+ return False
407
+
408
+ def append_missing_fill_color_issue(current_path: str) -> None:
409
+ issues.append(
410
+ {
411
+ "level": "error",
412
+ "code": "icon_missing_fill_color",
413
+ "tag": "icon",
414
+ "path": current_path,
415
+ "message": f"<icon> must set explicit non-transparent fillColor for visual visibility at {current_path}",
416
+ "hint": 'Add <fill><fillColor color="rgba(R, G, B, 1)"/></fill> inside <icon>. This is a visual lint rule, not an SXSD required field.',
417
+ }
418
+ )
419
+
420
+ def visit(element: ET.Element, path: str) -> None:
421
+ nonlocal supported_icon_types
422
+ tag_name = xml_local_name(element.tag)
423
+ current_path = f"{path}/{tag_name}" if path else tag_name
424
+ if tag_name == "icon":
425
+ icon_type = element.attrib.get("iconType")
426
+ if icon_type is not None:
427
+ if supported_icon_types is None:
428
+ supported_icon_types = load_iconpark_icon_types()
429
+ if icon_type not in supported_icon_types:
430
+ issues.append(
431
+ {
432
+ "level": "error",
433
+ "code": "iconpark_unsupported_icon_type",
434
+ "tag": "icon",
435
+ "attr": "iconType",
436
+ "iconType": icon_type,
437
+ "path": current_path,
438
+ "message": f'unsupported iconpark iconType "{icon_type}" at {current_path}',
439
+ "hint": build_iconpark_icon_type_hint(icon_type, supported_icon_types),
440
+ }
441
+ )
442
+ fill = direct_child(element, "fill")
443
+ fill_color = direct_child(fill, "fillColor") if fill is not None else None
444
+ color = fill_color.attrib.get("color") if fill_color is not None else None
445
+ if not color:
446
+ append_missing_fill_color_issue(current_path)
447
+ elif is_transparent_color(color):
448
+ issues.append(
449
+ {
450
+ "level": "error",
451
+ "code": "icon_transparent_fill_color",
452
+ "tag": "icon",
453
+ "attr": "fillColor",
454
+ "path": current_path,
455
+ "color": color,
456
+ "message": f'<icon> fillColor must not be transparent for visual visibility at {current_path}: "{color}"',
457
+ "hint": 'Use an opaque visible color, for example <fillColor color="rgba(37, 99, 235, 1)"/>.',
458
+ }
459
+ )
460
+ for child in element:
461
+ visit(child, current_path)
462
+
463
+ visit(root, "")
464
+ return issues
465
+
466
+
467
+ def extract_error_context(xml: str, line: int | None, column: int | None, radius: int = 40) -> str | None:
468
+ if line is None or column is None:
469
+ return None
470
+ lines = xml.splitlines()
471
+ if line < 1 or line > len(lines):
472
+ return None
473
+ source_line = lines[line - 1]
474
+ start = max(column - radius, 0)
475
+ end = min(column + radius, len(source_line))
476
+ return source_line[start:end].strip()
477
+
478
+
479
+ def build_xml_error_issue(error: ET.ParseError, xml: str) -> dict[str, Any]:
480
+ line, column = getattr(error, "position", (None, None))
481
+ return {
482
+ "level": "error",
483
+ "code": "xml_not_well_formed",
484
+ "message": f"XML is not well-formed: {error}",
485
+ "line": line,
486
+ "column": column,
487
+ "context": extract_error_context(xml, line, column),
488
+ "hint": (
489
+ "Escape raw user text before placing it in XML. In text nodes and attribute values, bare & must be "
490
+ "written as &amp;. In text nodes, write < as &lt; and > as &gt;. For attribute URLs, use a=1&amp;b=2."
491
+ ),
492
+ }
493
+
494
+
495
+ def validate_sml_tag_prefixes(xml: str) -> list[dict[str, Any]]:
496
+ namespace_map: dict[str, str] = {}
497
+ pending_declarations: list[tuple[str, str | None]] = []
498
+ declarations_by_element: list[list[tuple[str, str | None]]] = []
499
+ element_stack: list[str] = []
500
+ issues: list[dict[str, Any]] = []
501
+
502
+ parser = expat.ParserCreate(namespace_separator="|")
503
+ parser.namespace_prefixes = True
504
+
505
+ def handle_namespace_decl(prefix: str | None, namespace: str) -> None:
506
+ normalized_prefix = prefix or ""
507
+ previous_namespace = namespace_map.get(normalized_prefix)
508
+ namespace_map[normalized_prefix] = namespace
509
+ pending_declarations.append((normalized_prefix, previous_namespace))
510
+
511
+ def handle_start_element(name: str, _attrs: dict[str, str]) -> None:
512
+ declarations_by_element.append(pending_declarations.copy())
513
+ pending_declarations.clear()
514
+ name_parts = name.rsplit("|", 2)
515
+ if len(name_parts) == 3:
516
+ _namespace, local_name, prefix = name_parts
517
+ element_name = f"{prefix}:{local_name}"
518
+ else:
519
+ prefix = ""
520
+ local_name = name_parts[-1]
521
+ element_name = local_name
522
+ element_stack.append(element_name)
523
+ if not prefix:
524
+ return
525
+
526
+ if namespace_map.get(prefix) != SML_NAMESPACE:
527
+ return
528
+ path = "/".join(element_stack)
529
+ issues.append(
530
+ {
531
+ "level": "error",
532
+ "code": "sml_prefixed_tag",
533
+ "tag": element_name,
534
+ "namespace": SML_NAMESPACE,
535
+ "path": path,
536
+ "line": parser.CurrentLineNumber,
537
+ "column": parser.CurrentColumnNumber,
538
+ "message": f"SML tag <{element_name}> must not use a namespace prefix at {path}",
539
+ "hint": (
540
+ f'Use <{local_name}> under the default namespace '
541
+ f'<{local_name} xmlns="{SML_NAMESPACE}">, or use an unprefixed SML tag.'
542
+ ),
543
+ }
544
+ )
545
+
546
+ def handle_end_element(_name: str) -> None:
547
+ for prefix, previous_namespace in reversed(declarations_by_element.pop()):
548
+ if previous_namespace is None:
549
+ namespace_map.pop(prefix, None)
550
+ else:
551
+ namespace_map[prefix] = previous_namespace
552
+ element_stack.pop()
553
+
554
+ parser.StartNamespaceDeclHandler = handle_namespace_decl
555
+ parser.StartElementHandler = handle_start_element
556
+ parser.EndElementHandler = handle_end_element
557
+ parser.Parse(xml, True)
558
+ return issues
559
+
560
+
561
+ def parse_xml_root(xml: str) -> tuple[ET.Element | None, dict[str, Any] | None]:
562
+ try:
563
+ root = ET.fromstring(xml)
564
+ except ET.ParseError as error:
565
+ return None, build_xml_error_issue(error, xml)
566
+
567
+ root_name = xml_local_name(root.tag)
568
+ if root_name not in {"presentation", "slide"}:
569
+ fail("input must contain a <presentation> or <slide> root")
570
+ return root, None
571
+
572
+
573
+ def validate_xml_well_formed(xml: str) -> dict[str, Any] | None:
574
+ _, xml_error = parse_xml_root(xml)
575
+ return xml_error
576
+
577
+
578
+ def parse_presentation(xml: str) -> dict[str, Any]:
579
+ presentation_match = re.search(r"<presentation\b([^>]*)>", xml)
580
+ if presentation_match:
581
+ return {
582
+ "width": int(float(extract_attribute(presentation_match.group(1), "width") or 960)),
583
+ "height": int(float(extract_attribute(presentation_match.group(1), "height") or 540)),
584
+ "slides": re.findall(r"<slide\b[\s\S]*?</slide>", xml),
585
+ }
586
+ slide_match = re.findall(r"<slide\b[\s\S]*?</slide>", xml)
587
+ if slide_match:
588
+ return {"width": 960, "height": 540, "slides": slide_match}
589
+ fail("input must contain a <presentation> or <slide> root")
590
+
591
+
592
+ def extract_elements(slide_xml: str) -> list[dict[str, Any]]:
593
+ elements: list[dict[str, Any]] = []
594
+
595
+ for match in re.finditer(r"<(shape|img|table|chart|whiteboard)\b([^>]*)>", slide_xml):
596
+ kind, attrs = match.group(1), match.group(2)
597
+ content = ""
598
+ if kind in {"shape", "table"}:
599
+ close_index = slide_xml.find(f"</{kind}>", match.end())
600
+ if close_index != -1:
601
+ content = slide_xml[match.end() : close_index]
602
+
603
+ element_id = extract_attribute(attrs, "id") or f"{kind}-{len(elements) + 1}"
604
+ x = extract_numeric_attribute(attrs, "topLeftX")
605
+ y = extract_numeric_attribute(attrs, "topLeftY")
606
+ width = extract_numeric_attribute(attrs, "width")
607
+ height = extract_numeric_attribute(attrs, "height")
608
+ rotation = extract_numeric_attribute(attrs, "rotation") or 0
609
+ table_layouts: dict[str, dict[str, Any] | None] = {}
610
+ if kind == "table":
611
+ width, table_layouts["width"] = resolve_table_dimension(
612
+ content, width, extract_table_column_sizes, DEFAULT_TABLE_COLUMN_WIDTH
613
+ )
614
+ height, table_layouts["height"] = resolve_table_dimension(
615
+ content, height, extract_table_row_sizes, DEFAULT_TABLE_ROW_HEIGHT
616
+ )
617
+ if all(value is not None for value in [x, y, width, height]):
618
+ element = {
619
+ "id": element_id,
620
+ "kind": kind,
621
+ "type": extract_attribute(attrs, "type") or kind,
622
+ "x": x,
623
+ "y": y,
624
+ "width": width,
625
+ "height": height,
626
+ "rotation": rotation,
627
+ "order": len(elements),
628
+ }
629
+ if kind == "table":
630
+ element.update(
631
+ {
632
+ "declared_width": extract_numeric_attribute(attrs, "width"),
633
+ "declared_height": extract_numeric_attribute(attrs, "height"),
634
+ "table_layouts": table_layouts,
635
+ }
636
+ )
637
+ if kind == "shape":
638
+ element.update(
639
+ {
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
+ ),
646
+ "text": strip_xml_paragraphs(content),
647
+ }
648
+ )
649
+ elements.append(element)
650
+ return elements
651
+
652
+
653
+ def intersects(left: dict[str, Any], right: dict[str, Any]) -> bool:
654
+ return (
655
+ left["x"] < right["x"] + right["width"]
656
+ and left["x"] + left["width"] > right["x"]
657
+ and left["y"] < right["y"] + right["height"]
658
+ and left["y"] + left["height"] > right["y"]
659
+ )
660
+
661
+
662
+ def is_text_element(element: dict[str, Any]) -> bool:
663
+ return element["kind"] == "shape" and element["type"] == "text"
664
+
665
+
666
+ def is_whiteboard_element(element: dict[str, Any]) -> bool:
667
+ return element["kind"] == "whiteboard"
668
+
669
+
670
+ def has_text_content(element: dict[str, Any]) -> bool:
671
+ return bool(element.get("text"))
672
+
673
+
674
+ def is_decorative_text(element: dict[str, Any]) -> bool:
675
+ text = element.get("text") or ""
676
+ return bool(text) and re.search(r"[A-Za-z0-9\u4e00-\u9fff]", text) is None
677
+
678
+
679
+ def normalize_text_for_overlap(text: str) -> str:
680
+ return re.sub(r"\s+", "", text)
681
+
682
+
683
+ def estimate_character_width(character: str, font_size: int | float) -> int | float:
684
+ if character.isspace():
685
+ return font_size * 0.33
686
+ if unicodedata.east_asian_width(character) in {"F", "W"}:
687
+ return font_size
688
+ return font_size * 0.55
689
+
690
+
691
+ def estimate_text_width(text: str, font_size: int | float) -> int | float:
692
+ return sum(estimate_character_width(character, font_size) for character in text)
693
+
694
+
695
+ def estimate_text_max_line_width(element: dict[str, Any]) -> int | float:
696
+ font_size = element["fontSize"] if isinstance(element["fontSize"], (int, float)) else 16
697
+ paragraphs = [paragraph for paragraph in re.split(r"\n+", element["text"]) if paragraph]
698
+ return max([estimate_text_width(paragraph, font_size) for paragraph in paragraphs] or [1])
699
+
700
+
701
+ def is_similar_text_overlay(left: dict[str, Any], right: dict[str, Any]) -> bool:
702
+ left_text = normalize_text_for_overlap(left.get("text") or "")
703
+ right_text = normalize_text_for_overlap(right.get("text") or "")
704
+ if not left_text or not right_text:
705
+ return False
706
+ if left_text == right_text or left_text in right_text or right_text in left_text:
707
+ return True
708
+ return SequenceMatcher(None, left_text, right_text).ratio() >= 0.75
709
+
710
+
711
+ def estimate_text_line_count(element: dict[str, Any]) -> int:
712
+ 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]
714
+ line_count = 0
715
+ for paragraph in paragraphs:
716
+ logical_width = max(estimate_text_width(paragraph, font_size), 1)
717
+ line_count += max(1, math.ceil(logical_width / max(element["width"], 1)))
718
+ return max(line_count, 1)
719
+
720
+
721
+ def estimate_text_visual_bbox(element: dict[str, Any]) -> dict[str, int | float] | None:
722
+ if not is_text_element(element) or not has_text_content(element) or is_decorative_text(element):
723
+ return None
724
+
725
+ font_size = element["fontSize"] if isinstance(element["fontSize"], (int, float)) else 16
726
+ 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))
729
+ return {
730
+ "x": element["x"],
731
+ "y": element["y"],
732
+ "width": visual_width,
733
+ "height": visual_height,
734
+ }
735
+
736
+
737
+ def intersection_area(left: dict[str, Any], right: dict[str, Any]) -> int | float:
738
+ width = min(left["x"] + left["width"], right["x"] + right["width"]) - max(left["x"], right["x"])
739
+ height = min(left["y"] + left["height"], right["y"] + right["height"]) - max(left["y"], right["y"])
740
+ if width <= 0 or height <= 0:
741
+ return 0
742
+ return width * height
743
+
744
+
745
+ def intersection_height(left: dict[str, Any], right: dict[str, Any]) -> int | float:
746
+ height = min(left["y"] + left["height"], right["y"] + right["height"]) - max(left["y"], right["y"])
747
+ return max(height, 0)
748
+
749
+
750
+ def intersection_width(left: dict[str, Any], right: dict[str, Any]) -> int | float:
751
+ width = min(left["x"] + left["width"], right["x"] + right["width"]) - max(left["x"], right["x"])
752
+ return max(width, 0)
753
+
754
+
755
+ def element_area(element: dict[str, Any]) -> int | float:
756
+ return max(element["width"], 0) * max(element["height"], 0)
757
+
758
+
759
+ def contains(outer: dict[str, Any], inner: dict[str, Any], tolerance: int | float = 2) -> bool:
760
+ return (
761
+ inner["x"] >= outer["x"] - tolerance
762
+ and inner["y"] >= outer["y"] - tolerance
763
+ and inner["x"] + inner["width"] <= outer["x"] + outer["width"] + tolerance
764
+ and inner["y"] + inner["height"] <= outer["y"] + outer["height"] + tolerance
765
+ )
766
+
767
+
768
+ def is_bottom_layer_full_slide_whiteboard(
769
+ whiteboard: dict[str, Any], other: dict[str, Any], slide_width: int | float, slide_height: int | float
770
+ ) -> bool:
771
+ return (
772
+ whiteboard["order"] < other["order"]
773
+ and whiteboard["x"] <= 2
774
+ and whiteboard["y"] <= 2
775
+ and whiteboard["width"] >= slide_width - 4
776
+ and whiteboard["height"] >= slide_height - 4
777
+ )
778
+
779
+
780
+ def is_background_container_for_whiteboard(container: dict[str, Any], whiteboard: dict[str, Any]) -> bool:
781
+ if container["order"] > whiteboard["order"]:
782
+ return False
783
+ if is_text_element(container):
784
+ return False
785
+ return contains(container, whiteboard)
786
+
787
+
788
+ def is_template_text_stack(left: dict[str, Any], right: dict[str, Any]) -> bool:
789
+ if not (is_text_element(left) and is_text_element(right)):
790
+ return False
791
+ if not (has_text_content(left) and has_text_content(right)):
792
+ return True
793
+ top, bottom = sorted([left, right], key=lambda element: element["y"])
794
+ top_type = top.get("textType")
795
+ bottom_type = bottom.get("textType")
796
+ allowed_pairs = {
797
+ ("title", "sub-headline"),
798
+ ("title", None),
799
+ ("headline", "headline"),
800
+ ("headline", None),
801
+ }
802
+ if (top_type, bottom_type) not in allowed_pairs:
803
+ return False
804
+ same_column = abs(top["x"] - bottom["x"]) <= 4
805
+ vertical_offset = bottom["y"] - top["y"]
806
+ top_font_size = float(top.get("fontSize", 16))
807
+ return same_column and vertical_offset >= top_font_size * 0.75
808
+
809
+
810
+ def should_flag_horizontal_text_overflow(left: dict[str, Any], right: dict[str, Any]) -> bool:
811
+ if not (is_text_element(left) and is_text_element(right)):
812
+ return False
813
+ if not (has_text_content(left) and has_text_content(right)):
814
+ return False
815
+ if is_template_text_stack(left, right) or is_similar_text_overlay(left, right):
816
+ return False
817
+
818
+ source, target = sorted([left, right], key=lambda element: element["x"])
819
+ if source["x"] == target["x"]:
820
+ return False
821
+ if source.get("autoFit") == "normal-auto-fit":
822
+ return False
823
+ if source.get("textAlign") in {"center", "right"}:
824
+ return False
825
+
826
+ font_size = source["fontSize"] if isinstance(source["fontSize"], (int, float)) else 16
827
+ visual_width = estimate_text_max_line_width(source)
828
+ overflow_width = visual_width - source["width"]
829
+ min_overflow = max(font_size * 1.5, source["width"] * 0.08)
830
+ if overflow_width < min_overflow:
831
+ return False
832
+
833
+ intrusion_width = source["x"] + visual_width - target["x"]
834
+ min_intrusion = max(font_size * 1.5, target["width"] * 0.08)
835
+ if intrusion_width < min_intrusion:
836
+ return False
837
+
838
+ vertical_overlap = intersection_height(source, target)
839
+ min_vertical_overlap = min(source["height"], target["height"]) * 0.40
840
+ return vertical_overlap >= min_vertical_overlap
841
+
842
+
843
+ def should_flag_overlap(left: dict[str, Any], right: dict[str, Any]) -> bool:
844
+ if is_text_element(left) and not has_text_content(left):
845
+ return False
846
+ if is_text_element(right) and not has_text_content(right):
847
+ return False
848
+ if is_template_text_stack(left, right):
849
+ return False
850
+ if is_text_element(left) and is_text_element(right):
851
+ if is_similar_text_overlay(left, right):
852
+ return False
853
+ left_visual = estimate_text_visual_bbox(left)
854
+ right_visual = estimate_text_visual_bbox(right)
855
+ if left_visual is None or right_visual is None:
856
+ return False
857
+ overlap_area = intersection_area(left_visual, right_visual)
858
+ if overlap_area <= 0:
859
+ return False
860
+ smaller_area = min(
861
+ left_visual["width"] * left_visual["height"],
862
+ right_visual["width"] * right_visual["height"],
863
+ )
864
+ return smaller_area > 0 and overlap_area / smaller_area >= 0.30
865
+ return False
866
+
867
+
868
+ def build_whiteboard_external_overlap_issue(
869
+ whiteboard: dict[str, Any], overlap_details: list[dict[str, Any]]
870
+ ) -> dict[str, Any]:
871
+ element_ids = [detail["element"] for detail in overlap_details]
872
+ return {
873
+ "level": "warning",
874
+ "code": "whiteboard_external_overlap",
875
+ "elements": [whiteboard["id"], *element_ids],
876
+ "message": f'whiteboard {whiteboard["id"]} overlaps {len(element_ids)} sibling elements across its boundary',
877
+ "hint": (
878
+ "Treat this as a static whiteboard container-bbox risk, not final visual proof. "
879
+ "After moving or accepting the overlap, use screenshot QA or equivalent rendered visual inspection as "
880
+ "the final authority because XML readback does not include whiteboard SVG/Mermaid internals."
881
+ ),
882
+ "overlaps": overlap_details,
883
+ }
884
+
885
+
886
+ def should_report_whiteboard_overlap(
887
+ whiteboard: dict[str, Any],
888
+ other: dict[str, Any],
889
+ slide_width: int | float,
890
+ slide_height: int | float,
891
+ ) -> dict[str, Any] | None:
892
+ if other is whiteboard or not intersects(whiteboard, other):
893
+ return None
894
+ if contains(whiteboard, other):
895
+ return None
896
+ if is_bottom_layer_full_slide_whiteboard(whiteboard, other, slide_width, slide_height):
897
+ return None
898
+ if is_background_container_for_whiteboard(other, whiteboard):
899
+ return None
900
+
901
+ overlap_width = intersection_width(whiteboard, other)
902
+ overlap_height = intersection_height(whiteboard, other)
903
+ if overlap_width < 8 or overlap_height < 8:
904
+ return None
905
+
906
+ other_area = element_area(other)
907
+ if other_area <= 0:
908
+ return None
909
+ overlap_area = overlap_width * overlap_height
910
+ overlap_ratio = overlap_area / other_area
911
+ if overlap_ratio < 0.15:
912
+ return None
913
+
914
+ return {
915
+ "element": other["id"],
916
+ "kind": other["kind"],
917
+ "type": other.get("type"),
918
+ "overlap_width": overlap_width,
919
+ "overlap_height": overlap_height,
920
+ "target_overlap_ratio": round(overlap_ratio, 3),
921
+ }
922
+
923
+
924
+ def prune_contained_text_overlap_details(
925
+ overlap_details: list[dict[str, Any]], elements_by_id: dict[str, dict[str, Any]]
926
+ ) -> list[dict[str, Any]]:
927
+ pruned: list[dict[str, Any]] = []
928
+ for detail in overlap_details:
929
+ element = elements_by_id[detail["element"]]
930
+ if is_text_element(element):
931
+ has_reported_container = any(
932
+ detail["element"] != other_detail["element"]
933
+ and not is_text_element(elements_by_id[other_detail["element"]])
934
+ and contains(elements_by_id[other_detail["element"]], element)
935
+ for other_detail in overlap_details
936
+ )
937
+ if has_reported_container:
938
+ continue
939
+ pruned.append(detail)
940
+ return pruned
941
+
942
+
943
+ def detect_whiteboard_external_overlaps(
944
+ elements: list[dict[str, Any]], slide_width: int | float, slide_height: int | float
945
+ ) -> list[dict[str, Any]]:
946
+ issues: list[dict[str, Any]] = []
947
+ elements_by_id = {element["id"]: element for element in elements}
948
+ for whiteboard in [element for element in elements if is_whiteboard_element(element)]:
949
+ overlap_details = [
950
+ detail
951
+ for element in elements
952
+ if (
953
+ detail := should_report_whiteboard_overlap(
954
+ whiteboard,
955
+ element,
956
+ slide_width,
957
+ slide_height,
958
+ )
959
+ )
960
+ is not None
961
+ ]
962
+ overlap_details = prune_contained_text_overlap_details(overlap_details, elements_by_id)
963
+ if overlap_details:
964
+ issues.append(build_whiteboard_external_overlap_issue(whiteboard, overlap_details))
965
+ return issues
966
+
967
+
968
+ def element_canvas_bbox(element: dict[str, Any]) -> dict[str, int | float]:
969
+ bbox = {key: element[key] for key in ("x", "y", "width", "height")}
970
+ if element["kind"] != "chart" and not (element["kind"] == "shape" and element["type"] == "text"):
971
+ return bbox
972
+
973
+ rotation = element["rotation"]
974
+ if not isinstance(rotation, (int, float)) or not math.isfinite(rotation):
975
+ rotation = 0
976
+ rotation %= 360
977
+ if math.isclose(rotation, 0, abs_tol=1e-9):
978
+ return bbox
979
+ radians = math.radians(rotation)
980
+ sine = abs(math.sin(radians))
981
+ cosine = abs(math.cos(radians))
982
+ sine = 0 if math.isclose(sine, 0, abs_tol=1e-12) else sine
983
+ cosine = 0 if math.isclose(cosine, 0, abs_tol=1e-12) else cosine
984
+ rotated_width = element["width"] * cosine + element["height"] * sine
985
+ rotated_height = element["width"] * sine + element["height"] * cosine
986
+ return {
987
+ "x": element["x"] - (rotated_width - element["width"]) / 2,
988
+ "y": element["y"] - (rotated_height - element["height"]) / 2,
989
+ "width": rotated_width,
990
+ "height": rotated_height,
991
+ }
992
+
993
+
994
+ def detect_elements_out_of_canvas(
995
+ elements: list[dict[str, Any]], slide_width: int | float, slide_height: int | float
996
+ ) -> list[dict[str, Any]]:
997
+ issues: list[dict[str, Any]] = []
998
+ for element in (
999
+ element
1000
+ for element in elements
1001
+ if element["kind"] in {"table", "chart"}
1002
+ or (element["kind"] == "shape" and element["type"] == "text")
1003
+ ):
1004
+ bbox = element_canvas_bbox(element)
1005
+ overflow = {
1006
+ "left": max(-bbox["x"], 0),
1007
+ "top": max(-bbox["y"], 0),
1008
+ "right": max(bbox["x"] + bbox["width"] - slide_width, 0),
1009
+ "bottom": max(bbox["y"] + bbox["height"] - slide_height, 0),
1010
+ }
1011
+ overflow_details = [
1012
+ f"{side} by {amount:g}px" for side, amount in overflow.items() if amount > 0
1013
+ ]
1014
+ if not overflow_details:
1015
+ continue
1016
+ issues.append(
1017
+ {
1018
+ "level": "error",
1019
+ "code": f'{element["kind"]}_out_of_canvas',
1020
+ "elements": [element["id"]],
1021
+ "canvas": {"width": slide_width, "height": slide_height},
1022
+ "bbox": bbox,
1023
+ "overflow": overflow,
1024
+ "message": (
1025
+ f'{element["kind"]} {element["id"]} exceeds the {slide_width:g}x{slide_height:g} canvas '
1026
+ f'({", ".join(overflow_details)})'
1027
+ ),
1028
+ "hint": (
1029
+ "Move the table inside the canvas, reduce table.width/table.height, or split the table across "
1030
+ "slides."
1031
+ if element["kind"] == "table"
1032
+ else f'Move the {element["kind"]} inside the canvas or reduce its width/height.'
1033
+ ),
1034
+ }
1035
+ )
1036
+ return issues
1037
+
1038
+
1039
+ def extract_table_column_sizes(table_xml: str) -> list[int | float | None]:
1040
+ sizes: list[int | float | None] = []
1041
+ for match in re.finditer(r"<col\b([^>]*)/?>", table_xml):
1042
+ attrs = match.group(1)
1043
+ span = extract_numeric_attribute(attrs, "span") or 1
1044
+ span_count = int(span) if math.isfinite(span) and span > 0 and float(span).is_integer() else 1
1045
+ sizes.extend([extract_numeric_attribute(attrs, "width")] * span_count)
1046
+ return sizes
1047
+
1048
+
1049
+ def extract_table_row_sizes(table_xml: str) -> list[int | float | None]:
1050
+ return [extract_numeric_attribute(match.group(1), "height") for match in re.finditer(r"<tr\b([^>]*)>", table_xml)]
1051
+
1052
+
1053
+ def resolve_table_dimension(
1054
+ table_xml: str,
1055
+ declared_size: int | float | None,
1056
+ extract_sizes: Any,
1057
+ default_size: int | float,
1058
+ ) -> tuple[int | float | None, dict[str, Any] | None]:
1059
+ input_sizes = extract_sizes(table_xml)
1060
+ if not input_sizes:
1061
+ return declared_size, None
1062
+ layout = solve_weighted_min_layout(
1063
+ input_sizes, default_size, declared_size if is_filled_size(declared_size) else None
1064
+ )
1065
+ return layout["actual_size"], layout
1066
+
1067
+
1068
+ def format_size(size: int | float) -> str:
1069
+ return f"{size:g}"
1070
+
1071
+
1072
+ def detect_table_layout_size_mismatches(elements: list[dict[str, Any]]) -> list[dict[str, Any]]:
1073
+ issues: list[dict[str, Any]] = []
1074
+ dimensions = {
1075
+ "width": ("col", "column widths"),
1076
+ "height": ("tr", "row heights"),
1077
+ }
1078
+ for table in (element for element in elements if element["kind"] == "table"):
1079
+ for dimension, (child_tag, child_description) in dimensions.items():
1080
+ target_size = table[f"declared_{dimension}"]
1081
+ if not is_filled_size(target_size):
1082
+ continue
1083
+ layout = table["table_layouts"][dimension]
1084
+ if layout is None:
1085
+ continue
1086
+ actual_size = layout["actual_size"]
1087
+ if math.isclose(actual_size, target_size, rel_tol=1e-9, abs_tol=1e-9):
1088
+ continue
1089
+ issues.append(
1090
+ {
1091
+ "level": "info",
1092
+ "code": "table_resolved_size_mismatch",
1093
+ "elements": [table["id"]],
1094
+ "dimension": dimension,
1095
+ "declared_size": target_size,
1096
+ "resolved_size": actual_size,
1097
+ "resolved_sizes": layout["final_sizes"],
1098
+ "message": (
1099
+ f'table {table["id"]} declares {dimension}={format_size(target_size)}px, but its '
1100
+ f"{child_description} resolve to {format_size(actual_size)}px"
1101
+ ),
1102
+ "hint": (
1103
+ f"Set table.{dimension} to {format_size(actual_size)}px, or adjust <{child_tag}> sizes "
1104
+ f"so their resolved total matches {format_size(target_size)}px."
1105
+ ),
1106
+ }
1107
+ )
1108
+ return issues
1109
+
1110
+
1111
+ def lint_slide(
1112
+ slide_xml: str, slide_number: int, slide_width: int | float = 960, slide_height: int | float = 540
1113
+ ) -> dict[str, Any]:
1114
+ elements = extract_elements(slide_xml)
1115
+ issues: list[dict[str, Any]] = [
1116
+ *detect_whiteboard_external_overlaps(elements, slide_width, slide_height),
1117
+ *detect_elements_out_of_canvas(elements, slide_width, slide_height),
1118
+ *detect_table_layout_size_mismatches(elements),
1119
+ ]
1120
+
1121
+ for index, left in enumerate(elements):
1122
+ for right in elements[index + 1 :]:
1123
+ horizontal_overflow = should_flag_horizontal_text_overflow(left, right)
1124
+ if not horizontal_overflow and (not intersects(left, right) or not should_flag_overlap(left, right)):
1125
+ continue
1126
+ issues.append(
1127
+ {
1128
+ "level": "error",
1129
+ "code": "bbox_overlap",
1130
+ "elements": [left["id"], right["id"]],
1131
+ "message": f'{left["id"]} overlaps {right["id"]}',
1132
+ }
1133
+ )
1134
+
1135
+ return {"slide_number": slide_number, "element_count": len(elements), "issues": issues}
1136
+
1137
+
1138
+ def lint_xml(xml: str, source_path: str | None = None) -> dict[str, Any]:
1139
+ root, xml_error = parse_xml_root(xml)
1140
+ if xml_error:
1141
+ return {
1142
+ "file": source_path,
1143
+ "slide_size": {"width": 960, "height": 540},
1144
+ "summary": {"slide_count": 0, "error_count": 1, "warning_count": 0, "info_count": 0},
1145
+ "issues": [xml_error],
1146
+ "slides": [],
1147
+ }
1148
+
1149
+ namespace_issues = validate_sml_tag_prefixes(xml)
1150
+ sxsd_issues = validate_sxsd_tag_attributes(root) if root is not None else []
1151
+ iconpark_issues = validate_iconpark_icon_types(root) if root is not None else []
1152
+ top_level_issues = [*namespace_issues, *sxsd_issues, *iconpark_issues]
1153
+ if namespace_issues:
1154
+ error_count = sum(1 for issue in top_level_issues if issue["level"] == "error")
1155
+ warning_count = sum(1 for issue in top_level_issues if issue["level"] == "warning")
1156
+ info_count = sum(1 for issue in top_level_issues if issue["level"] == "info")
1157
+ return {
1158
+ "file": source_path,
1159
+ "slide_size": {"width": 960, "height": 540},
1160
+ "summary": {
1161
+ "slide_count": 0,
1162
+ "error_count": error_count,
1163
+ "warning_count": warning_count,
1164
+ "info_count": info_count,
1165
+ },
1166
+ "issues": top_level_issues,
1167
+ "slides": [],
1168
+ }
1169
+ presentation = parse_presentation(xml)
1170
+ slides = [
1171
+ lint_slide(slide_xml, index + 1, presentation["width"], presentation["height"])
1172
+ for index, slide_xml in enumerate(presentation["slides"])
1173
+ ]
1174
+ error_count = sum(1 for issue in top_level_issues if issue["level"] == "error")
1175
+ error_count += sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "error")
1176
+ warning_count = sum(1 for issue in top_level_issues if issue["level"] == "warning")
1177
+ warning_count += sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "warning")
1178
+ info_count = sum(1 for issue in top_level_issues if issue["level"] == "info")
1179
+ info_count += sum(1 for slide in slides for issue in slide["issues"] if issue["level"] == "info")
1180
+ result = {
1181
+ "file": source_path,
1182
+ "slide_size": {"width": presentation["width"], "height": presentation["height"]},
1183
+ "summary": {
1184
+ "slide_count": len(slides),
1185
+ "error_count": error_count,
1186
+ "warning_count": warning_count,
1187
+ "info_count": info_count,
1188
+ },
1189
+ "slides": slides,
1190
+ }
1191
+ if top_level_issues:
1192
+ result["issues"] = top_level_issues
1193
+ return result
1194
+
1195
+
1196
+ def print_usage() -> None:
1197
+ print("Usage:\n python3 xml_text_overlap_lint.py --input <presentation.xml>", file=sys.stderr)
1198
+
1199
+
1200
+ def run_cli(argv: list[str] | None = None) -> None:
1201
+ options = parse_args(argv or sys.argv[1:])
1202
+ if options.get("help") or options.get("--help"):
1203
+ print_usage()
1204
+ raise SystemExit(0)
1205
+ if not options.get("input"):
1206
+ print_usage()
1207
+ fail("--input is required")
1208
+ input_path = Path(options["input"]).resolve()
1209
+ result = lint_xml(read_file(input_path), str(input_path))
1210
+ print(json.dumps(result, ensure_ascii=False, indent=2))
1211
+ if result["summary"]["error_count"] > 0:
1212
+ raise SystemExit(1)
1213
+
1214
+
1215
+ if __name__ == "__main__":
1216
+ try:
1217
+ run_cli()
1218
+ except XmlTextOverlapLintError as error:
1219
+ print(f"xml-text-overlap-lint error: {error}", file=sys.stderr)
1220
+ raise SystemExit(1) from error