@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.
- package/package.json +3 -3
- package/skills/lark-approval/SKILL.md +99 -0
- package/skills/lark-approval/references/lark-approval-approvals-get.md +128 -0
- package/skills/lark-approval/references/lark-approval-approvals-search.md +103 -0
- package/skills/lark-approval/references/lark-approval-initiate.md +221 -0
- package/skills/lark-approval/references/lark-approval-instance-form-control-parameters.md +606 -0
- package/skills/lark-approval/references/lark-approval-instance-value-sourcing.md +108 -0
- package/skills/lark-approval/references/lark-approval-instances-cancel.md +78 -0
- package/skills/lark-approval/references/lark-approval-instances-cc.md +105 -0
- package/skills/lark-approval/references/lark-approval-instances-get.md +145 -0
- package/skills/lark-approval/references/lark-approval-instances-initiated.md +128 -0
- package/skills/lark-approval/references/lark-approval-tasks-add-sign.md +120 -0
- package/skills/lark-approval/references/lark-approval-tasks-approve.md +81 -0
- package/skills/lark-approval/references/lark-approval-tasks-query.md +85 -0
- package/skills/lark-approval/references/lark-approval-tasks-reject.md +73 -0
- package/skills/lark-approval/references/lark-approval-tasks-remind.md +82 -0
- package/skills/lark-approval/references/lark-approval-tasks-rollback.md +89 -0
- package/skills/lark-approval/references/lark-approval-tasks-transfer.md +91 -0
- package/skills/lark-apps/SKILL.md +110 -0
- package/skills/lark-apps/references/lark-apps-access-scope-get.md +28 -0
- package/skills/lark-apps/references/lark-apps-access-scope-set.md +40 -0
- package/skills/lark-apps/references/lark-apps-automation.md +164 -0
- package/skills/lark-apps/references/lark-apps-cloud-dev.md +120 -0
- package/skills/lark-apps/references/lark-apps-create.md +40 -0
- package/skills/lark-apps/references/lark-apps-db-execute.md +228 -0
- package/skills/lark-apps/references/lark-apps-db.md +162 -0
- package/skills/lark-apps/references/lark-apps-env-pull.md +37 -0
- package/skills/lark-apps/references/lark-apps-env.md +48 -0
- package/skills/lark-apps/references/lark-apps-file.md +96 -0
- package/skills/lark-apps/references/lark-apps-get.md +43 -0
- package/skills/lark-apps/references/lark-apps-git-credential.md +37 -0
- package/skills/lark-apps/references/lark-apps-html-publish.md +62 -0
- package/skills/lark-apps/references/lark-apps-init.md +36 -0
- package/skills/lark-apps/references/lark-apps-list.md +37 -0
- package/skills/lark-apps/references/lark-apps-local-dev.md +78 -0
- package/skills/lark-apps/references/lark-apps-observability.md +48 -0
- package/skills/lark-apps/references/lark-apps-openapi-key.md +79 -0
- package/skills/lark-apps/references/lark-apps-plugin-install.md +36 -0
- package/skills/lark-apps/references/lark-apps-plugin-list.md +23 -0
- package/skills/lark-apps/references/lark-apps-plugin-uninstall.md +25 -0
- package/skills/lark-apps/references/lark-apps-release-create.md +32 -0
- package/skills/lark-apps/references/lark-apps-release-get.md +28 -0
- package/skills/lark-apps/references/lark-apps-release-list.md +31 -0
- package/skills/lark-apps/references/lark-apps-role.md +133 -0
- package/skills/lark-apps/references/lark-apps-session-messages-list.md +53 -0
- package/skills/lark-apps/references/lark-apps-update.md +30 -0
- package/skills/lark-attendance/SKILL.md +57 -0
- package/skills/lark-base/SKILL.md +161 -0
- package/skills/lark-base/references/dashboard-block-data-config.md +376 -0
- package/skills/lark-base/references/formula-field-guide.md +737 -0
- package/skills/lark-base/references/lark-base-cell-value.md +158 -0
- package/skills/lark-base/references/lark-base-dashboard-block-get-data.md +717 -0
- package/skills/lark-base/references/lark-base-dashboard.md +247 -0
- package/skills/lark-base/references/lark-base-data-analysis-sop.md +210 -0
- package/skills/lark-base/references/lark-base-data-query-guide.md +61 -0
- package/skills/lark-base/references/lark-base-data-query.md +454 -0
- package/skills/lark-base/references/lark-base-field-create.md +105 -0
- package/skills/lark-base/references/lark-base-field-json.md +527 -0
- package/skills/lark-base/references/lark-base-field-update.md +173 -0
- package/skills/lark-base/references/lark-base-form-detail.md +71 -0
- package/skills/lark-base/references/lark-base-form-questions-create.md +118 -0
- package/skills/lark-base/references/lark-base-form-questions-update.md +92 -0
- package/skills/lark-base/references/lark-base-form-submit.md +170 -0
- package/skills/lark-base/references/lark-base-record-batch-create.md +57 -0
- package/skills/lark-base/references/lark-base-record-batch-update.md +52 -0
- package/skills/lark-base/references/lark-base-record-history-list.md +43 -0
- package/skills/lark-base/references/lark-base-record-upsert.md +63 -0
- package/skills/lark-base/references/lark-base-role-guide.md +65 -0
- package/skills/lark-base/references/lark-base-view-set-filter.md +191 -0
- package/skills/lark-base/references/lark-base-workflow-guide.md +830 -0
- package/skills/lark-base/references/lark-base-workflow-schema.md +1071 -0
- package/skills/lark-base/references/lookup-field-guide.md +512 -0
- package/skills/lark-base/references/role-config.md +549 -0
- package/skills/lark-calendar/SKILL.md +198 -0
- package/skills/lark-calendar/references/lark-calendar-create.md +83 -0
- package/skills/lark-calendar/references/lark-calendar-meeting.md +40 -0
- package/skills/lark-calendar/references/lark-calendar-recurring.md +90 -0
- package/skills/lark-calendar/references/lark-calendar-room-find.md +109 -0
- package/skills/lark-calendar/references/lark-calendar-rsvp.md +38 -0
- package/skills/lark-calendar/references/lark-calendar-schedule-clear-time.md +59 -0
- package/skills/lark-calendar/references/lark-calendar-schedule-fuzzy-time.md +88 -0
- package/skills/lark-calendar/references/lark-calendar-schedule-meeting.md +122 -0
- package/skills/lark-calendar/references/lark-calendar-suggestion.md +121 -0
- package/skills/lark-calendar/references/lark-calendar-update.md +100 -0
- package/skills/lark-contact/SKILL.md +55 -0
- package/skills/lark-contact/references/lark-contact-get-user.md +19 -0
- package/skills/lark-contact/references/lark-contact-search-user.md +123 -0
- package/skills/lark-doc/SKILL.md +84 -0
- package/skills/lark-doc/references/lark-doc-create.md +80 -0
- package/skills/lark-doc/references/lark-doc-fetch.md +140 -0
- package/skills/lark-doc/references/lark-doc-history.md +107 -0
- package/skills/lark-doc/references/lark-doc-md.md +76 -0
- package/skills/lark-doc/references/lark-doc-media-download.md +50 -0
- package/skills/lark-doc/references/lark-doc-media-insert.md +114 -0
- package/skills/lark-doc/references/lark-doc-media-preview.md +41 -0
- package/skills/lark-doc/references/lark-doc-mindnote.md +128 -0
- package/skills/lark-doc/references/lark-doc-resource-cover.md +70 -0
- package/skills/lark-doc/references/lark-doc-update.md +260 -0
- package/skills/lark-doc/references/lark-doc-whiteboard.md +158 -0
- package/skills/lark-doc/references/lark-doc-word-stat.md +93 -0
- package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +35 -0
- package/skills/lark-doc/references/lark-doc-xml.md +182 -0
- package/skills/lark-doc/references/style/lark-doc-create-workflow.md +47 -0
- package/skills/lark-doc/references/style/lark-doc-style.md +68 -0
- package/skills/lark-doc/references/style/lark-doc-update-workflow.md +48 -0
- package/skills/lark-doc/scripts/doc_word_stat.py +1243 -0
- package/skills/lark-drive/SKILL.md +232 -0
- package/skills/lark-drive/references/lark-drive-add-comment.md +193 -0
- package/skills/lark-drive/references/lark-drive-apply-permission.md +77 -0
- package/skills/lark-drive/references/lark-drive-comment-location.md +205 -0
- package/skills/lark-drive/references/lark-drive-comments-guide.md +80 -0
- package/skills/lark-drive/references/lark-drive-cover.md +79 -0
- package/skills/lark-drive/references/lark-drive-create-folder.md +73 -0
- package/skills/lark-drive/references/lark-drive-create-shortcut.md +103 -0
- package/skills/lark-drive/references/lark-drive-delete.md +103 -0
- package/skills/lark-drive/references/lark-drive-download.md +31 -0
- package/skills/lark-drive/references/lark-drive-export-download.md +50 -0
- package/skills/lark-drive/references/lark-drive-export.md +174 -0
- package/skills/lark-drive/references/lark-drive-files-list.md +183 -0
- package/skills/lark-drive/references/lark-drive-import.md +178 -0
- package/skills/lark-drive/references/lark-drive-inspect.md +52 -0
- package/skills/lark-drive/references/lark-drive-list-comments.md +125 -0
- package/skills/lark-drive/references/lark-drive-member-add.md +66 -0
- package/skills/lark-drive/references/lark-drive-move.md +122 -0
- package/skills/lark-drive/references/lark-drive-permission-guide.md +53 -0
- package/skills/lark-drive/references/lark-drive-preview.md +87 -0
- package/skills/lark-drive/references/lark-drive-pull.md +137 -0
- package/skills/lark-drive/references/lark-drive-push.md +189 -0
- package/skills/lark-drive/references/lark-drive-reactions.md +113 -0
- package/skills/lark-drive/references/lark-drive-search.md +273 -0
- package/skills/lark-drive/references/lark-drive-secure-label.md +52 -0
- package/skills/lark-drive/references/lark-drive-status.md +135 -0
- package/skills/lark-drive/references/lark-drive-task-result.md +355 -0
- package/skills/lark-drive/references/lark-drive-upload.md +101 -0
- package/skills/lark-drive/references/lark-drive-version-delete.md +38 -0
- package/skills/lark-drive/references/lark-drive-version-get.md +71 -0
- package/skills/lark-drive/references/lark-drive-version-history.md +73 -0
- package/skills/lark-drive/references/lark-drive-version-revert.md +35 -0
- package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-analysis.md +249 -0
- package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-discovery.md +253 -0
- package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-execution.md +200 -0
- package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-planning.md +336 -0
- package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize-rollback.md +308 -0
- package/skills/lark-drive/references/lark-drive-workflow-knowledge-organize.md +232 -0
- package/skills/lark-drive/references/lark-drive-workflow-permission-governance-commands.md +168 -0
- package/skills/lark-drive/references/lark-drive-workflow-permission-governance-outputs.md +424 -0
- package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +207 -0
- package/skills/lark-drive/references/lark-drive-workflow.md +131 -0
- package/skills/lark-event/SKILL.md +155 -0
- package/skills/lark-event/references/lark-event-approval.md +170 -0
- package/skills/lark-event/references/lark-event-im.md +87 -0
- package/skills/lark-event/references/lark-event-minutes.md +54 -0
- package/skills/lark-event/references/lark-event-task.md +78 -0
- package/skills/lark-event/references/lark-event-vc.md +106 -0
- package/skills/lark-event/references/lark-event-whiteboard.md +67 -0
- package/skills/lark-im/SKILL.md +249 -0
- package/skills/lark-im/references/card/card-2.0-schema.md +107 -0
- package/skills/lark-im/references/card/components/button.md +63 -0
- package/skills/lark-im/references/card/components/chart.md +57 -0
- package/skills/lark-im/references/card/components/checker.md +38 -0
- package/skills/lark-im/references/card/components/collapsible_panel.md +46 -0
- package/skills/lark-im/references/card/components/column_set.md +53 -0
- package/skills/lark-im/references/card/components/date_picker.md +34 -0
- package/skills/lark-im/references/card/components/div.md +36 -0
- package/skills/lark-im/references/card/components/form.md +51 -0
- package/skills/lark-im/references/card/components/header.md +34 -0
- package/skills/lark-im/references/card/components/hr.md +17 -0
- package/skills/lark-im/references/card/components/img.md +34 -0
- package/skills/lark-im/references/card/components/img_combination.md +30 -0
- package/skills/lark-im/references/card/components/input.md +43 -0
- package/skills/lark-im/references/card/components/interactive_container.md +46 -0
- package/skills/lark-im/references/card/components/markdown.md +56 -0
- package/skills/lark-im/references/card/components/multi_select_person.md +40 -0
- package/skills/lark-im/references/card/components/multi_select_static.md +40 -0
- package/skills/lark-im/references/card/components/overflow.md +36 -0
- package/skills/lark-im/references/card/components/person.md +30 -0
- package/skills/lark-im/references/card/components/person_list.md +31 -0
- package/skills/lark-im/references/card/components/picker_datetime.md +34 -0
- package/skills/lark-im/references/card/components/picker_time.md +34 -0
- package/skills/lark-im/references/card/components/recycling_container.md +35 -0
- package/skills/lark-im/references/card/components/select_img.md +42 -0
- package/skills/lark-im/references/card/components/select_person.md +39 -0
- package/skills/lark-im/references/card/components/select_static.md +43 -0
- package/skills/lark-im/references/card/components/table.md +53 -0
- package/skills/lark-im/references/card/lark-im-card-create.md +180 -0
- package/skills/lark-im/references/card/lark-im-card-style.md +281 -0
- package/skills/lark-im/references/card/resource/colors.md +34 -0
- package/skills/lark-im/references/card/resource/icons.md +38 -0
- package/skills/lark-im/references/lark-im-card-action-reply.md +175 -0
- package/skills/lark-im/references/lark-im-chat-create.md +162 -0
- package/skills/lark-im/references/lark-im-chat-identity.md +55 -0
- package/skills/lark-im/references/lark-im-chat-list.md +166 -0
- package/skills/lark-im/references/lark-im-chat-members-list.md +83 -0
- package/skills/lark-im/references/lark-im-chat-messages-list.md +157 -0
- package/skills/lark-im/references/lark-im-chat-search.md +142 -0
- package/skills/lark-im/references/lark-im-chat-update.md +84 -0
- package/skills/lark-im/references/lark-im-feed-group-list-item.md +68 -0
- package/skills/lark-im/references/lark-im-feed-group-list.md +65 -0
- package/skills/lark-im/references/lark-im-feed-group-query-item.md +44 -0
- package/skills/lark-im/references/lark-im-feed-groups.md +452 -0
- package/skills/lark-im/references/lark-im-feed-shortcut-create.md +97 -0
- package/skills/lark-im/references/lark-im-feed-shortcut-list.md +103 -0
- package/skills/lark-im/references/lark-im-feed-shortcut-remove.md +48 -0
- package/skills/lark-im/references/lark-im-flag-cancel.md +67 -0
- package/skills/lark-im/references/lark-im-flag-create.md +67 -0
- package/skills/lark-im/references/lark-im-flag-list.md +100 -0
- package/skills/lark-im/references/lark-im-message-enrichment.md +54 -0
- package/skills/lark-im/references/lark-im-messages-mget.md +99 -0
- package/skills/lark-im/references/lark-im-messages-reply.md +277 -0
- package/skills/lark-im/references/lark-im-messages-resources-download.md +94 -0
- package/skills/lark-im/references/lark-im-messages-search.md +234 -0
- package/skills/lark-im/references/lark-im-messages-send.md +279 -0
- package/skills/lark-im/references/lark-im-reactions.md +299 -0
- package/skills/lark-im/references/lark-im-threads-messages-list.md +115 -0
- package/skills/lark-mail/SKILL.md +290 -0
- package/skills/lark-mail/assets/templates/job-application--resume.html +33 -0
- package/skills/lark-mail/assets/templates/newsletter--weekly-brief.html +50 -0
- package/skills/lark-mail/assets/templates/research--market-report.html +256 -0
- package/skills/lark-mail/assets/templates/weekly--personal-report.html +43 -0
- package/skills/lark-mail/assets/templates/weekly--team-report.html +9 -0
- package/skills/lark-mail/references/lark-mail-calendar-invite.md +36 -0
- package/skills/lark-mail/references/lark-mail-decline-receipt.md +115 -0
- package/skills/lark-mail/references/lark-mail-draft-create.md +127 -0
- package/skills/lark-mail/references/lark-mail-draft-edit.md +404 -0
- package/skills/lark-mail/references/lark-mail-forward.md +239 -0
- package/skills/lark-mail/references/lark-mail-html.md +333 -0
- package/skills/lark-mail/references/lark-mail-lint-html.md +243 -0
- package/skills/lark-mail/references/lark-mail-message-modify.md +48 -0
- package/skills/lark-mail/references/lark-mail-message-trash.md +41 -0
- package/skills/lark-mail/references/lark-mail-message.md +233 -0
- package/skills/lark-mail/references/lark-mail-messages.md +108 -0
- package/skills/lark-mail/references/lark-mail-recall.md +66 -0
- package/skills/lark-mail/references/lark-mail-recipient-search.md +59 -0
- package/skills/lark-mail/references/lark-mail-reply-all.md +213 -0
- package/skills/lark-mail/references/lark-mail-reply.md +249 -0
- package/skills/lark-mail/references/lark-mail-rules.md +31 -0
- package/skills/lark-mail/references/lark-mail-send-as.md +44 -0
- package/skills/lark-mail/references/lark-mail-send-receipt.md +120 -0
- package/skills/lark-mail/references/lark-mail-send-status.md +46 -0
- package/skills/lark-mail/references/lark-mail-send.md +222 -0
- package/skills/lark-mail/references/lark-mail-share-to-chat.md +87 -0
- package/skills/lark-mail/references/lark-mail-signature.md +98 -0
- package/skills/lark-mail/references/lark-mail-template-create.md +129 -0
- package/skills/lark-mail/references/lark-mail-template-update.md +150 -0
- package/skills/lark-mail/references/lark-mail-template.md +54 -0
- package/skills/lark-mail/references/lark-mail-thread.md +111 -0
- package/skills/lark-mail/references/lark-mail-triage.md +131 -0
- package/skills/lark-mail/references/lark-mail-watch.md +94 -0
- package/skills/lark-markdown/SKILL.md +70 -0
- package/skills/lark-markdown/references/lark-markdown-create.md +114 -0
- package/skills/lark-markdown/references/lark-markdown-diff.md +156 -0
- package/skills/lark-markdown/references/lark-markdown-fetch.md +79 -0
- package/skills/lark-markdown/references/lark-markdown-overwrite.md +85 -0
- package/skills/lark-markdown/references/lark-markdown-patch.md +160 -0
- package/skills/lark-minutes/SKILL.md +207 -0
- package/skills/lark-minutes/references/lark-minutes-detail.md +62 -0
- package/skills/lark-minutes/references/lark-minutes-download.md +135 -0
- package/skills/lark-minutes/references/lark-minutes-search.md +202 -0
- package/skills/lark-minutes/references/lark-minutes-speaker-replace.md +107 -0
- package/skills/lark-minutes/references/lark-minutes-summary.md +120 -0
- package/skills/lark-minutes/references/lark-minutes-todo.md +136 -0
- package/skills/lark-minutes/references/lark-minutes-update.md +39 -0
- package/skills/lark-minutes/references/lark-minutes-upload.md +104 -0
- package/skills/lark-note/SKILL.md +94 -0
- package/skills/lark-note/references/lark-note-detail.md +26 -0
- package/skills/lark-note/references/lark-note-transcript.md +23 -0
- package/skills/lark-okr/SKILL.md +122 -0
- package/skills/lark-okr/references/lark-okr-alignments.md +180 -0
- package/skills/lark-okr/references/lark-okr-batch-create.md +106 -0
- package/skills/lark-okr/references/lark-okr-contentblock.md +427 -0
- package/skills/lark-okr/references/lark-okr-cycle-detail.md +91 -0
- package/skills/lark-okr/references/lark-okr-cycle-list.md +93 -0
- package/skills/lark-okr/references/lark-okr-entities.md +329 -0
- package/skills/lark-okr/references/lark-okr-image-upload.md +116 -0
- package/skills/lark-okr/references/lark-okr-indicator-update.md +80 -0
- package/skills/lark-okr/references/lark-okr-indicators.md +223 -0
- package/skills/lark-okr/references/lark-okr-patch.md +104 -0
- package/skills/lark-okr/references/lark-okr-progress-create.md +85 -0
- package/skills/lark-okr/references/lark-okr-progress-delete.md +47 -0
- package/skills/lark-okr/references/lark-okr-progress-get.md +93 -0
- package/skills/lark-okr/references/lark-okr-progress-list.md +80 -0
- package/skills/lark-okr/references/lark-okr-progress-update.md +85 -0
- package/skills/lark-okr/references/lark-okr-reorder.md +81 -0
- package/skills/lark-okr/references/lark-okr-weight.md +96 -0
- package/skills/lark-openapi-explorer/SKILL.md +153 -0
- package/skills/lark-shared/SKILL.md +211 -0
- package/skills/lark-shared/references/lark-wiki-token-routing.md +42 -0
- package/skills/lark-sheets/SKILL.md +234 -0
- package/skills/lark-sheets/references/lark-sheets-batch-update.md +200 -0
- package/skills/lark-sheets/references/lark-sheets-changeset.md +105 -0
- package/skills/lark-sheets/references/lark-sheets-chart.md +332 -0
- package/skills/lark-sheets/references/lark-sheets-conditional-format.md +181 -0
- package/skills/lark-sheets/references/lark-sheets-filter-view.md +137 -0
- package/skills/lark-sheets/references/lark-sheets-filter.md +130 -0
- package/skills/lark-sheets/references/lark-sheets-float-image.md +159 -0
- package/skills/lark-sheets/references/lark-sheets-formula-translation.md +276 -0
- package/skills/lark-sheets/references/lark-sheets-formula-verify.md +77 -0
- package/skills/lark-sheets/references/lark-sheets-history.md +93 -0
- package/skills/lark-sheets/references/lark-sheets-pivot-table.md +171 -0
- package/skills/lark-sheets/references/lark-sheets-range-operations.md +297 -0
- package/skills/lark-sheets/references/lark-sheets-read-data.md +235 -0
- package/skills/lark-sheets/references/lark-sheets-search-replace.md +111 -0
- package/skills/lark-sheets/references/lark-sheets-sheet-structure.md +212 -0
- package/skills/lark-sheets/references/lark-sheets-sparkline.md +149 -0
- package/skills/lark-sheets/references/lark-sheets-visual-standards.md +205 -0
- package/skills/lark-sheets/references/lark-sheets-workbook.md +420 -0
- package/skills/lark-sheets/references/lark-sheets-write-cells.md +575 -0
- package/skills/lark-sheets/scripts/sheets_df.py +32 -0
- package/skills/lark-skill-maker/SKILL.md +85 -0
- package/skills/lark-slides/SKILL.md +285 -0
- package/skills/lark-slides/references/asset-planning.md +135 -0
- package/skills/lark-slides/references/examples.md +91 -0
- package/skills/lark-slides/references/iconpark-index.json +41901 -0
- package/skills/lark-slides/references/iconpark.md +46 -0
- package/skills/lark-slides/references/lark-slides-create.md +156 -0
- package/skills/lark-slides/references/lark-slides-edit-workflows.md +144 -0
- package/skills/lark-slides/references/lark-slides-media-upload.md +127 -0
- package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +89 -0
- package/skills/lark-slides/references/lark-slides-replace-pages.md +95 -0
- package/skills/lark-slides/references/lark-slides-replace-slide.md +240 -0
- package/skills/lark-slides/references/lark-slides-screenshot.md +97 -0
- package/skills/lark-slides/references/lark-slides-whiteboard.md +331 -0
- package/skills/lark-slides/references/lark-slides-xml-get.md +100 -0
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +125 -0
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +110 -0
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +189 -0
- package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +99 -0
- package/skills/lark-slides/references/planning-layer.md +246 -0
- package/skills/lark-slides/references/slide-templates.md +201 -0
- package/skills/lark-slides/references/slides_chart_demo.xml +1 -0
- package/skills/lark-slides/references/slides_demo.xml +226 -0
- package/skills/lark-slides/references/slides_xml_schema_definition.xml +3054 -0
- package/skills/lark-slides/references/troubleshooting.md +45 -0
- package/skills/lark-slides/references/validation-checklist.md +119 -0
- package/skills/lark-slides/references/visual-planning.md +255 -0
- package/skills/lark-slides/references/xml-format-guide.md +433 -0
- package/skills/lark-slides/references/xml-schema-quick-ref.md +249 -0
- package/skills/lark-slides/scripts/iconpark_tool.py +362 -0
- package/skills/lark-slides/scripts/iconpark_tool_test.py +177 -0
- package/skills/lark-slides/scripts/xml_text_overlap_lint.py +1220 -0
- package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +1170 -0
- package/skills/lark-task/SKILL.md +168 -0
- package/skills/lark-task/references/lark-task-assign.md +38 -0
- package/skills/lark-task/references/lark-task-comment.md +28 -0
- package/skills/lark-task/references/lark-task-complete.md +27 -0
- package/skills/lark-task/references/lark-task-create.md +70 -0
- package/skills/lark-task/references/lark-task-followers.md +35 -0
- package/skills/lark-task/references/lark-task-get-my-tasks.md +61 -0
- package/skills/lark-task/references/lark-task-get-related-tasks.md +53 -0
- package/skills/lark-task/references/lark-task-reminder.md +36 -0
- package/skills/lark-task/references/lark-task-reopen.md +27 -0
- package/skills/lark-task/references/lark-task-search.md +41 -0
- package/skills/lark-task/references/lark-task-set-ancestor.md +32 -0
- package/skills/lark-task/references/lark-task-tasklist-create.md +35 -0
- package/skills/lark-task/references/lark-task-tasklist-members.md +36 -0
- package/skills/lark-task/references/lark-task-tasklist-search.md +38 -0
- package/skills/lark-task/references/lark-task-tasklist-task-add.md +38 -0
- package/skills/lark-task/references/lark-task-update.md +37 -0
- package/skills/lark-task/references/lark-task-upload-attachment.md +59 -0
- package/skills/lark-vc/SKILL.md +205 -0
- package/skills/lark-vc/references/lark-vc-detail.md +44 -0
- package/skills/lark-vc/references/lark-vc-recording.md +152 -0
- package/skills/lark-vc/references/lark-vc-search.md +163 -0
- package/skills/lark-vc/references/vc-domain-boundaries.md +196 -0
- package/skills/lark-vc-agent/SKILL.md +201 -0
- package/skills/lark-vc-agent/references/lark-vc-agent-meeting-events.md +315 -0
- package/skills/lark-vc-agent/references/lark-vc-agent-meeting-join.md +141 -0
- package/skills/lark-vc-agent/references/lark-vc-agent-meeting-leave.md +105 -0
- package/skills/lark-vc-agent/references/lark-vc-agent-meeting-list-active.md +91 -0
- package/skills/lark-vc-agent/references/lark-vc-agent-meeting-message-send.md +134 -0
- package/skills/lark-whiteboard/SKILL.md +47 -0
- package/skills/lark-whiteboard/elements/connectors.md +102 -0
- package/skills/lark-whiteboard/elements/content.md +40 -0
- package/skills/lark-whiteboard/elements/image.md +80 -0
- package/skills/lark-whiteboard/elements/layout.md +374 -0
- package/skills/lark-whiteboard/elements/schema.md +357 -0
- package/skills/lark-whiteboard/elements/style.md +318 -0
- package/skills/lark-whiteboard/elements/typography.md +73 -0
- package/skills/lark-whiteboard/references/lark-whiteboard-query.md +60 -0
- package/skills/lark-whiteboard/references/lark-whiteboard-update.md +122 -0
- package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +97 -0
- package/skills/lark-whiteboard/routes/dsl.md +107 -0
- package/skills/lark-whiteboard/routes/mermaid.md +27 -0
- package/skills/lark-whiteboard/routes/svg-edit.md +85 -0
- package/skills/lark-whiteboard/routes/svg.md +54 -0
- package/skills/lark-whiteboard/scenes/architecture.md +433 -0
- package/skills/lark-whiteboard/scenes/bar-chart.md +187 -0
- package/skills/lark-whiteboard/scenes/comparison.md +135 -0
- package/skills/lark-whiteboard/scenes/fishbone.md +238 -0
- package/skills/lark-whiteboard/scenes/flowchart.md +185 -0
- package/skills/lark-whiteboard/scenes/flywheel.md +195 -0
- package/skills/lark-whiteboard/scenes/funnel.md +101 -0
- package/skills/lark-whiteboard/scenes/line-chart.md +214 -0
- package/skills/lark-whiteboard/scenes/mermaid.md +130 -0
- package/skills/lark-whiteboard/scenes/milestone.md +139 -0
- package/skills/lark-whiteboard/scenes/organization.md +173 -0
- package/skills/lark-whiteboard/scenes/photo-showcase.md +126 -0
- package/skills/lark-whiteboard/scenes/pyramid.md +99 -0
- package/skills/lark-whiteboard/scenes/swimlane.md +371 -0
- package/skills/lark-whiteboard/scenes/treemap.md +216 -0
- package/skills/lark-wiki/SKILL.md +114 -0
- package/skills/lark-wiki/references/lark-wiki-delete-space.md +205 -0
- package/skills/lark-wiki/references/lark-wiki-member-add.md +67 -0
- package/skills/lark-wiki/references/lark-wiki-member-list.md +76 -0
- package/skills/lark-wiki/references/lark-wiki-member-remove.md +61 -0
- package/skills/lark-wiki/references/lark-wiki-move-to-drive.md +122 -0
- package/skills/lark-wiki/references/lark-wiki-move.md +185 -0
- package/skills/lark-wiki/references/lark-wiki-node-copy.md +72 -0
- package/skills/lark-wiki/references/lark-wiki-node-create.md +127 -0
- package/skills/lark-wiki/references/lark-wiki-node-delete.md +62 -0
- package/skills/lark-wiki/references/lark-wiki-node-get.md +57 -0
- package/skills/lark-wiki/references/lark-wiki-node-list.md +95 -0
- package/skills/lark-wiki/references/lark-wiki-space-create.md +46 -0
- package/skills/lark-wiki/references/lark-wiki-space-list.md +68 -0
- package/skills/lark-workflow-meeting-summary/SKILL.md +122 -0
- 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(" ", " ")
|
|
175
|
+
stripped = stripped.replace("&", "&")
|
|
176
|
+
stripped = stripped.replace("<", "<")
|
|
177
|
+
stripped = stripped.replace(">", ">")
|
|
178
|
+
stripped = stripped.replace(""", '"')
|
|
179
|
+
stripped = stripped.replace("'", "'")
|
|
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 &. In text nodes, write < as < and > as >. For attribute URLs, use a=1&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
|