@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.
- package/README.md +5 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/skills/lark-apps/SKILL.md +59 -14
- package/skills/lark-apps/creative-design/agents/assets/vision-probe.png +0 -0
- package/skills/lark-apps/creative-design/agents/fork-verifier-agent.md +71 -0
- package/skills/lark-apps/creative-design/agents/vision-probe-agent.md +41 -0
- package/skills/lark-apps/creative-design/assets/index.html +27 -0
- package/skills/lark-apps/creative-design/creative-design.md +239 -0
- package/skills/lark-apps/creative-design/references/aily.md +39 -0
- package/skills/lark-apps/creative-design/references/animated-video.md +34 -0
- package/skills/lark-apps/creative-design/references/charts.md +165 -0
- package/skills/lark-apps/creative-design/references/claude.md +36 -0
- package/skills/lark-apps/creative-design/references/codex.md +32 -0
- package/skills/lark-apps/creative-design/references/data-report.md +108 -0
- package/skills/lark-apps/creative-design/references/frontend-design.md +71 -0
- package/skills/lark-apps/creative-design/references/hi-fi-design.md +32 -0
- package/skills/lark-apps/creative-design/references/interactive-prototype.md +24 -0
- package/skills/lark-apps/creative-design/references/make-a-deck.md +133 -0
- package/skills/lark-apps/creative-design/references/visual-exposure.md +82 -0
- package/skills/lark-apps/creative-design/references/wireframe.md +14 -0
- package/skills/lark-apps/creative-design/starter-components/android-frame.jsx +188 -0
- package/skills/lark-apps/creative-design/starter-components/animations.jsx +773 -0
- package/skills/lark-apps/creative-design/starter-components/browser-window.jsx +122 -0
- package/skills/lark-apps/creative-design/starter-components/deck-stage.js +2483 -0
- package/skills/lark-apps/creative-design/starter-components/design-canvas.jsx +1432 -0
- package/skills/lark-apps/creative-design/starter-components/ios-frame.jsx +270 -0
- package/skills/lark-apps/creative-design/starter-components/macos-window.jsx +197 -0
- package/skills/lark-apps/creative-design/starter-components/tweaks-panel.jsx +752 -0
- package/skills/lark-apps/references/lark-apps-automation.md +80 -2
- package/skills/lark-apps/references/lark-apps-cache.md +61 -0
- package/skills/lark-apps/references/lark-apps-cloud-dev.md +5 -5
- package/skills/lark-apps/references/lark-apps-create.md +6 -4
- package/skills/lark-apps/references/lark-apps-db.md +1 -1
- package/skills/lark-apps/references/lark-apps-env-pull.md +1 -1
- package/skills/lark-apps/references/lark-apps-file.md +2 -2
- package/skills/lark-apps/references/lark-apps-get.md +1 -1
- package/skills/lark-apps/references/lark-apps-git-credential.md +1 -1
- package/skills/lark-apps/references/lark-apps-html-publish.md +4 -8
- package/skills/lark-apps/references/lark-apps-init.md +1 -1
- package/skills/lark-apps/references/lark-apps-list.md +2 -2
- package/skills/lark-apps/references/lark-apps-local-dev.md +80 -11
- package/skills/lark-apps/references/lark-apps-openapi-key.md +1 -1
- package/skills/lark-apps/references/lark-apps-release-create.md +2 -2
- package/skills/lark-apps/references/lark-apps-release-get.md +3 -3
- package/skills/lark-base/SKILL.md +34 -19
- package/skills/lark-base/references/lark-base-cell-value.md +3 -3
- package/skills/lark-base/references/lark-base-dashboard-block-get-data.md +17 -1
- package/skills/lark-base/references/lark-base-dashboard.md +17 -4
- package/skills/lark-base/references/lark-base-data-query-guide.md +8 -0
- package/skills/lark-base/references/lark-base-data-query.md +11 -4
- package/skills/lark-base/references/lark-base-field-create.md +21 -6
- package/skills/lark-base/references/lark-base-field-json.md +9 -6
- package/skills/lark-base/references/lark-base-field-update.md +17 -1
- package/skills/lark-base/references/lark-base-filter-condition.md +179 -0
- package/skills/lark-base/references/lark-base-form-questions-create.md +40 -7
- package/skills/lark-base/references/lark-base-form-questions-update.md +73 -20
- package/skills/lark-base/references/lark-base-form-submit.md +16 -7
- package/skills/lark-base/references/lark-base-record-batch-create.md +12 -10
- package/skills/lark-base/references/lark-base-record-batch-update.md +11 -9
- package/skills/lark-base/references/lark-base-record-upsert.md +1 -1
- package/skills/lark-base/references/lark-base-role-guide.md +11 -0
- package/skills/lark-base/references/lark-base-view-set-filter.md +11 -137
- package/skills/lark-base/references/role-config.md +31 -5
- package/skills/lark-calendar/SKILL.md +14 -8
- package/skills/lark-calendar/references/lark-calendar-create.md +6 -5
- package/skills/lark-calendar/references/lark-calendar-recurring.md +1 -0
- package/skills/lark-calendar/references/lark-calendar-room-find.md +2 -1
- package/skills/lark-calendar/references/lark-calendar-schedule-clear-time.md +1 -0
- package/skills/lark-calendar/references/lark-calendar-suggestion.md +1 -1
- package/skills/lark-calendar/references/lark-calendar-update.md +10 -4
- package/skills/lark-contact/SKILL.md +19 -3
- package/skills/lark-contact/references/lark-contact-search-bot.md +60 -0
- package/skills/lark-doc/SKILL.md +26 -61
- package/skills/lark-doc/references/genres/business-analysis.md +30 -0
- package/skills/lark-doc/references/genres/data-report.md +32 -0
- package/skills/lark-doc/references/genres/email.md +38 -0
- package/skills/lark-doc/references/genres/execution-plan.md +27 -0
- package/skills/lark-doc/references/genres/formal-doc.md +37 -0
- package/skills/lark-doc/references/genres/meeting-minutes.md +24 -0
- package/skills/lark-doc/references/genres/memo-brief.md +25 -0
- package/skills/lark-doc/references/genres/official-redhead.md +73 -0
- package/skills/lark-doc/references/genres/prd.md +26 -0
- package/skills/lark-doc/references/genres/proposal.md +24 -0
- package/skills/lark-doc/references/genres/research-report.md +32 -0
- package/skills/lark-doc/references/genres/retrospective.md +25 -0
- package/skills/lark-doc/references/genres/route-consumer.md +37 -0
- package/skills/lark-doc/references/genres/route-creative.md +36 -0
- package/skills/lark-doc/references/genres/route-knowledge.md +39 -0
- package/skills/lark-doc/references/genres/route-marketing.md +40 -0
- package/skills/lark-doc/references/genres/route-media.md +36 -0
- package/skills/lark-doc/references/genres/route-opinion.md +38 -0
- package/skills/lark-doc/references/genres/route-personal-brand.md +36 -0
- package/skills/lark-doc/references/genres/route-platform.md +9 -0
- package/skills/lark-doc/references/genres/route-report.md +10 -0
- package/skills/lark-doc/references/genres/route-workplace.md +17 -0
- package/skills/lark-doc/references/genres/sop-tutorial.md +41 -0
- package/skills/lark-doc/references/genres/technical-doc.md +39 -0
- package/skills/lark-doc/references/genres/wechat.md +39 -0
- package/skills/lark-doc/references/genres/weekly-report.md +24 -0
- package/skills/lark-doc/references/genres/white-paper.md +32 -0
- package/skills/lark-doc/references/genres/xiaohongshu.md +38 -0
- package/skills/lark-doc/references/lark-doc-create-workflow.md +121 -0
- package/skills/lark-doc/references/lark-doc-create.md +22 -48
- package/skills/lark-doc/references/lark-doc-fetch.md +84 -93
- package/skills/lark-doc/references/lark-doc-history.md +16 -15
- package/skills/lark-doc/references/lark-doc-md.md +5 -1
- package/skills/lark-doc/references/lark-doc-media-download.md +2 -1
- package/skills/lark-doc/references/lark-doc-script.md +76 -0
- package/skills/lark-doc/references/lark-doc-update.md +70 -222
- package/skills/lark-doc/references/lark-doc-whiteboard.md +14 -17
- package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +46 -0
- package/skills/lark-doc/references/lark-doc-xml.md +38 -166
- package/skills/lark-drive/SKILL.md +32 -50
- package/skills/lark-drive/references/lark-drive-add-comment.md +2 -4
- package/skills/lark-drive/references/lark-drive-add-reply.md +47 -0
- package/skills/lark-drive/references/lark-drive-apply-permission.md +3 -3
- package/skills/lark-drive/references/lark-drive-batch-query-comments.md +46 -0
- package/skills/lark-drive/references/lark-drive-comment-content.md +50 -0
- package/skills/lark-drive/references/lark-drive-comment-location.md +9 -15
- package/skills/lark-drive/references/lark-drive-copy.md +87 -0
- package/skills/lark-drive/references/lark-drive-delete-reply.md +48 -0
- package/skills/lark-drive/references/lark-drive-download.md +6 -1
- package/skills/lark-drive/references/lark-drive-export.md +3 -0
- package/skills/lark-drive/references/lark-drive-list-comments.md +25 -68
- package/skills/lark-drive/references/lark-drive-list-replies.md +54 -0
- package/skills/lark-drive/references/lark-drive-member-add.md +2 -2
- package/skills/lark-drive/references/lark-drive-member-list.md +65 -0
- package/skills/lark-drive/references/lark-drive-permission-get-setting.md +48 -0
- package/skills/lark-drive/references/lark-drive-preview.md +11 -1
- package/skills/lark-drive/references/lark-drive-react-reply.md +51 -0
- package/skills/lark-drive/references/lark-drive-reactions.md +27 -25
- package/skills/lark-drive/references/lark-drive-resolve-comment.md +45 -0
- package/skills/lark-drive/references/lark-drive-restore-comment.md +46 -0
- package/skills/lark-drive/references/lark-drive-search.md +7 -1
- package/skills/lark-drive/references/lark-drive-secure-label.md +1 -1
- package/skills/lark-drive/references/lark-drive-task-result.md +3 -0
- package/skills/lark-drive/references/lark-drive-update-reply.md +46 -0
- package/skills/lark-drive/references/lark-drive-update-title.md +78 -0
- package/skills/lark-drive/references/lark-drive-upload.md +1 -0
- package/skills/lark-drive/references/lark-drive-workflow-permission-governance-commands.md +38 -8
- package/skills/lark-drive/references/lark-drive-workflow-permission-governance-outputs.md +10 -10
- package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +22 -20
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-execute.md +273 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-recall.md +202 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-resolve-verify.md +231 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-review-plan.md +248 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector-setup.md +174 -0
- package/skills/lark-drive/references/lark-drive-workflow-topic-move-collector.md +202 -0
- package/skills/lark-drive/references/lark-drive-workflow.md +5 -4
- package/skills/lark-event/SKILL.md +8 -4
- package/skills/lark-event/references/lark-event-application.md +38 -0
- package/skills/lark-event/references/lark-event-vc.md +8 -2
- package/skills/lark-im/SKILL.md +9 -9
- package/skills/lark-im/references/card/card-2.0-schema.md +1 -1
- package/skills/lark-im/references/card/lark-im-card-style.md +4 -4
- package/skills/lark-im/references/card/resource/icons.md +14 -0
- package/skills/lark-im/references/lark-im-chat-list.md +9 -2
- package/skills/lark-im/references/lark-im-chat-members-list.md +7 -4
- package/skills/lark-im/references/lark-im-chat-messages-list.md +10 -3
- package/skills/lark-im/references/lark-im-chat-search.md +9 -2
- package/skills/lark-im/references/lark-im-feed-group-list-item.md +2 -2
- package/skills/lark-im/references/lark-im-feed-group-list.md +2 -2
- package/skills/lark-im/references/lark-im-feed-shortcut-list.md +1 -1
- package/skills/lark-im/references/lark-im-flag-list.md +9 -8
- package/skills/lark-im/references/lark-im-message-enrichment.md +1 -1
- package/skills/lark-im/references/lark-im-messages-resources-download.md +19 -25
- package/skills/lark-im/references/lark-im-messages-search.md +4 -5
- package/skills/lark-im/references/lark-im-threads-messages-list.md +8 -4
- package/skills/lark-mail/references/lark-mail-triage.md +19 -4
- package/skills/lark-minutes/SKILL.md +1 -1
- package/skills/lark-minutes/references/lark-minutes-search.md +6 -7
- package/skills/lark-okr/SKILL.md +71 -26
- package/skills/lark-okr/references/lark-okr-batch-create.md +19 -18
- package/skills/lark-okr/references/lark-okr-create.md +173 -0
- package/skills/lark-okr/references/lark-okr-cycle-list.md +17 -7
- package/skills/lark-okr/references/lark-okr-entities.md +1 -0
- package/skills/lark-okr/references/lark-okr-indicator-update.md +3 -1
- package/skills/lark-okr/references/lark-okr-indicators.md +61 -12
- package/skills/lark-okr/references/lark-okr-progress-list.md +21 -9
- package/skills/lark-shared/SKILL.md +3 -3
- package/skills/lark-sheets/SKILL.md +83 -82
- package/skills/lark-sheets/references/lark-sheets-batch-update.md +13 -58
- package/skills/lark-sheets/references/lark-sheets-chart.md +2 -1
- package/skills/lark-sheets/references/lark-sheets-conditional-format.md +1 -1
- package/skills/lark-sheets/references/lark-sheets-range-operations.md +5 -5
- package/skills/lark-sheets/references/lark-sheets-read-data.md +80 -6
- package/skills/lark-sheets/references/lark-sheets-sheet-structure.md +21 -10
- package/skills/lark-sheets/references/lark-sheets-styles-put.md +93 -0
- package/skills/lark-sheets/references/lark-sheets-visual-standards.md +2 -2
- package/skills/lark-sheets/references/lark-sheets-workbook.md +4 -3
- package/skills/lark-sheets/references/lark-sheets-write-cells.md +40 -12
- package/skills/lark-sheets/scripts/lark_detect_subtables.py +593 -0
- package/skills/lark-sheets/scripts/lark_inspect_workbook.py +188 -0
- package/skills/lark-sheets/scripts/lark_profile_table.py +614 -0
- package/skills/lark-sheets/scripts/lark_sheet_range.py +176 -0
- package/skills/lark-sheets/scripts/lark_sheet_read_cli.py +184 -0
- package/skills/lark-sheets/scripts/sheets_df.py +21 -3
- package/skills/lark-slides/SKILL.md +134 -104
- package/skills/lark-slides/references/asset-planning.md +6 -4
- package/skills/lark-slides/references/iconpark.md +2 -2
- package/skills/lark-slides/references/lark-slides-add-slide.md +92 -0
- package/skills/lark-slides/references/lark-slides-create.md +86 -66
- package/skills/lark-slides/references/lark-slides-delete-slide.md +65 -0
- package/skills/lark-slides/references/lark-slides-edit-workflows.md +6 -7
- package/skills/lark-slides/references/lark-slides-history.md +132 -0
- package/skills/lark-slides/references/lark-slides-media-upload.md +4 -27
- package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +7 -11
- package/skills/lark-slides/references/lark-slides-replace-slide.md +22 -4
- package/skills/lark-slides/references/lark-slides-screenshot.md +33 -15
- package/skills/lark-slides/references/lark-slides-update-slide.md +146 -0
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +2 -2
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +2 -3
- package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +90 -32
- package/skills/lark-slides/references/planning-layer.md +11 -10
- package/skills/lark-slides/references/slides_chart_demo.xml +1415 -1
- package/skills/lark-slides/references/slides_xml_schema_definition.xml +539 -79
- package/skills/lark-slides/references/troubleshooting.md +26 -9
- package/skills/lark-slides/references/validation-checklist.md +55 -18
- package/skills/lark-slides/references/visual-planning.md +25 -22
- package/skills/lark-slides/references/xml-schema-quick-ref.md +299 -51
- package/skills/lark-slides/scripts/sxsd_validator.py +1052 -0
- package/skills/lark-slides/scripts/xml_text_overlap_lint.py +1964 -195
- package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +4051 -501
- package/skills/lark-task/SKILL.md +7 -0
- package/skills/lark-task/references/lark-task-complete.md +6 -2
- package/skills/lark-task/references/lark-task-create.md +9 -0
- package/skills/lark-task/references/lark-task-update.md +6 -2
- package/skills/lark-whiteboard/SKILL.md +21 -13
- package/skills/lark-whiteboard/elements/layout.md +1 -1
- package/skills/lark-whiteboard/elements/schema.md +2 -2
- package/skills/lark-whiteboard/references/{lark-whiteboard-query.md → lark-whiteboard-export.md} +17 -16
- package/skills/lark-whiteboard/references/lark-whiteboard-update.md +7 -7
- package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +23 -31
- package/skills/lark-whiteboard/routes/dsl.md +11 -5
- package/skills/lark-whiteboard/routes/mermaid.md +3 -3
- package/skills/lark-whiteboard/routes/svg-edit.md +9 -6
- package/skills/lark-whiteboard/routes/svg.md +14 -7
- package/skills/lark-whiteboard/scenes/bar-chart.md +1 -1
- package/skills/lark-whiteboard/scenes/fishbone.md +1 -1
- package/skills/lark-whiteboard/scenes/flywheel.md +1 -1
- package/skills/lark-whiteboard/scenes/line-chart.md +1 -1
- package/skills/lark-whiteboard/scenes/mention.md +71 -0
- package/skills/lark-whiteboard/scenes/treemap.md +1 -1
- package/skills/lark-wiki/SKILL.md +6 -3
- package/skills/lark-wiki/references/lark-wiki-delete-space.md +6 -3
- package/skills/lark-doc/references/lark-doc-word-stat.md +0 -93
- package/skills/lark-doc/references/style/lark-doc-create-workflow.md +0 -47
- package/skills/lark-doc/references/style/lark-doc-style.md +0 -68
- package/skills/lark-doc/references/style/lark-doc-update-workflow.md +0 -48
- package/skills/lark-doc/scripts/doc_word_stat.py +0 -1243
- package/skills/lark-drive/references/lark-drive-comments-guide.md +0 -80
- package/skills/lark-slides/references/examples.md +0 -91
- package/skills/lark-slides/references/lark-slides-replace-pages.md +0 -95
- package/skills/lark-slides/references/lark-slides-whiteboard.md +0 -331
- package/skills/lark-slides/references/lark-slides-xml-get.md +0 -100
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +0 -125
- package/skills/lark-slides/references/slide-templates.md +0 -201
- package/skills/lark-slides/references/slides_demo.xml +0 -226
- package/skills/lark-slides/references/xml-format-guide.md +0 -433
|
@@ -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
|