@amaster.ai/pi-lark 0.1.6 → 0.1.8

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 (266) hide show
  1. package/README.md +5 -1
  2. package/dist/config.d.ts +1 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +2 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/package.json +3 -3
  10. package/skills/lark-apps/SKILL.md +59 -14
  11. package/skills/lark-apps/creative-design/agents/assets/vision-probe.png +0 -0
  12. package/skills/lark-apps/creative-design/agents/fork-verifier-agent.md +71 -0
  13. package/skills/lark-apps/creative-design/agents/vision-probe-agent.md +41 -0
  14. package/skills/lark-apps/creative-design/assets/index.html +27 -0
  15. package/skills/lark-apps/creative-design/creative-design.md +239 -0
  16. package/skills/lark-apps/creative-design/references/aily.md +39 -0
  17. package/skills/lark-apps/creative-design/references/animated-video.md +34 -0
  18. package/skills/lark-apps/creative-design/references/charts.md +165 -0
  19. package/skills/lark-apps/creative-design/references/claude.md +36 -0
  20. package/skills/lark-apps/creative-design/references/codex.md +32 -0
  21. package/skills/lark-apps/creative-design/references/data-report.md +108 -0
  22. package/skills/lark-apps/creative-design/references/frontend-design.md +71 -0
  23. package/skills/lark-apps/creative-design/references/hi-fi-design.md +32 -0
  24. package/skills/lark-apps/creative-design/references/interactive-prototype.md +24 -0
  25. package/skills/lark-apps/creative-design/references/make-a-deck.md +133 -0
  26. package/skills/lark-apps/creative-design/references/visual-exposure.md +82 -0
  27. package/skills/lark-apps/creative-design/references/wireframe.md +14 -0
  28. package/skills/lark-apps/creative-design/starter-components/android-frame.jsx +188 -0
  29. package/skills/lark-apps/creative-design/starter-components/animations.jsx +773 -0
  30. package/skills/lark-apps/creative-design/starter-components/browser-window.jsx +122 -0
  31. package/skills/lark-apps/creative-design/starter-components/deck-stage.js +2483 -0
  32. package/skills/lark-apps/creative-design/starter-components/design-canvas.jsx +1432 -0
  33. package/skills/lark-apps/creative-design/starter-components/ios-frame.jsx +270 -0
  34. package/skills/lark-apps/creative-design/starter-components/macos-window.jsx +197 -0
  35. package/skills/lark-apps/creative-design/starter-components/tweaks-panel.jsx +752 -0
  36. package/skills/lark-apps/references/lark-apps-automation.md +80 -2
  37. package/skills/lark-apps/references/lark-apps-cache.md +61 -0
  38. package/skills/lark-apps/references/lark-apps-cloud-dev.md +5 -5
  39. package/skills/lark-apps/references/lark-apps-create.md +6 -4
  40. package/skills/lark-apps/references/lark-apps-db.md +1 -1
  41. package/skills/lark-apps/references/lark-apps-env-pull.md +1 -1
  42. package/skills/lark-apps/references/lark-apps-file.md +2 -2
  43. package/skills/lark-apps/references/lark-apps-get.md +1 -1
  44. package/skills/lark-apps/references/lark-apps-git-credential.md +1 -1
  45. package/skills/lark-apps/references/lark-apps-html-publish.md +4 -8
  46. package/skills/lark-apps/references/lark-apps-init.md +1 -1
  47. package/skills/lark-apps/references/lark-apps-list.md +2 -2
  48. package/skills/lark-apps/references/lark-apps-local-dev.md +80 -11
  49. package/skills/lark-apps/references/lark-apps-openapi-key.md +1 -1
  50. package/skills/lark-apps/references/lark-apps-release-create.md +2 -2
  51. package/skills/lark-apps/references/lark-apps-release-get.md +3 -3
  52. package/skills/lark-base/SKILL.md +34 -19
  53. package/skills/lark-base/references/lark-base-cell-value.md +3 -3
  54. package/skills/lark-base/references/lark-base-dashboard-block-get-data.md +17 -1
  55. package/skills/lark-base/references/lark-base-dashboard.md +17 -4
  56. package/skills/lark-base/references/lark-base-data-query-guide.md +8 -0
  57. package/skills/lark-base/references/lark-base-data-query.md +11 -4
  58. package/skills/lark-base/references/lark-base-field-create.md +21 -6
  59. package/skills/lark-base/references/lark-base-field-json.md +9 -6
  60. package/skills/lark-base/references/lark-base-field-update.md +17 -1
  61. package/skills/lark-base/references/lark-base-filter-condition.md +179 -0
  62. package/skills/lark-base/references/lark-base-form-questions-create.md +40 -7
  63. package/skills/lark-base/references/lark-base-form-questions-update.md +73 -20
  64. package/skills/lark-base/references/lark-base-form-submit.md +16 -7
  65. package/skills/lark-base/references/lark-base-record-batch-create.md +12 -10
  66. package/skills/lark-base/references/lark-base-record-batch-update.md +11 -9
  67. package/skills/lark-base/references/lark-base-record-upsert.md +1 -1
  68. package/skills/lark-base/references/lark-base-role-guide.md +11 -0
  69. package/skills/lark-base/references/lark-base-view-set-filter.md +11 -137
  70. package/skills/lark-base/references/role-config.md +31 -5
  71. package/skills/lark-calendar/SKILL.md +14 -8
  72. package/skills/lark-calendar/references/lark-calendar-create.md +6 -5
  73. package/skills/lark-calendar/references/lark-calendar-recurring.md +1 -0
  74. package/skills/lark-calendar/references/lark-calendar-room-find.md +2 -1
  75. package/skills/lark-calendar/references/lark-calendar-schedule-clear-time.md +1 -0
  76. package/skills/lark-calendar/references/lark-calendar-suggestion.md +1 -1
  77. package/skills/lark-calendar/references/lark-calendar-update.md +10 -4
  78. package/skills/lark-contact/SKILL.md +19 -3
  79. package/skills/lark-contact/references/lark-contact-search-bot.md +60 -0
  80. package/skills/lark-doc/SKILL.md +26 -61
  81. package/skills/lark-doc/references/genres/business-analysis.md +30 -0
  82. package/skills/lark-doc/references/genres/data-report.md +32 -0
  83. package/skills/lark-doc/references/genres/email.md +38 -0
  84. package/skills/lark-doc/references/genres/execution-plan.md +27 -0
  85. package/skills/lark-doc/references/genres/formal-doc.md +37 -0
  86. package/skills/lark-doc/references/genres/meeting-minutes.md +24 -0
  87. package/skills/lark-doc/references/genres/memo-brief.md +25 -0
  88. package/skills/lark-doc/references/genres/official-redhead.md +73 -0
  89. package/skills/lark-doc/references/genres/prd.md +26 -0
  90. package/skills/lark-doc/references/genres/proposal.md +24 -0
  91. package/skills/lark-doc/references/genres/research-report.md +32 -0
  92. package/skills/lark-doc/references/genres/retrospective.md +25 -0
  93. package/skills/lark-doc/references/genres/route-consumer.md +37 -0
  94. package/skills/lark-doc/references/genres/route-creative.md +36 -0
  95. package/skills/lark-doc/references/genres/route-knowledge.md +39 -0
  96. package/skills/lark-doc/references/genres/route-marketing.md +40 -0
  97. package/skills/lark-doc/references/genres/route-media.md +36 -0
  98. package/skills/lark-doc/references/genres/route-opinion.md +38 -0
  99. package/skills/lark-doc/references/genres/route-personal-brand.md +36 -0
  100. package/skills/lark-doc/references/genres/route-platform.md +9 -0
  101. package/skills/lark-doc/references/genres/route-report.md +10 -0
  102. package/skills/lark-doc/references/genres/route-workplace.md +17 -0
  103. package/skills/lark-doc/references/genres/sop-tutorial.md +41 -0
  104. package/skills/lark-doc/references/genres/technical-doc.md +39 -0
  105. package/skills/lark-doc/references/genres/wechat.md +39 -0
  106. package/skills/lark-doc/references/genres/weekly-report.md +24 -0
  107. package/skills/lark-doc/references/genres/white-paper.md +32 -0
  108. package/skills/lark-doc/references/genres/xiaohongshu.md +38 -0
  109. package/skills/lark-doc/references/lark-doc-create-workflow.md +121 -0
  110. package/skills/lark-doc/references/lark-doc-create.md +22 -48
  111. package/skills/lark-doc/references/lark-doc-fetch.md +84 -93
  112. package/skills/lark-doc/references/lark-doc-history.md +16 -15
  113. package/skills/lark-doc/references/lark-doc-md.md +5 -1
  114. package/skills/lark-doc/references/lark-doc-media-download.md +2 -1
  115. package/skills/lark-doc/references/lark-doc-script.md +76 -0
  116. package/skills/lark-doc/references/lark-doc-update.md +70 -222
  117. package/skills/lark-doc/references/lark-doc-whiteboard.md +14 -17
  118. package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +46 -0
  119. package/skills/lark-doc/references/lark-doc-xml.md +38 -166
  120. package/skills/lark-drive/SKILL.md +32 -50
  121. package/skills/lark-drive/references/lark-drive-add-comment.md +2 -4
  122. package/skills/lark-drive/references/lark-drive-add-reply.md +47 -0
  123. package/skills/lark-drive/references/lark-drive-apply-permission.md +3 -3
  124. package/skills/lark-drive/references/lark-drive-batch-query-comments.md +46 -0
  125. package/skills/lark-drive/references/lark-drive-comment-content.md +50 -0
  126. package/skills/lark-drive/references/lark-drive-comment-location.md +9 -15
  127. package/skills/lark-drive/references/lark-drive-copy.md +87 -0
  128. package/skills/lark-drive/references/lark-drive-delete-reply.md +48 -0
  129. package/skills/lark-drive/references/lark-drive-download.md +6 -1
  130. package/skills/lark-drive/references/lark-drive-export.md +3 -0
  131. package/skills/lark-drive/references/lark-drive-list-comments.md +25 -68
  132. package/skills/lark-drive/references/lark-drive-list-replies.md +54 -0
  133. package/skills/lark-drive/references/lark-drive-member-add.md +2 -2
  134. package/skills/lark-drive/references/lark-drive-member-list.md +65 -0
  135. package/skills/lark-drive/references/lark-drive-permission-get-setting.md +48 -0
  136. package/skills/lark-drive/references/lark-drive-preview.md +11 -1
  137. package/skills/lark-drive/references/lark-drive-react-reply.md +51 -0
  138. package/skills/lark-drive/references/lark-drive-reactions.md +27 -25
  139. package/skills/lark-drive/references/lark-drive-resolve-comment.md +45 -0
  140. package/skills/lark-drive/references/lark-drive-restore-comment.md +46 -0
  141. package/skills/lark-drive/references/lark-drive-search.md +7 -1
  142. package/skills/lark-drive/references/lark-drive-secure-label.md +1 -1
  143. package/skills/lark-drive/references/lark-drive-task-result.md +3 -0
  144. package/skills/lark-drive/references/lark-drive-update-reply.md +46 -0
  145. package/skills/lark-drive/references/lark-drive-update-title.md +78 -0
  146. package/skills/lark-drive/references/lark-drive-upload.md +1 -0
  147. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-commands.md +38 -8
  148. package/skills/lark-drive/references/lark-drive-workflow-permission-governance-outputs.md +10 -10
  149. package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +22 -20
  150. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-execute.md +273 -0
  151. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-recall.md +202 -0
  152. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-resolve-verify.md +231 -0
  153. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-review-plan.md +248 -0
  154. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-setup.md +174 -0
  155. package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector.md +202 -0
  156. package/skills/lark-drive/references/lark-drive-workflow.md +5 -4
  157. package/skills/lark-event/SKILL.md +8 -4
  158. package/skills/lark-event/references/lark-event-application.md +38 -0
  159. package/skills/lark-event/references/lark-event-vc.md +8 -2
  160. package/skills/lark-im/SKILL.md +9 -9
  161. package/skills/lark-im/references/card/card-2.0-schema.md +1 -1
  162. package/skills/lark-im/references/card/lark-im-card-style.md +4 -4
  163. package/skills/lark-im/references/card/resource/icons.md +14 -0
  164. package/skills/lark-im/references/lark-im-chat-list.md +9 -2
  165. package/skills/lark-im/references/lark-im-chat-members-list.md +7 -4
  166. package/skills/lark-im/references/lark-im-chat-messages-list.md +10 -3
  167. package/skills/lark-im/references/lark-im-chat-search.md +9 -2
  168. package/skills/lark-im/references/lark-im-feed-group-list-item.md +2 -2
  169. package/skills/lark-im/references/lark-im-feed-group-list.md +2 -2
  170. package/skills/lark-im/references/lark-im-feed-shortcut-list.md +1 -1
  171. package/skills/lark-im/references/lark-im-flag-list.md +9 -8
  172. package/skills/lark-im/references/lark-im-message-enrichment.md +1 -1
  173. package/skills/lark-im/references/lark-im-messages-resources-download.md +19 -25
  174. package/skills/lark-im/references/lark-im-messages-search.md +4 -5
  175. package/skills/lark-im/references/lark-im-threads-messages-list.md +8 -4
  176. package/skills/lark-mail/references/lark-mail-triage.md +19 -4
  177. package/skills/lark-minutes/SKILL.md +1 -1
  178. package/skills/lark-minutes/references/lark-minutes-search.md +6 -7
  179. package/skills/lark-okr/SKILL.md +71 -26
  180. package/skills/lark-okr/references/lark-okr-batch-create.md +19 -18
  181. package/skills/lark-okr/references/lark-okr-create.md +173 -0
  182. package/skills/lark-okr/references/lark-okr-cycle-list.md +17 -7
  183. package/skills/lark-okr/references/lark-okr-entities.md +1 -0
  184. package/skills/lark-okr/references/lark-okr-indicator-update.md +3 -1
  185. package/skills/lark-okr/references/lark-okr-indicators.md +61 -12
  186. package/skills/lark-okr/references/lark-okr-progress-list.md +21 -9
  187. package/skills/lark-shared/SKILL.md +3 -3
  188. package/skills/lark-sheets/SKILL.md +83 -82
  189. package/skills/lark-sheets/references/lark-sheets-batch-update.md +13 -58
  190. package/skills/lark-sheets/references/lark-sheets-chart.md +2 -1
  191. package/skills/lark-sheets/references/lark-sheets-conditional-format.md +1 -1
  192. package/skills/lark-sheets/references/lark-sheets-range-operations.md +5 -5
  193. package/skills/lark-sheets/references/lark-sheets-read-data.md +80 -6
  194. package/skills/lark-sheets/references/lark-sheets-sheet-structure.md +21 -10
  195. package/skills/lark-sheets/references/lark-sheets-styles-put.md +93 -0
  196. package/skills/lark-sheets/references/lark-sheets-visual-standards.md +2 -2
  197. package/skills/lark-sheets/references/lark-sheets-workbook.md +4 -3
  198. package/skills/lark-sheets/references/lark-sheets-write-cells.md +40 -12
  199. package/skills/lark-sheets/scripts/lark_detect_subtables.py +593 -0
  200. package/skills/lark-sheets/scripts/lark_inspect_workbook.py +188 -0
  201. package/skills/lark-sheets/scripts/lark_profile_table.py +614 -0
  202. package/skills/lark-sheets/scripts/lark_sheet_range.py +176 -0
  203. package/skills/lark-sheets/scripts/lark_sheet_read_cli.py +184 -0
  204. package/skills/lark-sheets/scripts/sheets_df.py +21 -3
  205. package/skills/lark-slides/SKILL.md +134 -104
  206. package/skills/lark-slides/references/asset-planning.md +6 -4
  207. package/skills/lark-slides/references/iconpark.md +2 -2
  208. package/skills/lark-slides/references/lark-slides-add-slide.md +92 -0
  209. package/skills/lark-slides/references/lark-slides-create.md +86 -66
  210. package/skills/lark-slides/references/lark-slides-delete-slide.md +65 -0
  211. package/skills/lark-slides/references/lark-slides-edit-workflows.md +6 -7
  212. package/skills/lark-slides/references/lark-slides-history.md +132 -0
  213. package/skills/lark-slides/references/lark-slides-media-upload.md +4 -27
  214. package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +7 -11
  215. package/skills/lark-slides/references/lark-slides-replace-slide.md +22 -4
  216. package/skills/lark-slides/references/lark-slides-screenshot.md +33 -15
  217. package/skills/lark-slides/references/lark-slides-update-slide.md +146 -0
  218. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +2 -2
  219. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +2 -3
  220. package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +90 -32
  221. package/skills/lark-slides/references/planning-layer.md +11 -10
  222. package/skills/lark-slides/references/slides_chart_demo.xml +1415 -1
  223. package/skills/lark-slides/references/slides_xml_schema_definition.xml +539 -79
  224. package/skills/lark-slides/references/troubleshooting.md +26 -9
  225. package/skills/lark-slides/references/validation-checklist.md +55 -18
  226. package/skills/lark-slides/references/visual-planning.md +25 -22
  227. package/skills/lark-slides/references/xml-schema-quick-ref.md +299 -51
  228. package/skills/lark-slides/scripts/sxsd_validator.py +1052 -0
  229. package/skills/lark-slides/scripts/xml_text_overlap_lint.py +1964 -195
  230. package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +4051 -501
  231. package/skills/lark-task/SKILL.md +7 -0
  232. package/skills/lark-task/references/lark-task-complete.md +6 -2
  233. package/skills/lark-task/references/lark-task-create.md +9 -0
  234. package/skills/lark-task/references/lark-task-update.md +6 -2
  235. package/skills/lark-whiteboard/SKILL.md +21 -13
  236. package/skills/lark-whiteboard/elements/layout.md +1 -1
  237. package/skills/lark-whiteboard/elements/schema.md +2 -2
  238. package/skills/lark-whiteboard/references/{lark-whiteboard-query.md → lark-whiteboard-export.md} +17 -16
  239. package/skills/lark-whiteboard/references/lark-whiteboard-update.md +7 -7
  240. package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +23 -31
  241. package/skills/lark-whiteboard/routes/dsl.md +11 -5
  242. package/skills/lark-whiteboard/routes/mermaid.md +3 -3
  243. package/skills/lark-whiteboard/routes/svg-edit.md +9 -6
  244. package/skills/lark-whiteboard/routes/svg.md +14 -7
  245. package/skills/lark-whiteboard/scenes/bar-chart.md +1 -1
  246. package/skills/lark-whiteboard/scenes/fishbone.md +1 -1
  247. package/skills/lark-whiteboard/scenes/flywheel.md +1 -1
  248. package/skills/lark-whiteboard/scenes/line-chart.md +1 -1
  249. package/skills/lark-whiteboard/scenes/mention.md +71 -0
  250. package/skills/lark-whiteboard/scenes/treemap.md +1 -1
  251. package/skills/lark-wiki/SKILL.md +6 -3
  252. package/skills/lark-wiki/references/lark-wiki-delete-space.md +6 -3
  253. package/skills/lark-doc/references/lark-doc-word-stat.md +0 -93
  254. package/skills/lark-doc/references/style/lark-doc-create-workflow.md +0 -47
  255. package/skills/lark-doc/references/style/lark-doc-style.md +0 -68
  256. package/skills/lark-doc/references/style/lark-doc-update-workflow.md +0 -48
  257. package/skills/lark-doc/scripts/doc_word_stat.py +0 -1243
  258. package/skills/lark-drive/references/lark-drive-comments-guide.md +0 -80
  259. package/skills/lark-slides/references/examples.md +0 -91
  260. package/skills/lark-slides/references/lark-slides-replace-pages.md +0 -95
  261. package/skills/lark-slides/references/lark-slides-whiteboard.md +0 -331
  262. package/skills/lark-slides/references/lark-slides-xml-get.md +0 -100
  263. package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +0 -125
  264. package/skills/lark-slides/references/slide-templates.md +0 -201
  265. package/skills/lark-slides/references/slides_demo.xml +0 -226
  266. package/skills/lark-slides/references/xml-format-guide.md +0 -433
@@ -0,0 +1,1052 @@
1
+ #!/usr/bin/env python3
2
+ # Copyright (c) 2026 Lark Technologies Pte. Ltd.
3
+ # SPDX-License-Identifier: MIT
4
+ """Internal XSD model and constraint validation for the Slides lint entrypoint."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+ import re
10
+ import xml.etree.ElementTree as ET
11
+ from dataclasses import dataclass
12
+ from decimal import Decimal, InvalidOperation
13
+ from functools import lru_cache
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+
18
+ XS_NS = "{http://www.w3.org/2001/XMLSchema}"
19
+ SML_NAMESPACE = "https://www.larkoffice.com/sml/2.0"
20
+ SML_LEGACY_HTTP_NAMESPACE = "http://www.larkoffice.com/sml/2.0"
21
+ SML_READBACK_NAMESPACE = "/sml/2.0"
22
+ ACCEPTED_SML_NAMESPACES = frozenset(
23
+ (SML_NAMESPACE, SML_LEGACY_HTTP_NAMESPACE, SML_READBACK_NAMESPACE)
24
+ )
25
+
26
+
27
+ def local_name(value: str) -> str:
28
+ if value.startswith("{"):
29
+ return value.rsplit("}", 1)[-1]
30
+ return value.rsplit(":", 1)[-1]
31
+
32
+
33
+ def direct_children(element: ET.Element, name: str) -> list[ET.Element]:
34
+ return [child for child in element if child.tag == f"{XS_NS}{name}"]
35
+
36
+
37
+ def first_direct_child(element: ET.Element, *names: str) -> ET.Element | None:
38
+ wanted = {f"{XS_NS}{name}" for name in names}
39
+ return next((child for child in element if child.tag in wanted), None)
40
+
41
+
42
+ def occurs_value(raw: str | None, default: int) -> int | None:
43
+ if raw == "unbounded":
44
+ return None
45
+ return int(raw) if raw is not None else default
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class SimpleTypeRule:
50
+ name: str
51
+ base: str | None = None
52
+ enums: tuple[str, ...] = ()
53
+ patterns: tuple[str, ...] = ()
54
+ bounds: tuple[tuple[str, Decimal], ...] = ()
55
+ length_bounds: tuple[tuple[str, int], ...] = ()
56
+ union_members: tuple[str, ...] = ()
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class AttributeRule:
61
+ name: str
62
+ type_name: str
63
+ required: bool
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class ElementRule:
68
+ name: str
69
+ type_name: str | None
70
+ inline_complex_type: ET.Element | None
71
+ ref_name: str | None
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class ChildRule:
76
+ element: ElementRule
77
+ min_occurs: int
78
+ max_occurs: int | None
79
+ order: int | None
80
+
81
+
82
+ @dataclass(frozen=True)
83
+ class WildcardRule:
84
+ namespace: str
85
+ process_contents: str
86
+ min_occurs: int
87
+ max_occurs: int | None
88
+ order: int | None
89
+
90
+
91
+ @dataclass(frozen=True)
92
+ class ChoiceRequirement:
93
+ names: tuple[str, ...]
94
+ min_occurs: int
95
+ max_occurs: int | None
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class SchemaModel:
100
+ simple_types: dict[str, SimpleTypeRule]
101
+ complex_types: dict[str, ET.Element]
102
+ element_candidates: dict[str, tuple[ElementRule, ...]]
103
+ global_elements: dict[str, ElementRule]
104
+
105
+
106
+ def parse_simple_type(
107
+ element: ET.Element,
108
+ fallback_name: str,
109
+ simple_types: dict[str, SimpleTypeRule] | None = None,
110
+ ) -> SimpleTypeRule:
111
+ name = element.attrib.get("name", fallback_name)
112
+ restriction = first_direct_child(element, "restriction")
113
+ union = first_direct_child(element, "union")
114
+ if union is not None:
115
+ union_members = [
116
+ local_name(member) for member in union.attrib.get("memberTypes", "").split()
117
+ ]
118
+ for index, inline_simple in enumerate(direct_children(union, "simpleType"), start=1):
119
+ inline_name = f"__inline_union_member_{name}_{index}"
120
+ union_members.append(inline_name)
121
+ if simple_types is not None:
122
+ simple_types[inline_name] = parse_simple_type(
123
+ inline_simple,
124
+ inline_name,
125
+ simple_types,
126
+ )
127
+ return SimpleTypeRule(
128
+ name=name,
129
+ union_members=tuple(union_members),
130
+ )
131
+ if restriction is None:
132
+ return SimpleTypeRule(name=name)
133
+
134
+ facet_names = {
135
+ "minInclusive",
136
+ "minExclusive",
137
+ "maxInclusive",
138
+ "maxExclusive",
139
+ }
140
+ bounds: list[tuple[str, Decimal]] = []
141
+ length_bounds: list[tuple[str, int]] = []
142
+ for child in restriction:
143
+ facet = local_name(child.tag)
144
+ if "value" not in child.attrib:
145
+ continue
146
+ if facet in facet_names:
147
+ bounds.append((facet, Decimal(child.attrib["value"])))
148
+ elif facet in {"minLength", "maxLength"}:
149
+ length_bounds.append((facet, int(child.attrib["value"])))
150
+ return SimpleTypeRule(
151
+ name=name,
152
+ base=local_name(restriction.attrib.get("base", "string")),
153
+ enums=tuple(child.attrib["value"] for child in direct_children(restriction, "enumeration")),
154
+ patterns=tuple(child.attrib["value"] for child in direct_children(restriction, "pattern")),
155
+ bounds=tuple(bounds),
156
+ length_bounds=tuple(length_bounds),
157
+ )
158
+
159
+
160
+ def parse_element_rule(element: ET.Element) -> ElementRule | None:
161
+ raw_ref = element.attrib.get("ref")
162
+ name = element.attrib.get("name")
163
+ if name is None and raw_ref:
164
+ name = local_name(raw_ref)
165
+ if not name:
166
+ return None
167
+ return ElementRule(
168
+ name=name,
169
+ type_name=local_name(element.attrib["type"]) if element.attrib.get("type") else None,
170
+ inline_complex_type=first_direct_child(element, "complexType"),
171
+ ref_name=local_name(raw_ref) if raw_ref else None,
172
+ )
173
+
174
+
175
+ @lru_cache(maxsize=4)
176
+ def load_schema_model(schema_path: str) -> SchemaModel:
177
+ root = ET.parse(schema_path).getroot()
178
+ simple_types: dict[str, SimpleTypeRule] = {}
179
+ for element in direct_children(root, "simpleType"):
180
+ name = element.attrib.get("name")
181
+ if not name:
182
+ continue
183
+ simple_types[name] = parse_simple_type(element, name, simple_types)
184
+ for attribute in root.iter(f"{XS_NS}attribute"):
185
+ inline_simple = first_direct_child(attribute, "simpleType")
186
+ if inline_simple is None:
187
+ continue
188
+ inline_name = f"__inline_attribute_{attribute.attrib.get('name', 'anonymous')}_{id(attribute)}"
189
+ simple_types[inline_name] = parse_simple_type(inline_simple, inline_name, simple_types)
190
+ complex_types = {
191
+ element.attrib["name"]: element
192
+ for element in direct_children(root, "complexType")
193
+ if element.attrib.get("name")
194
+ }
195
+ candidates: dict[str, list[ElementRule]] = {}
196
+ for element in root.iter(f"{XS_NS}element"):
197
+ rule = parse_element_rule(element)
198
+ if rule is not None:
199
+ candidates.setdefault(rule.name, []).append(rule)
200
+ global_elements = {
201
+ rule.name: rule
202
+ for element in direct_children(root, "element")
203
+ if (rule := parse_element_rule(element)) is not None
204
+ }
205
+ return SchemaModel(
206
+ simple_types=simple_types,
207
+ complex_types=complex_types,
208
+ element_candidates={name: tuple(rules) for name, rules in candidates.items()},
209
+ global_elements=global_elements,
210
+ )
211
+
212
+
213
+ def attributes_for_complex_type(
214
+ complex_type: ET.Element,
215
+ model: SchemaModel,
216
+ resolving: set[str] | None = None,
217
+ ) -> dict[str, AttributeRule]:
218
+ resolving = resolving or set()
219
+ attributes: dict[str, AttributeRule] = {}
220
+
221
+ for content_name in ("simpleContent", "complexContent"):
222
+ content = first_direct_child(complex_type, content_name)
223
+ if content is None:
224
+ continue
225
+ extension = first_direct_child(content, "extension")
226
+ if extension is None:
227
+ continue
228
+ base_name = local_name(extension.attrib.get("base", ""))
229
+ if base_name in model.complex_types and base_name not in resolving:
230
+ resolving.add(base_name)
231
+ attributes.update(attributes_for_complex_type(model.complex_types[base_name], model, resolving))
232
+ resolving.remove(base_name)
233
+ attributes.update(direct_attribute_rules(extension))
234
+
235
+ attributes.update(direct_attribute_rules(complex_type))
236
+ return attributes
237
+
238
+
239
+ def direct_attribute_rules(element: ET.Element) -> dict[str, AttributeRule]:
240
+ rules: dict[str, AttributeRule] = {}
241
+ for attribute in direct_children(element, "attribute"):
242
+ name = attribute.attrib.get("name")
243
+ if not name:
244
+ continue
245
+ type_name = local_name(attribute.attrib.get("type", "string"))
246
+ inline_simple = first_direct_child(attribute, "simpleType")
247
+ if inline_simple is not None:
248
+ type_name = f"__inline_attribute_{name}_{id(attribute)}"
249
+ rules[name] = AttributeRule(
250
+ name=name,
251
+ type_name=type_name,
252
+ required=attribute.attrib.get("use") == "required",
253
+ )
254
+ return rules
255
+
256
+
257
+ def attributes_for_element(rule: ElementRule, model: SchemaModel) -> dict[str, AttributeRule]:
258
+ complex_type = rule.inline_complex_type
259
+ if complex_type is None and rule.type_name in model.complex_types:
260
+ complex_type = model.complex_types[rule.type_name]
261
+ if complex_type is None:
262
+ return {}
263
+ return attributes_for_complex_type(complex_type, model)
264
+
265
+
266
+ def best_element_rule(element_name: str, model: SchemaModel) -> ElementRule | None:
267
+ candidates = model.element_candidates.get(element_name, ())
268
+ if not candidates:
269
+ return None
270
+ return max(
271
+ candidates,
272
+ key=lambda candidate: (
273
+ candidate.type_name is not None or candidate.inline_complex_type is not None,
274
+ len(attributes_for_element(candidate, model)),
275
+ ),
276
+ )
277
+
278
+
279
+ def concrete_element_rule(rule: ElementRule, model: SchemaModel) -> ElementRule:
280
+ if rule.ref_name is not None:
281
+ return model.global_elements.get(rule.ref_name, rule)
282
+ if rule.type_name is not None or rule.inline_complex_type is not None:
283
+ return rule
284
+ candidate = best_element_rule(rule.name, model)
285
+ return candidate or rule
286
+
287
+
288
+ def complex_type_for_element(rule: ElementRule, model: SchemaModel) -> ET.Element | None:
289
+ rule = concrete_element_rule(rule, model)
290
+ if rule.inline_complex_type is not None:
291
+ return rule.inline_complex_type
292
+ if rule.type_name is not None:
293
+ return model.complex_types.get(rule.type_name)
294
+ return None
295
+
296
+
297
+ def particle_for_complex_type(complex_type: ET.Element) -> ET.Element | None:
298
+ particle = first_direct_child(complex_type, "sequence", "all", "choice")
299
+ if particle is not None:
300
+ return particle
301
+ for content_name in ("simpleContent", "complexContent"):
302
+ content = first_direct_child(complex_type, content_name)
303
+ if content is None:
304
+ continue
305
+ extension = first_direct_child(content, "extension")
306
+ if extension is not None:
307
+ return first_direct_child(extension, "sequence", "all", "choice")
308
+ return None
309
+
310
+
311
+ def multiplied_max(left: int | None, right: int | None) -> int | None:
312
+ if left is None or right is None:
313
+ return None
314
+ return left * right
315
+
316
+
317
+ def child_rules_for_complex_type(
318
+ complex_type: ET.Element,
319
+ ) -> tuple[list[ChildRule], list[WildcardRule], list[ChoiceRequirement]]:
320
+ particle = particle_for_complex_type(complex_type)
321
+ if particle is None:
322
+ return [], [], []
323
+
324
+ rules: list[ChildRule] = []
325
+ wildcard_rules: list[WildcardRule] = []
326
+ requirements: list[ChoiceRequirement] = []
327
+ next_order = 0
328
+
329
+ def add_element(
330
+ element: ET.Element,
331
+ *,
332
+ order: int | None,
333
+ optional_by_choice: bool,
334
+ max_multiplier: int | None,
335
+ ) -> None:
336
+ element_rule = parse_element_rule(element)
337
+ if element_rule is None:
338
+ return
339
+ minimum = occurs_value(element.attrib.get("minOccurs"), 1) or 0
340
+ maximum = occurs_value(element.attrib.get("maxOccurs"), 1)
341
+ rules.append(
342
+ ChildRule(
343
+ element=element_rule,
344
+ min_occurs=0 if optional_by_choice else minimum,
345
+ max_occurs=multiplied_max(maximum, max_multiplier),
346
+ order=order,
347
+ )
348
+ )
349
+
350
+ def add_wildcard(
351
+ wildcard: ET.Element,
352
+ *,
353
+ order: int | None,
354
+ optional_by_choice: bool,
355
+ max_multiplier: int | None,
356
+ ) -> None:
357
+ minimum = occurs_value(wildcard.attrib.get("minOccurs"), 1) or 0
358
+ maximum = occurs_value(wildcard.attrib.get("maxOccurs"), 1)
359
+ wildcard_rules.append(
360
+ WildcardRule(
361
+ namespace=wildcard.attrib.get("namespace", "##any"),
362
+ process_contents=wildcard.attrib.get("processContents", "strict"),
363
+ min_occurs=0 if optional_by_choice else minimum,
364
+ max_occurs=multiplied_max(maximum, max_multiplier),
365
+ order=order,
366
+ )
367
+ )
368
+
369
+ def walk_group(
370
+ group: ET.Element,
371
+ *,
372
+ ordered: bool,
373
+ fixed_order: int | None = None,
374
+ optional_by_choice: bool = False,
375
+ max_multiplier: int | None = 1,
376
+ ) -> None:
377
+ nonlocal next_order
378
+ kind = local_name(group.tag)
379
+ group_min = occurs_value(group.attrib.get("minOccurs"), 1) or 0
380
+ group_max = occurs_value(group.attrib.get("maxOccurs"), 1)
381
+ effective_max = multiplied_max(max_multiplier, group_max)
382
+
383
+ if kind == "choice":
384
+ choice_order = fixed_order
385
+ if choice_order is None and ordered:
386
+ choice_order = next_order
387
+ next_order += 1
388
+ names: list[str] = []
389
+ for child in group:
390
+ child_kind = local_name(child.tag)
391
+ if child_kind == "element":
392
+ parsed = parse_element_rule(child)
393
+ if parsed is not None:
394
+ names.append(parsed.name)
395
+ add_element(
396
+ child,
397
+ order=choice_order,
398
+ optional_by_choice=True,
399
+ max_multiplier=effective_max,
400
+ )
401
+ elif child_kind == "any":
402
+ add_wildcard(
403
+ child,
404
+ order=choice_order,
405
+ optional_by_choice=True,
406
+ max_multiplier=effective_max,
407
+ )
408
+ elif child_kind in {"sequence", "all", "choice"}:
409
+ walk_group(
410
+ child,
411
+ ordered=ordered,
412
+ fixed_order=choice_order,
413
+ optional_by_choice=True,
414
+ max_multiplier=effective_max,
415
+ )
416
+ if names and (group_min > 0 or effective_max is not None):
417
+ requirements.append(ChoiceRequirement(tuple(names), group_min, effective_max))
418
+ return
419
+
420
+ group_ordered = kind == "sequence"
421
+ for child in group:
422
+ child_kind = local_name(child.tag)
423
+ if child_kind == "element":
424
+ child_order = fixed_order
425
+ if child_order is None and ordered and group_ordered:
426
+ child_order = next_order
427
+ next_order += 1
428
+ add_element(
429
+ child,
430
+ order=child_order,
431
+ optional_by_choice=optional_by_choice or group_min == 0,
432
+ max_multiplier=effective_max,
433
+ )
434
+ elif child_kind == "any":
435
+ child_order = fixed_order
436
+ if child_order is None and ordered and group_ordered:
437
+ child_order = next_order
438
+ next_order += 1
439
+ add_wildcard(
440
+ child,
441
+ order=child_order,
442
+ optional_by_choice=optional_by_choice or group_min == 0,
443
+ max_multiplier=effective_max,
444
+ )
445
+ elif child_kind in {"sequence", "all", "choice"}:
446
+ walk_group(
447
+ child,
448
+ ordered=ordered and group_ordered,
449
+ fixed_order=fixed_order,
450
+ optional_by_choice=optional_by_choice or group_min == 0,
451
+ max_multiplier=effective_max,
452
+ )
453
+
454
+ walk_group(particle, ordered=local_name(particle.tag) == "sequence")
455
+ return rules, wildcard_rules, requirements
456
+
457
+
458
+ def issue(
459
+ code: str,
460
+ path: str,
461
+ tag: str,
462
+ *,
463
+ attr: str | None,
464
+ expected: str,
465
+ actual: Any,
466
+ message: str,
467
+ hint: str,
468
+ ) -> dict[str, Any]:
469
+ result: dict[str, Any] = {
470
+ "level": "error",
471
+ "code": code,
472
+ "path": path,
473
+ "tag": tag,
474
+ "expected": expected,
475
+ "actual": actual,
476
+ "message": message,
477
+ "hint": hint,
478
+ }
479
+ if attr is not None:
480
+ result["attr"] = attr
481
+ return result
482
+
483
+
484
+ def builtin_scalar_value(type_name: str, value: str) -> Decimal | str | bool:
485
+ if type_name in {"string", "anyURI"}:
486
+ return value
487
+ if type_name == "boolean":
488
+ if value not in {"true", "false", "1", "0"}:
489
+ raise ValueError("expected boolean")
490
+ return value in {"true", "1"}
491
+ if type_name in {"integer", "positiveInteger", "nonNegativeInteger"}:
492
+ if re.fullmatch(r"[+-]?\d+", value) is None:
493
+ raise ValueError("expected integer")
494
+ number = Decimal(value)
495
+ if type_name == "positiveInteger" and number <= 0:
496
+ raise ArithmeticError("expected positive integer")
497
+ if type_name == "nonNegativeInteger" and number < 0:
498
+ raise ArithmeticError("expected non-negative integer")
499
+ return number
500
+ if type_name in {"double", "decimal"}:
501
+ lexical_value = value.strip(" \t\n\r")
502
+ decimal_pattern = r"[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)"
503
+ double_pattern = decimal_pattern + r"(?:[eE][+-]?[0-9]+)?"
504
+ expected_pattern = double_pattern if type_name == "double" else decimal_pattern
505
+ if re.fullmatch(expected_pattern, lexical_value) is None:
506
+ raise ValueError(f"expected {type_name}")
507
+ try:
508
+ number = Decimal(lexical_value)
509
+ except InvalidOperation as error:
510
+ raise ValueError(f"expected {type_name}") from error
511
+ if not math.isfinite(float(number)):
512
+ raise ValueError(f"expected finite {type_name}")
513
+ return number
514
+ return value
515
+
516
+
517
+ def scalar_value_for_type(
518
+ type_name: str,
519
+ value: str,
520
+ model: SchemaModel,
521
+ resolving: set[str] | None = None,
522
+ ) -> Decimal | str | bool:
523
+ resolving = resolving or set()
524
+ if type_name in resolving:
525
+ return value
526
+ rule = model.simple_types.get(type_name)
527
+ if rule is None or rule.base is None:
528
+ return builtin_scalar_value(type_name, value)
529
+ resolving.add(type_name)
530
+ try:
531
+ return scalar_value_for_type(rule.base, value, model, resolving)
532
+ finally:
533
+ resolving.remove(type_name)
534
+
535
+
536
+ @lru_cache(maxsize=32)
537
+ def python_pattern_for_xsd(pattern: str) -> str:
538
+ translated: list[str] = []
539
+ in_character_class = False
540
+ index = 0
541
+ while index < len(pattern):
542
+ char = pattern[index]
543
+ if char == "\\" and index + 1 < len(pattern):
544
+ escaped = pattern[index + 1]
545
+ if escaped in {"s", "S"}:
546
+ body = r" \t\n\r"
547
+ if in_character_class:
548
+ if escaped == "S":
549
+ raise ValueError(
550
+ f"unsupported complemented XSD character class \\{escaped} inside []"
551
+ )
552
+ translated.append(body)
553
+ else:
554
+ prefix = "^" if escaped == "S" else ""
555
+ translated.append(f"[{prefix}{body}]")
556
+ index += 2
557
+ continue
558
+ translated.extend((char, escaped))
559
+ index += 2
560
+ continue
561
+ if char == "[":
562
+ in_character_class = True
563
+ elif char == "]":
564
+ in_character_class = False
565
+ elif char == "." and not in_character_class:
566
+ translated.append(r"[^\n\r]")
567
+ index += 1
568
+ continue
569
+ elif char in "^$" and not in_character_class:
570
+ translated.append(f"\\{char}")
571
+ index += 1
572
+ continue
573
+ translated.append(char)
574
+ index += 1
575
+ return "".join(translated)
576
+
577
+
578
+ def xsd_pattern_matches(pattern: str, value: str) -> bool:
579
+ if pattern == r"[\w.-]+[.:]\S*":
580
+ if any(character in " \t\n\r" for character in value):
581
+ return False
582
+ for index, character in enumerate(value):
583
+ if index > 0 and character in ".:":
584
+ return True
585
+ if not (character == "_" or character.isalnum() or character in ".-"):
586
+ return False
587
+ return False
588
+ return re.fullmatch(python_pattern_for_xsd(pattern), value) is not None
589
+
590
+
591
+ def value_error_for_type(
592
+ type_name: str,
593
+ value: str,
594
+ model: SchemaModel,
595
+ resolving: set[str] | None = None,
596
+ ) -> tuple[str, str] | None:
597
+ resolving = resolving or set()
598
+ if type_name in resolving:
599
+ return None
600
+ rule = model.simple_types.get(type_name)
601
+ if rule is None:
602
+ try:
603
+ builtin_scalar_value(type_name, value)
604
+ except ValueError:
605
+ return "sxsd_invalid_scalar", f"value valid for {type_name}"
606
+ except ArithmeticError:
607
+ return "sxsd_value_out_of_range", f"value in the range allowed by {type_name}"
608
+ return None
609
+
610
+ resolving.add(type_name)
611
+ try:
612
+ if rule.union_members:
613
+ member_errors = [value_error_for_type(member, value, model, resolving) for member in rule.union_members]
614
+ if any(error is None for error in member_errors):
615
+ return None
616
+ unsupported_error = next(
617
+ (error for error in member_errors if error and error[0] == "sxsd_unsupported_pattern"),
618
+ None,
619
+ )
620
+ if unsupported_error is not None:
621
+ return unsupported_error
622
+ if any(error and error[0] == "sxsd_pattern_mismatch" for error in member_errors):
623
+ return "sxsd_pattern_mismatch", f"value matching one member of {type_name}"
624
+ return member_errors[0]
625
+
626
+ if rule.enums and value not in rule.enums:
627
+ return "sxsd_invalid_enum", "one of: " + ", ".join(rule.enums)
628
+
629
+ if rule.patterns:
630
+ unsupported_patterns: list[str] = []
631
+ for pattern in rule.patterns:
632
+ try:
633
+ if xsd_pattern_matches(pattern, value):
634
+ break
635
+ except (ValueError, re.error) as error:
636
+ unsupported_patterns.append(f"{pattern!r}: {error}")
637
+ else:
638
+ if unsupported_patterns:
639
+ return (
640
+ "sxsd_unsupported_pattern",
641
+ "lint support for XSD pattern " + "; ".join(unsupported_patterns),
642
+ )
643
+ return "sxsd_pattern_mismatch", "value matching pattern " + " or ".join(rule.patterns)
644
+
645
+ base_name = rule.base or "string"
646
+ base_error = value_error_for_type(base_name, value, model, resolving)
647
+ if base_error is not None:
648
+ return base_error
649
+ for facet, bound in rule.length_bounds:
650
+ allowed = len(value) >= bound if facet == "minLength" else len(value) <= bound
651
+ if not allowed:
652
+ return "sxsd_value_out_of_range", f"{facet} {bound}"
653
+ scalar = scalar_value_for_type(base_name, value, model)
654
+ if isinstance(scalar, Decimal):
655
+ for facet, bound in rule.bounds:
656
+ allowed = {
657
+ "minInclusive": scalar >= bound,
658
+ "minExclusive": scalar > bound,
659
+ "maxInclusive": scalar <= bound,
660
+ "maxExclusive": scalar < bound,
661
+ }[facet]
662
+ if not allowed:
663
+ return "sxsd_value_out_of_range", f"{facet} {bound}"
664
+ return None
665
+ finally:
666
+ resolving.remove(type_name)
667
+
668
+
669
+ def validate_element_attributes(
670
+ element: ET.Element,
671
+ path: str,
672
+ model: SchemaModel,
673
+ element_rule: ElementRule | None = None,
674
+ ) -> list[dict[str, Any]]:
675
+ tag = local_name(element.tag)
676
+ element_rule = element_rule or best_element_rule(tag, model)
677
+ if element_rule is None:
678
+ return []
679
+ attribute_rules = attributes_for_element(element_rule, model)
680
+ issues: list[dict[str, Any]] = []
681
+ for attr_rule in attribute_rules.values():
682
+ if attr_rule.required and attr_rule.name not in element.attrib:
683
+ issues.append(
684
+ issue(
685
+ "sxsd_missing_required_attr",
686
+ path,
687
+ tag,
688
+ attr=attr_rule.name,
689
+ expected=f"required attribute of type {attr_rule.type_name}",
690
+ actual=None,
691
+ message=f'missing required SXSD attribute "{attr_rule.name}" on <{tag}> at {path}',
692
+ hint=f'Add attribute "{attr_rule.name}" with a value valid for {attr_rule.type_name}.',
693
+ )
694
+ )
695
+ for raw_name, value in element.attrib.items():
696
+ attr_name = local_name(raw_name)
697
+ attr_rule = attribute_rules.get(attr_name)
698
+ if attr_rule is None:
699
+ continue
700
+ validation_error = value_error_for_type(attr_rule.type_name, value, model)
701
+ if validation_error is None:
702
+ continue
703
+ code, expected = validation_error
704
+ if code == "sxsd_unsupported_pattern":
705
+ message = (
706
+ f'unsupported SXSD pattern for attribute "{attr_name}" on <{tag}> at {path}'
707
+ )
708
+ hint = (
709
+ f"Extend the SXSD pattern interpreter for {attr_rule.type_name}; "
710
+ "do not treat this attribute value as validated."
711
+ )
712
+ else:
713
+ message = (
714
+ f'invalid SXSD value {value!r} for attribute "{attr_name}" on <{tag}> at {path}'
715
+ )
716
+ hint = f'Set attribute "{attr_name}" to a value valid for {attr_rule.type_name}.'
717
+ issues.append(
718
+ issue(
719
+ code,
720
+ path,
721
+ tag,
722
+ attr=attr_name,
723
+ expected=expected,
724
+ actual=value,
725
+ message=message,
726
+ hint=hint,
727
+ )
728
+ )
729
+ return issues
730
+
731
+
732
+ def element_namespace(tag: str) -> str | None:
733
+ if not tag.startswith("{"):
734
+ return None
735
+ return tag[1:].split("}", 1)[0]
736
+
737
+
738
+ def wildcard_matches_namespace(namespace_rule: str, namespace: str | None) -> bool:
739
+ tokens = namespace_rule.split()
740
+ if "##any" in tokens:
741
+ return True
742
+ if "##local" in tokens and namespace is None:
743
+ return True
744
+ if "##targetNamespace" in tokens and namespace == SML_NAMESPACE:
745
+ return True
746
+ if "##other" in tokens and namespace not in {None, SML_NAMESPACE}:
747
+ return True
748
+ return namespace in tokens
749
+
750
+
751
+ def wildcard_namespace_description(namespace_rule: str) -> str:
752
+ if namespace_rule == "##any":
753
+ return "any namespace"
754
+ if namespace_rule == "##local":
755
+ return "the local namespace"
756
+ if namespace_rule == "##targetNamespace":
757
+ return f"the target namespace {SML_NAMESPACE}"
758
+ if namespace_rule == "##other":
759
+ return "a namespace other than the SXSD target namespace"
760
+ return f"namespace {namespace_rule}"
761
+
762
+
763
+ def validate_element_children(
764
+ element: ET.Element,
765
+ path: str,
766
+ element_rule: ElementRule,
767
+ model: SchemaModel,
768
+ ) -> tuple[list[dict[str, Any]], dict[int, ElementRule]]:
769
+ tag = local_name(element.tag)
770
+ complex_type = complex_type_for_element(element_rule, model)
771
+ child_rules, wildcard_rules, choice_requirements = (
772
+ child_rules_for_complex_type(complex_type)
773
+ if complex_type is not None
774
+ else ([], [], [])
775
+ )
776
+ rules_by_name: dict[str, list[ChildRule]] = {}
777
+ for child_rule in child_rules:
778
+ rules_by_name.setdefault(child_rule.element.name, []).append(child_rule)
779
+
780
+ issues: list[dict[str, Any]] = []
781
+ matched: dict[int, ElementRule] = {}
782
+ counts: dict[str, int] = {}
783
+ wildcard_counts: dict[int, int] = {}
784
+ latest_order = -1
785
+ for child in element:
786
+ child_name = local_name(child.tag)
787
+ child_path = f"{path}/{child_name}"
788
+ candidates = rules_by_name.get(child_name, [])
789
+ wildcard_match = next(
790
+ (
791
+ (index, wildcard_rule)
792
+ for index, wildcard_rule in enumerate(wildcard_rules)
793
+ if wildcard_matches_namespace(
794
+ wildcard_rule.namespace,
795
+ element_namespace(child.tag),
796
+ )
797
+ ),
798
+ None,
799
+ )
800
+ if not candidates and wildcard_match is None:
801
+ expected_children = sorted(rules_by_name)
802
+ expected_children.extend(
803
+ wildcard_namespace_description(rule.namespace) for rule in wildcard_rules
804
+ )
805
+ issues.append(
806
+ issue(
807
+ "sxsd_unexpected_child",
808
+ child_path,
809
+ child_name,
810
+ attr=None,
811
+ expected="one of: " + ", ".join(expected_children)
812
+ if expected_children
813
+ else "no child elements",
814
+ actual=child_name,
815
+ message=f"unexpected SXSD child <{child_name}> under <{tag}> at {child_path}",
816
+ hint=f"Move or remove <{child_name}> so <{tag}> follows the SXSD child structure.",
817
+ )
818
+ )
819
+ continue
820
+
821
+ if not candidates and wildcard_match is not None:
822
+ wildcard_index, wildcard_rule = wildcard_match
823
+ if wildcard_rule.order is not None:
824
+ if wildcard_rule.order < latest_order:
825
+ issues.append(
826
+ issue(
827
+ "sxsd_invalid_child_order",
828
+ child_path,
829
+ child_name,
830
+ attr=None,
831
+ expected="children in xs:sequence order",
832
+ actual=child_name,
833
+ message=f"SXSD child <{child_name}> is out of order under <{tag}> at {child_path}",
834
+ hint=f"Reorder <{child_name}> according to the SXSD sequence for <{tag}>.",
835
+ )
836
+ )
837
+ latest_order = max(latest_order, wildcard_rule.order)
838
+ wildcard_counts[wildcard_index] = wildcard_counts.get(wildcard_index, 0) + 1
839
+ actual_count = wildcard_counts[wildcard_index]
840
+ if wildcard_rule.max_occurs is not None and actual_count > wildcard_rule.max_occurs:
841
+ issues.append(
842
+ issue(
843
+ "sxsd_too_many_children",
844
+ child_path,
845
+ child_name,
846
+ attr=None,
847
+ expected=f"at most {wildcard_rule.max_occurs} child from {wildcard_namespace_description(wildcard_rule.namespace)}",
848
+ actual=actual_count,
849
+ message=f"too many wildcard SXSD children under <{tag}> at {path}",
850
+ hint=f"Keep at most {wildcard_rule.max_occurs} child from {wildcard_namespace_description(wildcard_rule.namespace)} under <{tag}>.",
851
+ )
852
+ )
853
+ continue
854
+
855
+ child_rule = candidates[0]
856
+ if child_rule.order is not None:
857
+ if child_rule.order < latest_order:
858
+ issues.append(
859
+ issue(
860
+ "sxsd_invalid_child_order",
861
+ child_path,
862
+ child_name,
863
+ attr=None,
864
+ expected="children in xs:sequence order",
865
+ actual=child_name,
866
+ message=f"SXSD child <{child_name}> is out of order under <{tag}> at {child_path}",
867
+ hint=f"Reorder <{child_name}> according to the SXSD sequence for <{tag}>.",
868
+ )
869
+ )
870
+ latest_order = max(latest_order, child_rule.order)
871
+
872
+ counts[child_name] = counts.get(child_name, 0) + 1
873
+ if child_rule.max_occurs is not None and counts[child_name] > child_rule.max_occurs:
874
+ issues.append(
875
+ issue(
876
+ "sxsd_too_many_children",
877
+ child_path,
878
+ child_name,
879
+ attr=None,
880
+ expected=f"at most {child_rule.max_occurs}",
881
+ actual=counts[child_name],
882
+ message=f"too many SXSD <{child_name}> children under <{tag}> at {path}",
883
+ hint=f"Keep at most {child_rule.max_occurs} <{child_name}> children under <{tag}>.",
884
+ )
885
+ )
886
+ matched[id(child)] = concrete_element_rule(child_rule.element, model)
887
+
888
+ for child_rule in child_rules:
889
+ child_name = child_rule.element.name
890
+ actual_count = counts.get(child_name, 0)
891
+ if child_rule.min_occurs <= actual_count:
892
+ continue
893
+ issues.append(
894
+ issue(
895
+ "sxsd_missing_required_child",
896
+ path,
897
+ tag,
898
+ attr=None,
899
+ expected=f"{child_name} (at least {child_rule.min_occurs})",
900
+ actual=actual_count,
901
+ message=f"missing required SXSD child <{child_name}> under <{tag}> at {path}",
902
+ hint=f"Add at least {child_rule.min_occurs} <{child_name}> child under <{tag}>.",
903
+ )
904
+ )
905
+
906
+ for wildcard_index, wildcard_rule in enumerate(wildcard_rules):
907
+ actual_count = wildcard_counts.get(wildcard_index, 0)
908
+ if wildcard_rule.min_occurs <= actual_count:
909
+ continue
910
+ namespace_description = wildcard_namespace_description(wildcard_rule.namespace)
911
+ issues.append(
912
+ issue(
913
+ "sxsd_missing_required_child",
914
+ path,
915
+ tag,
916
+ attr=None,
917
+ expected=f"at least {wildcard_rule.min_occurs} child from {namespace_description}",
918
+ actual=actual_count,
919
+ message=f"missing required wildcard SXSD child under <{tag}> at {path}",
920
+ hint=f"Add at least {wildcard_rule.min_occurs} child from {namespace_description} under <{tag}>.",
921
+ )
922
+ )
923
+
924
+ for requirement in choice_requirements:
925
+ actual_count = sum(counts.get(name, 0) for name in requirement.names)
926
+ expected_names = ", ".join(requirement.names)
927
+ if actual_count < requirement.min_occurs:
928
+ issues.append(
929
+ issue(
930
+ "sxsd_missing_required_child",
931
+ path,
932
+ tag,
933
+ attr=None,
934
+ expected=f"one of: {expected_names} (at least {requirement.min_occurs})",
935
+ actual=actual_count,
936
+ message=f"missing required SXSD choice child under <{tag}> at {path}",
937
+ hint=f"Add at least {requirement.min_occurs} child from: {expected_names}.",
938
+ )
939
+ )
940
+ if requirement.max_occurs is not None and actual_count > requirement.max_occurs:
941
+ issues.append(
942
+ issue(
943
+ "sxsd_too_many_children",
944
+ path,
945
+ tag,
946
+ attr=None,
947
+ expected=f"at most {requirement.max_occurs} child from: {expected_names}",
948
+ actual=actual_count,
949
+ message=f"too many SXSD choice children under <{tag}> at {path}",
950
+ hint=f"Keep at most {requirement.max_occurs} child from: {expected_names}.",
951
+ )
952
+ )
953
+ return issues, matched
954
+
955
+
956
+ def validate_sxsd(root: ET.Element, schema_path: Path) -> list[dict[str, Any]]:
957
+ model = load_schema_model(str(schema_path.resolve()))
958
+ issues: list[dict[str, Any]] = []
959
+ root_name = local_name(root.tag)
960
+ document_namespace = element_namespace(root.tag)
961
+ is_bare_slide_fragment = root_name == "slide" and document_namespace is None
962
+ has_valid_document_namespace = (
963
+ document_namespace in ACCEPTED_SML_NAMESPACES or is_bare_slide_fragment
964
+ )
965
+
966
+ def visit(element: ET.Element, parent_path: str, element_rule: ElementRule) -> None:
967
+ tag = local_name(element.tag)
968
+ path = f"{parent_path}/{tag}" if parent_path else tag
969
+ namespace = element_namespace(element.tag)
970
+ invalid_root_namespace = (
971
+ not parent_path
972
+ and namespace not in ACCEPTED_SML_NAMESPACES
973
+ and not is_bare_slide_fragment
974
+ )
975
+ invalid_descendant_namespace = (
976
+ bool(parent_path)
977
+ and has_valid_document_namespace
978
+ and namespace != document_namespace
979
+ )
980
+ if invalid_root_namespace or invalid_descendant_namespace:
981
+ expected_namespace = document_namespace if parent_path else SML_NAMESPACE
982
+ namespace_hint = (
983
+ "Keep SXSD descendants without xmlns in a bare <slide> readback fragment."
984
+ if expected_namespace is None
985
+ else f'Use xmlns="{expected_namespace}" for SXSD elements.'
986
+ )
987
+ issues.append(
988
+ issue(
989
+ "sxsd_invalid_namespace",
990
+ path,
991
+ tag,
992
+ attr=None,
993
+ expected=expected_namespace,
994
+ actual=namespace,
995
+ message=f"invalid SXSD namespace on <{tag}> at {path}",
996
+ hint=namespace_hint,
997
+ )
998
+ )
999
+ issues.extend(validate_element_attributes(element, path, model, element_rule))
1000
+ child_issues, matched = validate_element_children(element, path, element_rule, model)
1001
+ issues.extend(child_issues)
1002
+ for child in element:
1003
+ child_rule = matched.get(id(child))
1004
+ if child_rule is not None:
1005
+ visit(child, path, child_rule)
1006
+
1007
+ if root_name not in {"presentation", "slide"}:
1008
+ issues.append(
1009
+ issue(
1010
+ "sxsd_unexpected_root",
1011
+ root_name,
1012
+ root_name,
1013
+ attr=None,
1014
+ expected="presentation or slide",
1015
+ actual=root_name,
1016
+ message=f"unsupported SXSD root <{root_name}>",
1017
+ hint="Use a <presentation> or <slide> root.",
1018
+ )
1019
+ )
1020
+ return issues
1021
+ if root_name == "presentation":
1022
+ root_rule = model.global_elements.get("presentation")
1023
+ elif "SlideType" in model.complex_types:
1024
+ root_rule = ElementRule("slide", "SlideType", None, None)
1025
+ else:
1026
+ root_rule = None
1027
+ if root_rule is None:
1028
+ issues.append(
1029
+ issue(
1030
+ "sxsd_unexpected_root",
1031
+ root_name,
1032
+ root_name,
1033
+ attr=None,
1034
+ expected="presentation or slide",
1035
+ actual=root_name,
1036
+ message=f"unsupported SXSD root <{root_name}>",
1037
+ hint="Use a <presentation> or <slide> root.",
1038
+ )
1039
+ )
1040
+ return issues
1041
+ visit(root, "", root_rule)
1042
+ return issues
1043
+
1044
+
1045
+ def load_tag_attributes(schema_path: Path) -> dict[str, set[str]]:
1046
+ model = load_schema_model(str(schema_path.resolve()))
1047
+ tag_attributes: dict[str, set[str]] = {}
1048
+ for tag_name, candidates in model.element_candidates.items():
1049
+ attrs = tag_attributes.setdefault(tag_name, set())
1050
+ for candidate in candidates:
1051
+ attrs.update(attributes_for_element(concrete_element_rule(candidate, model), model))
1052
+ return tag_attributes