@amaster.ai/pi-lark 0.1.6 → 0.1.7
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 +24 -12
- package/skills/lark-apps/creative-design/agents/assets/vision-probe.png +0 -0
- package/skills/lark-apps/creative-design/agents/fork-verifier-agent.md +71 -0
- package/skills/lark-apps/creative-design/agents/vision-probe-agent.md +41 -0
- package/skills/lark-apps/creative-design/assets/index.html +27 -0
- package/skills/lark-apps/creative-design/creative-design.md +239 -0
- package/skills/lark-apps/creative-design/references/aily.md +39 -0
- package/skills/lark-apps/creative-design/references/animated-video.md +34 -0
- package/skills/lark-apps/creative-design/references/charts.md +165 -0
- package/skills/lark-apps/creative-design/references/claude.md +36 -0
- package/skills/lark-apps/creative-design/references/codex.md +32 -0
- package/skills/lark-apps/creative-design/references/data-report.md +108 -0
- package/skills/lark-apps/creative-design/references/frontend-design.md +71 -0
- package/skills/lark-apps/creative-design/references/hi-fi-design.md +32 -0
- package/skills/lark-apps/creative-design/references/interactive-prototype.md +24 -0
- package/skills/lark-apps/creative-design/references/make-a-deck.md +133 -0
- package/skills/lark-apps/creative-design/references/visual-exposure.md +82 -0
- package/skills/lark-apps/creative-design/references/wireframe.md +14 -0
- package/skills/lark-apps/creative-design/starter-components/android-frame.jsx +188 -0
- package/skills/lark-apps/creative-design/starter-components/animations.jsx +773 -0
- package/skills/lark-apps/creative-design/starter-components/browser-window.jsx +122 -0
- package/skills/lark-apps/creative-design/starter-components/deck-stage.js +2483 -0
- package/skills/lark-apps/creative-design/starter-components/design-canvas.jsx +1432 -0
- package/skills/lark-apps/creative-design/starter-components/ios-frame.jsx +270 -0
- package/skills/lark-apps/creative-design/starter-components/macos-window.jsx +197 -0
- package/skills/lark-apps/creative-design/starter-components/tweaks-panel.jsx +752 -0
- package/skills/lark-apps/references/lark-apps-automation.md +80 -2
- package/skills/lark-apps/references/lark-apps-cache.md +61 -0
- package/skills/lark-apps/references/lark-apps-cloud-dev.md +0 -1
- package/skills/lark-apps/references/lark-apps-create.md +1 -2
- package/skills/lark-apps/references/lark-apps-db.md +1 -1
- package/skills/lark-apps/references/lark-apps-env-pull.md +1 -1
- package/skills/lark-apps/references/lark-apps-file.md +2 -2
- package/skills/lark-apps/references/lark-apps-git-credential.md +1 -1
- package/skills/lark-apps/references/lark-apps-html-publish.md +4 -8
- package/skills/lark-apps/references/lark-apps-init.md +1 -1
- package/skills/lark-apps/references/lark-apps-list.md +1 -1
- package/skills/lark-apps/references/lark-apps-local-dev.md +54 -11
- package/skills/lark-apps/references/lark-apps-openapi-key.md +1 -1
- package/skills/lark-apps/references/lark-apps-release-create.md +2 -2
- package/skills/lark-apps/references/lark-apps-release-get.md +3 -3
- package/skills/lark-base/SKILL.md +20 -13
- package/skills/lark-base/references/lark-base-cell-value.md +3 -3
- package/skills/lark-base/references/lark-base-data-query.md +11 -4
- package/skills/lark-base/references/lark-base-field-create.md +4 -0
- package/skills/lark-base/references/lark-base-field-json.md +4 -4
- package/skills/lark-base/references/lark-base-field-update.md +17 -1
- package/skills/lark-base/references/lark-base-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/references/lark-doc-fetch.md +10 -2
- package/skills/lark-doc/references/lark-doc-whiteboard.md +9 -8
- package/skills/lark-doc/references/lark-doc-xml-extended-blocks.md +41 -0
- package/skills/lark-doc/references/lark-doc-xml.md +4 -3
- package/skills/lark-drive/SKILL.md +25 -45
- 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 +2 -2
- 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-delete-reply.md +48 -0
- package/skills/lark-drive/references/lark-drive-download.md +5 -1
- 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-update-reply.md +46 -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 +1 -0
- package/skills/lark-event/references/lark-event-application.md +38 -0
- package/skills/lark-im/SKILL.md +1 -1
- 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-flag-list.md +8 -7
- package/skills/lark-okr/SKILL.md +71 -26
- package/skills/lark-okr/references/lark-okr-batch-create.md +19 -18
- package/skills/lark-okr/references/lark-okr-create.md +173 -0
- package/skills/lark-okr/references/lark-okr-cycle-list.md +17 -7
- package/skills/lark-okr/references/lark-okr-entities.md +1 -0
- package/skills/lark-okr/references/lark-okr-indicator-update.md +3 -1
- package/skills/lark-okr/references/lark-okr-indicators.md +61 -12
- package/skills/lark-okr/references/lark-okr-progress-list.md +21 -9
- package/skills/lark-slides/SKILL.md +115 -68
- package/skills/lark-slides/references/asset-planning.md +6 -4
- package/skills/lark-slides/references/iconpark.md +2 -2
- package/skills/lark-slides/references/lark-slides-create.md +16 -8
- package/skills/lark-slides/references/lark-slides-history.md +132 -0
- package/skills/lark-slides/references/lark-slides-media-upload.md +2 -3
- package/skills/lark-slides/references/lark-slides-pptx-template-workflows.md +7 -11
- package/skills/lark-slides/references/lark-slides-replace-slide.md +0 -3
- package/skills/lark-slides/references/lark-slides-screenshot.md +4 -4
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-create.md +219 -0
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-delete.md +6 -5
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-get.md +2 -2
- package/skills/lark-slides/references/lark-slides-xml-presentation-slide-replace.md +2 -3
- package/skills/lark-slides/references/lark-slides-xml-presentations-get.md +65 -30
- package/skills/lark-slides/references/planning-layer.md +11 -10
- package/skills/lark-slides/references/slides_chart_demo.xml +1416 -1
- package/skills/lark-slides/references/slides_xml_schema_definition.xml +492 -76
- package/skills/lark-slides/references/troubleshooting.md +25 -7
- package/skills/lark-slides/references/validation-checklist.md +53 -16
- package/skills/lark-slides/references/visual-planning.md +25 -22
- package/skills/lark-slides/references/xml-schema-quick-ref.md +281 -45
- package/skills/lark-slides/scripts/sxsd_validator.py +908 -0
- package/skills/lark-slides/scripts/xml_text_overlap_lint.py +1650 -165
- package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +3139 -513
- 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 +13 -12
- package/skills/lark-whiteboard/elements/layout.md +1 -1
- package/skills/lark-whiteboard/elements/schema.md +2 -2
- package/skills/lark-whiteboard/references/{lark-whiteboard-query.md → lark-whiteboard-export.md} +15 -15
- package/skills/lark-whiteboard/references/lark-whiteboard-update.md +3 -3
- package/skills/lark-whiteboard/references/lark-whiteboard-workflow.md +7 -17
- package/skills/lark-whiteboard/routes/dsl.md +3 -3
- package/skills/lark-whiteboard/routes/mermaid.md +2 -2
- package/skills/lark-whiteboard/routes/svg-edit.md +4 -4
- package/skills/lark-whiteboard/routes/svg.md +11 -6
- package/skills/lark-whiteboard/scenes/bar-chart.md +1 -1
- package/skills/lark-whiteboard/scenes/fishbone.md +1 -1
- package/skills/lark-whiteboard/scenes/flywheel.md +1 -1
- package/skills/lark-whiteboard/scenes/line-chart.md +1 -1
- package/skills/lark-whiteboard/scenes/treemap.md +1 -1
- package/skills/lark-wiki/SKILL.md +1 -0
- package/skills/lark-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-whiteboard.md +0 -331
- package/skills/lark-slides/references/lark-slides-xml-get.md +0 -100
- package/skills/lark-slides/references/slide-templates.md +0 -201
- package/skills/lark-slides/references/slides_demo.xml +0 -226
- package/skills/lark-slides/references/xml-format-guide.md +0 -433
|
@@ -2,18 +2,22 @@
|
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import itertools
|
|
5
6
|
import json
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
9
10
|
import unittest
|
|
11
|
+
import xml.etree.ElementTree as ET
|
|
10
12
|
from pathlib import Path
|
|
13
|
+
from unittest import mock
|
|
11
14
|
|
|
15
|
+
import sxsd_validator
|
|
12
16
|
import xml_text_overlap_lint
|
|
13
17
|
|
|
14
18
|
|
|
15
|
-
class
|
|
16
|
-
def
|
|
19
|
+
class XmlTextOverlapLintGeometryTest(unittest.TestCase):
|
|
20
|
+
def assertNoXmlTextOverlapLintErrors(self, result: dict, sample_name: str) -> None:
|
|
17
21
|
issue_summaries = []
|
|
18
22
|
for slide in result.get("slides", []):
|
|
19
23
|
for issue in slide.get("issues", []):
|
|
@@ -28,12 +32,56 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
28
32
|
0,
|
|
29
33
|
f"{sample_name} has XML text overlap lint errors:\n" + "\n".join(issue_summaries),
|
|
30
34
|
)
|
|
35
|
+
|
|
36
|
+
def test_cli_suggests_input_flag_for_positional_argument(self) -> None:
|
|
37
|
+
script_path = Path(xml_text_overlap_lint.__file__).resolve()
|
|
38
|
+
input_path = "/sandboxdata/workspace/file/full_presentation.xml"
|
|
39
|
+
|
|
40
|
+
completed = subprocess.run(
|
|
41
|
+
[sys.executable, str(script_path), input_path],
|
|
42
|
+
capture_output=True,
|
|
43
|
+
check=False,
|
|
44
|
+
text=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self.assertEqual(completed.returncode, 1)
|
|
48
|
+
self.assertEqual(completed.stdout, "")
|
|
31
49
|
self.assertEqual(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
f"{sample_name} has XML text overlap lint warnings:\n" + "\n".join(issue_summaries),
|
|
50
|
+
completed.stderr,
|
|
51
|
+
f"xml-text-overlap-lint error: unexpected argument: {input_path}, need --input\n",
|
|
35
52
|
)
|
|
36
53
|
|
|
54
|
+
def test_cli_reports_structured_slide_sxsd_error_outside_skill_directory(self) -> None:
|
|
55
|
+
script_path = Path(xml_text_overlap_lint.__file__).resolve()
|
|
56
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
57
|
+
input_path = Path(temp_dir) / "invalid-slide.xml"
|
|
58
|
+
input_path.write_text(
|
|
59
|
+
"""
|
|
60
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
61
|
+
<data><shape type="text" topLeftX="10" topLeftY="20" width="300"/></data>
|
|
62
|
+
</slide>
|
|
63
|
+
""",
|
|
64
|
+
encoding="utf-8",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
completed = subprocess.run(
|
|
68
|
+
[sys.executable, str(script_path), "--input", str(input_path)],
|
|
69
|
+
cwd=temp_dir,
|
|
70
|
+
capture_output=True,
|
|
71
|
+
check=False,
|
|
72
|
+
text=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
result = json.loads(completed.stdout)
|
|
76
|
+
issue = result["slides"][0]["errors"][0]
|
|
77
|
+
self.assertEqual(completed.returncode, 1)
|
|
78
|
+
self.assertEqual(completed.stderr, "")
|
|
79
|
+
self.assertEqual(issue["code"], "sxsd_missing_required_attr")
|
|
80
|
+
self.assertEqual(issue["path"], "slide/data/shape")
|
|
81
|
+
self.assertEqual(issue["attr"], "height")
|
|
82
|
+
self.assertEqual(issue["target"]["slide_number"], 1)
|
|
83
|
+
self.assertTrue(issue["hint"])
|
|
84
|
+
|
|
37
85
|
def test_xml_text_overlap_lint_accepts_inline_fixture_xml_samples(self) -> None:
|
|
38
86
|
samples = {
|
|
39
87
|
"image-led-cover": """
|
|
@@ -83,7 +131,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
83
131
|
sample_xml,
|
|
84
132
|
sample_name,
|
|
85
133
|
)
|
|
86
|
-
self.
|
|
134
|
+
self.assertNoXmlTextOverlapLintErrors(result, sample_name)
|
|
87
135
|
|
|
88
136
|
def test_lint_xml_reports_unescaped_ampersand_in_text(self) -> None:
|
|
89
137
|
result = xml_text_overlap_lint.lint_xml(
|
|
@@ -160,12 +208,12 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
160
208
|
self.assertEqual(issues[0]["tag"], "ns0:slide")
|
|
161
209
|
self.assertIn("default namespace", issues[0]["hint"])
|
|
162
210
|
|
|
163
|
-
def
|
|
211
|
+
def test_lint_xml_accepts_server_readback_slide_without_namespace(self) -> None:
|
|
164
212
|
result = xml_text_overlap_lint.lint_xml(
|
|
165
213
|
"""
|
|
166
|
-
<slide>
|
|
214
|
+
<slide id="server-slide-id">
|
|
167
215
|
<data>
|
|
168
|
-
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
216
|
+
<shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
169
217
|
<content textType="body"><p>Unprefixed SML</p></content>
|
|
170
218
|
</shape>
|
|
171
219
|
</data>
|
|
@@ -174,6 +222,90 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
174
222
|
)
|
|
175
223
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
176
224
|
|
|
225
|
+
def test_lint_xml_accepts_server_readback_presentation_short_namespace(self) -> None:
|
|
226
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
227
|
+
"""
|
|
228
|
+
<presentation xmlns="/sml/2.0" width="960" height="540" id="server-presentation-id">
|
|
229
|
+
<slide id="server-slide-id">
|
|
230
|
+
<data>
|
|
231
|
+
<shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
232
|
+
<content textType="body"><p>Server readback presentation</p></content>
|
|
233
|
+
</shape>
|
|
234
|
+
</data>
|
|
235
|
+
</slide>
|
|
236
|
+
</presentation>
|
|
237
|
+
"""
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
self.assertEqual(result["summary"]["slide_count"], 1)
|
|
241
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
242
|
+
|
|
243
|
+
def test_lint_xml_accepts_server_readback_presentation_https_namespace(self) -> None:
|
|
244
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
245
|
+
"""
|
|
246
|
+
<presentation xmlns="https://www.larkoffice.com/sml/2.0" width="960" height="540" id="server-presentation-id">
|
|
247
|
+
<slide id="server-slide-id">
|
|
248
|
+
<data>
|
|
249
|
+
<shape id="server-shape-id" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
250
|
+
<content textType="body"><p>Server readback presentation</p></content>
|
|
251
|
+
</shape>
|
|
252
|
+
</data>
|
|
253
|
+
</slide>
|
|
254
|
+
</presentation>
|
|
255
|
+
"""
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
self.assertEqual(result["summary"]["slide_count"], 1)
|
|
259
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
260
|
+
|
|
261
|
+
def test_lint_xml_rejects_server_readback_presentation_wrong_namespace(self) -> None:
|
|
262
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
263
|
+
"""
|
|
264
|
+
<presentation xmlns="https://example.com/not-sml" width="960" height="540">
|
|
265
|
+
<slide/>
|
|
266
|
+
</presentation>
|
|
267
|
+
"""
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
271
|
+
self.assertEqual(result["issues"][0]["code"], "sxsd_invalid_namespace")
|
|
272
|
+
|
|
273
|
+
def test_lint_xml_rejects_xml_declaration(self) -> None:
|
|
274
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
275
|
+
'<?xml version="1.0"?><slide xmlns="http://www.larkoffice.com/sml/2.0"><data/></slide>'
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
279
|
+
self.assertEqual(result["issues"][0]["code"], "sxsd_unsupported_declaration")
|
|
280
|
+
|
|
281
|
+
def test_lint_xml_reports_missing_required_sxsd_attribute(self) -> None:
|
|
282
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
283
|
+
"""
|
|
284
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
285
|
+
<data><shape type="text" topLeftX="80" topLeftY="80" width="300"/></data>
|
|
286
|
+
</slide>
|
|
287
|
+
"""
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
291
|
+
self.assertNotIn("issues", result)
|
|
292
|
+
issue = result["slides"][0]["issues"][0]
|
|
293
|
+
self.assertEqual(issue["code"], "sxsd_missing_required_attr")
|
|
294
|
+
self.assertEqual(issue["attr"], "height")
|
|
295
|
+
|
|
296
|
+
def test_lint_xml_rejects_child_order_that_violates_xsd_sequence(self) -> None:
|
|
297
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
298
|
+
"""
|
|
299
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
300
|
+
<slide/>
|
|
301
|
+
<title>Late title</title>
|
|
302
|
+
</presentation>
|
|
303
|
+
"""
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
307
|
+
self.assertEqual(result["issues"][0]["code"], "sxsd_invalid_child_order")
|
|
308
|
+
|
|
177
309
|
def test_lint_xml_accepts_escaped_entities_without_suspicious_entity_warning(self) -> None:
|
|
178
310
|
result = xml_text_overlap_lint.lint_xml(
|
|
179
311
|
"""
|
|
@@ -203,7 +335,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
203
335
|
)
|
|
204
336
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
205
337
|
|
|
206
|
-
def
|
|
338
|
+
def test_lint_xml_single_slide_reports_out_of_canvas_and_blank_slide_errors(self) -> None:
|
|
207
339
|
result = xml_text_overlap_lint.lint_xml(
|
|
208
340
|
"""
|
|
209
341
|
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
@@ -217,8 +349,11 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
217
349
|
)
|
|
218
350
|
self.assertEqual(result["slide_size"], {"width": 960, "height": 540})
|
|
219
351
|
self.assertEqual(result["summary"]["slide_count"], 1)
|
|
220
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
221
|
-
self.assertEqual(
|
|
352
|
+
self.assertEqual(result["summary"]["error_count"], 2)
|
|
353
|
+
self.assertEqual(
|
|
354
|
+
[issue["code"] for issue in result["slides"][0]["errors"]],
|
|
355
|
+
["shape_out_of_canvas", "blank_slide"],
|
|
356
|
+
)
|
|
222
357
|
|
|
223
358
|
def test_lint_xml_preserves_presentation_canvas_and_slide_order(self) -> None:
|
|
224
359
|
result = xml_text_overlap_lint.lint_xml(
|
|
@@ -234,8 +369,8 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
234
369
|
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
235
370
|
<data>
|
|
236
371
|
<img id="second" src="tok" topLeftX="100" topLeftY="120" width="240" height="160"/>
|
|
237
|
-
<shape id="
|
|
238
|
-
<content textType="body"><p>
|
|
372
|
+
<shape id="second-shape" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
373
|
+
<content textType="body"><p>Second slide</p></content>
|
|
239
374
|
</shape>
|
|
240
375
|
</data>
|
|
241
376
|
</slide>
|
|
@@ -245,10 +380,151 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
245
380
|
self.assertEqual(result["slide_size"], {"width": 1280, "height": 720})
|
|
246
381
|
self.assertEqual(result["summary"]["slide_count"], 2)
|
|
247
382
|
self.assertEqual([slide["slide_number"] for slide in result["slides"]], [1, 2])
|
|
248
|
-
self.assertEqual([slide["element_count"] for slide in result["slides"]], [1,
|
|
383
|
+
self.assertEqual([slide["element_count"] for slide in result["slides"]], [1, 2])
|
|
249
384
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
250
385
|
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
251
386
|
|
|
387
|
+
def test_lint_xml_handles_self_closing_slide_before_normal_slide(self) -> None:
|
|
388
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
389
|
+
"""
|
|
390
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
391
|
+
<slide/>
|
|
392
|
+
<slide>
|
|
393
|
+
<data>
|
|
394
|
+
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
395
|
+
<content textType="body"><p>Second slide</p></content>
|
|
396
|
+
</shape>
|
|
397
|
+
</data>
|
|
398
|
+
</slide>
|
|
399
|
+
</presentation>
|
|
400
|
+
"""
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
self.assertEqual(result["summary"]["slide_count"], 2)
|
|
404
|
+
self.assertEqual(
|
|
405
|
+
[issue["code"] for issue in result["slides"][0]["errors"]],
|
|
406
|
+
["blank_slide"],
|
|
407
|
+
)
|
|
408
|
+
self.assertEqual(result["slides"][1]["status"], "passed")
|
|
409
|
+
|
|
410
|
+
def test_lint_xml_keeps_trailing_self_closing_slide(self) -> None:
|
|
411
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
412
|
+
"""
|
|
413
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
414
|
+
<slide>
|
|
415
|
+
<data>
|
|
416
|
+
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
417
|
+
<content textType="body"><p>First slide</p></content>
|
|
418
|
+
</shape>
|
|
419
|
+
</data>
|
|
420
|
+
</slide>
|
|
421
|
+
<slide/>
|
|
422
|
+
</presentation>
|
|
423
|
+
"""
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
self.assertEqual(result["summary"]["slide_count"], 2)
|
|
427
|
+
self.assertEqual(
|
|
428
|
+
[issue["code"] for issue in result["slides"][1]["errors"]],
|
|
429
|
+
["blank_slide"],
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def test_lint_xml_ignores_slide_markup_inside_xml_comments(self) -> None:
|
|
433
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
434
|
+
"""
|
|
435
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
436
|
+
<slide>
|
|
437
|
+
<data>
|
|
438
|
+
<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
439
|
+
<content textType="body"><p>Real slide</p></content>
|
|
440
|
+
</shape>
|
|
441
|
+
</data>
|
|
442
|
+
</slide>
|
|
443
|
+
<!-- Old draft: <slide id="ghost"/> -->
|
|
444
|
+
</presentation>
|
|
445
|
+
"""
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
self.assertEqual(result["summary"]["slide_count"], 1)
|
|
449
|
+
self.assertEqual(result["slides"][0]["status"], "passed")
|
|
450
|
+
|
|
451
|
+
def test_lint_xml_ignores_invalid_slide_markup_inside_xml_comments(self) -> None:
|
|
452
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
453
|
+
"""
|
|
454
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
455
|
+
<slide><data/></slide>
|
|
456
|
+
<!-- Old draft: <slide bogus="x"/> -->
|
|
457
|
+
</presentation>
|
|
458
|
+
"""
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
self.assertEqual(result["summary"]["slide_count"], 1)
|
|
462
|
+
self.assertEqual(
|
|
463
|
+
[issue["code"] for issue in result["slides"][0]["errors"]],
|
|
464
|
+
["blank_slide"],
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
def test_lint_xml_skips_only_invalid_slide_and_continues_geometry_checks(self) -> None:
|
|
468
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
469
|
+
"""
|
|
470
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
471
|
+
<slide>
|
|
472
|
+
<data>
|
|
473
|
+
<shape id="invalid" type="text" topLeftX="1000" topLeftY="80" width="300">
|
|
474
|
+
<content textType="body"><p>Missing height</p></content>
|
|
475
|
+
</shape>
|
|
476
|
+
</data>
|
|
477
|
+
</slide>
|
|
478
|
+
<slide>
|
|
479
|
+
<data>
|
|
480
|
+
<shape id="outside" type="text" topLeftX="1000" topLeftY="80" width="120" height="60">
|
|
481
|
+
<content textType="body"><p>Outside canvas</p></content>
|
|
482
|
+
</shape>
|
|
483
|
+
</data>
|
|
484
|
+
</slide>
|
|
485
|
+
</presentation>
|
|
486
|
+
"""
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
self.assertEqual(result["summary"]["slide_count"], 2)
|
|
490
|
+
self.assertEqual(result["slides"][0]["element_count"], 0)
|
|
491
|
+
self.assertEqual(
|
|
492
|
+
[issue["code"] for issue in result["slides"][0]["errors"]],
|
|
493
|
+
["sxsd_missing_required_attr"],
|
|
494
|
+
)
|
|
495
|
+
self.assertNotIn(
|
|
496
|
+
"shape_out_of_canvas",
|
|
497
|
+
[issue["code"] for issue in result["slides"][0]["issues"]],
|
|
498
|
+
)
|
|
499
|
+
self.assertIn(
|
|
500
|
+
"shape_out_of_canvas",
|
|
501
|
+
[issue["code"] for issue in result["slides"][1]["errors"]],
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
def test_lint_xml_scopes_invalid_slide_root_attribute_to_that_slide(self) -> None:
|
|
505
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
506
|
+
"""
|
|
507
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
508
|
+
<slide bogusAttr="oops">
|
|
509
|
+
<data><shape type="rect" topLeftX="80" topLeftY="80" width="100" height="100"/></data>
|
|
510
|
+
</slide>
|
|
511
|
+
<slide>
|
|
512
|
+
<data><shape type="rect" topLeftX="1000" topLeftY="80" width="100" height="100"/></data>
|
|
513
|
+
</slide>
|
|
514
|
+
</presentation>
|
|
515
|
+
"""
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
self.assertEqual(result["summary"]["slide_count"], 2)
|
|
519
|
+
self.assertEqual(
|
|
520
|
+
[issue["code"] for issue in result["slides"][0]["errors"]],
|
|
521
|
+
["sxsd_unsupported_attr"],
|
|
522
|
+
)
|
|
523
|
+
self.assertIn(
|
|
524
|
+
"shape_out_of_canvas",
|
|
525
|
+
[issue["code"] for issue in result["slides"][1]["errors"]],
|
|
526
|
+
)
|
|
527
|
+
|
|
252
528
|
def test_lint_xml_reports_sxsd_unsupported_tag_with_alias_hint(self) -> None:
|
|
253
529
|
cases = [
|
|
254
530
|
("textbox", '<textbox topLeftX="80" topLeftY="80" width="300" height="60">Text</textbox>', '<shape type="text">'),
|
|
@@ -263,8 +539,15 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
263
539
|
</slide>
|
|
264
540
|
"""
|
|
265
541
|
)
|
|
266
|
-
|
|
542
|
+
slide_issues = result["slides"][0]["issues"]
|
|
543
|
+
issue = next(
|
|
544
|
+
issue for issue in slide_issues if issue["code"] == "sxsd_unsupported_tag"
|
|
545
|
+
)
|
|
267
546
|
self.assertEqual(result["summary"]["error_count"], 1)
|
|
547
|
+
self.assertEqual(
|
|
548
|
+
[reported["code"] for reported in slide_issues],
|
|
549
|
+
["sxsd_unsupported_tag"],
|
|
550
|
+
)
|
|
268
551
|
self.assertEqual(issue["code"], "sxsd_unsupported_tag")
|
|
269
552
|
self.assertEqual(issue["tag"], tag_name)
|
|
270
553
|
self.assertIn(expected_hint, issue["hint"])
|
|
@@ -272,6 +555,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
272
555
|
def test_lint_xml_reports_sxsd_unsupported_attr_with_alias_hint(self) -> None:
|
|
273
556
|
cases = [
|
|
274
557
|
("shape", "x", "topLeftX", '<shape type="text" x="80" topLeftY="80" width="300" height="60"><content><p>Text</p></content></shape>'),
|
|
558
|
+
("shape", "heigth", "height", '<shape type="text" topLeftX="80" topLeftY="80" width="300" heigth="60"><content><p>Text</p></content></shape>'),
|
|
275
559
|
("content", "fontColor", "color", '<shape type="text" topLeftX="80" topLeftY="80" width="300" height="60"><content fontColor="rgba(0, 0, 0, 1)"><p>Text</p></content></shape>'),
|
|
276
560
|
]
|
|
277
561
|
for tag_name, attr_name, expected_attr, element_xml in cases:
|
|
@@ -283,87 +567,264 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
283
567
|
</slide>
|
|
284
568
|
"""
|
|
285
569
|
)
|
|
286
|
-
|
|
570
|
+
slide_issues = result["slides"][0]["issues"]
|
|
571
|
+
issue = next(
|
|
572
|
+
issue for issue in slide_issues if issue["code"] == "sxsd_unsupported_attr"
|
|
573
|
+
)
|
|
287
574
|
self.assertEqual(result["summary"]["error_count"], 1)
|
|
575
|
+
self.assertEqual(
|
|
576
|
+
[reported["code"] for reported in slide_issues],
|
|
577
|
+
["sxsd_unsupported_attr"],
|
|
578
|
+
)
|
|
288
579
|
self.assertEqual(issue["code"], "sxsd_unsupported_attr")
|
|
289
580
|
self.assertEqual(issue["tag"], tag_name)
|
|
290
581
|
self.assertEqual(issue["attr"], attr_name)
|
|
291
582
|
self.assertIn(expected_attr, issue["hint"])
|
|
292
583
|
|
|
293
|
-
def
|
|
584
|
+
def test_lint_xml_keeps_unrelated_unsupported_and_missing_attrs(self) -> None:
|
|
294
585
|
result = xml_text_overlap_lint.lint_xml(
|
|
295
586
|
"""
|
|
296
|
-
<slide
|
|
587
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
297
588
|
<data>
|
|
298
|
-
<shape
|
|
299
|
-
<fill id="server-fill-id" unexpected="value">
|
|
300
|
-
<fillColor color="rgba(255, 255, 255, 1)"/>
|
|
301
|
-
</fill>
|
|
302
|
-
</shape>
|
|
589
|
+
<shape type="text" bogus="x" topLeftX="80" topLeftY="80" width="300"/>
|
|
303
590
|
</data>
|
|
304
591
|
</slide>
|
|
305
592
|
"""
|
|
306
593
|
)
|
|
307
594
|
|
|
308
|
-
self.assertEqual(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
self.assertEqual(issue["attr"], "unexpected")
|
|
595
|
+
self.assertEqual(
|
|
596
|
+
[issue["code"] for issue in result["slides"][0]["issues"]],
|
|
597
|
+
["sxsd_unsupported_attr", "sxsd_missing_required_attr"],
|
|
598
|
+
)
|
|
313
599
|
|
|
314
|
-
def
|
|
600
|
+
def test_lint_xml_keeps_missing_attrs_when_suggestion_is_ambiguous(self) -> None:
|
|
315
601
|
result = xml_text_overlap_lint.lint_xml(
|
|
316
602
|
"""
|
|
317
603
|
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
318
604
|
<data>
|
|
319
|
-
<shape type="
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
605
|
+
<shape type="text" topLeft="80" width="300" height="60"/>
|
|
606
|
+
</data>
|
|
607
|
+
</slide>
|
|
608
|
+
"""
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
self.assertEqual(
|
|
612
|
+
[issue["code"] for issue in result["slides"][0]["issues"]],
|
|
613
|
+
[
|
|
614
|
+
"sxsd_unsupported_attr",
|
|
615
|
+
"sxsd_missing_required_attr",
|
|
616
|
+
"sxsd_missing_required_attr",
|
|
617
|
+
],
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
def test_lint_xml_suppresses_only_missing_attr_that_resolves_ambiguous_suggestion(self) -> None:
|
|
621
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
622
|
+
"""
|
|
623
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
624
|
+
<data>
|
|
625
|
+
<shape type="text" topLeftXX="80" topLeftY="80" width="300" height="60"/>
|
|
626
|
+
</data>
|
|
627
|
+
</slide>
|
|
628
|
+
"""
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
self.assertEqual(
|
|
632
|
+
[
|
|
633
|
+
(issue["code"], issue.get("attr"))
|
|
634
|
+
for issue in result["slides"][0]["issues"]
|
|
635
|
+
],
|
|
636
|
+
[("sxsd_unsupported_attr", "topLeftXX")],
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
def test_lint_xml_ignores_server_filled_id_attrs(self) -> None:
|
|
640
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
641
|
+
"""
|
|
642
|
+
<slide id="server-slide-id" xmlns="http://www.larkoffice.com/sml/2.0">
|
|
643
|
+
<data>
|
|
644
|
+
<shape id="server-shape-id" type="rect" topLeftX="80" topLeftY="80" width="300" height="160">
|
|
645
|
+
<fill id="server-fill-id" unexpected="value">
|
|
646
|
+
<fillColor color="rgba(255, 255, 255, 1)"/>
|
|
328
647
|
</fill>
|
|
329
648
|
</shape>
|
|
330
649
|
</data>
|
|
331
650
|
</slide>
|
|
332
651
|
"""
|
|
333
652
|
)
|
|
334
|
-
|
|
335
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
653
|
+
|
|
654
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
336
655
|
self.assertEqual(
|
|
337
|
-
|
|
338
|
-
|
|
656
|
+
[(issue["tag"], issue["attr"]) for issue in result["slides"][0]["issues"]],
|
|
657
|
+
[("fill", "unexpected")],
|
|
339
658
|
)
|
|
340
|
-
self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in result["issues"]))
|
|
341
|
-
self.assertTrue(all(issue["tag"] == "fillColor" for issue in result["issues"]))
|
|
342
659
|
|
|
343
|
-
def
|
|
660
|
+
def test_lint_xml_ignores_chart_roundtrip_attrs(self) -> None:
|
|
344
661
|
result = xml_text_overlap_lint.lint_xml(
|
|
345
662
|
"""
|
|
346
663
|
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
347
664
|
<data>
|
|
348
|
-
<chart topLeftX="80" topLeftY="80" width="
|
|
349
|
-
<chartPlotArea>
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
<dim1>
|
|
354
|
-
<chartField name="month" valueType="string">Jan, Feb</chartField>
|
|
355
|
-
</dim1>
|
|
356
|
-
<dim2>
|
|
357
|
-
<chartField name="value" valueType="number">1, 2</chartField>
|
|
358
|
-
</dim2>
|
|
665
|
+
<chart updated="true" topLeftX="80" topLeftY="80" width="300" height="160">
|
|
666
|
+
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
|
667
|
+
<chartData isStaticData="true">
|
|
668
|
+
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
|
|
669
|
+
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
|
|
359
670
|
</chartData>
|
|
360
671
|
</chart>
|
|
361
672
|
</data>
|
|
362
673
|
</slide>
|
|
363
674
|
"""
|
|
364
675
|
)
|
|
676
|
+
|
|
365
677
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
366
|
-
|
|
678
|
+
|
|
679
|
+
def test_lint_xml_ignores_chart_parsed_values_roundtrip_tags(self) -> None:
|
|
680
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
681
|
+
"""
|
|
682
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
683
|
+
<data>
|
|
684
|
+
<chart topLeftX="80" topLeftY="80" width="300" height="160">
|
|
685
|
+
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
|
686
|
+
<chartData isStaticData="true">
|
|
687
|
+
<dim1>
|
|
688
|
+
<chartField name="category" valueType="string">
|
|
689
|
+
A<chartParsedValues>A</chartParsedValues>
|
|
690
|
+
</chartField>
|
|
691
|
+
</dim1>
|
|
692
|
+
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
|
|
693
|
+
</chartData>
|
|
694
|
+
</chart>
|
|
695
|
+
</data>
|
|
696
|
+
</slide>
|
|
697
|
+
"""
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
701
|
+
|
|
702
|
+
def test_lint_xml_ignores_chart_parsed_values_roundtrip_tag(self) -> None:
|
|
703
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
704
|
+
"""
|
|
705
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
706
|
+
<data>
|
|
707
|
+
<chart topLeftX="80" topLeftY="80" width="300" height="160">
|
|
708
|
+
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
|
709
|
+
<chartData>
|
|
710
|
+
<dim1>
|
|
711
|
+
<chartField name="category" valueType="string">
|
|
712
|
+
Africa<chartParsedValues>Africa</chartParsedValues>
|
|
713
|
+
</chartField>
|
|
714
|
+
</dim1>
|
|
715
|
+
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
|
|
716
|
+
</chartData>
|
|
717
|
+
</chart>
|
|
718
|
+
</data>
|
|
719
|
+
</slide>
|
|
720
|
+
"""
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
724
|
+
self.assertNotIn("issues", result)
|
|
725
|
+
|
|
726
|
+
def test_lint_xml_rejects_chart_parsed_values_outside_chart_field(self) -> None:
|
|
727
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
728
|
+
"""
|
|
729
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
730
|
+
<data>
|
|
731
|
+
<shape type="rect" topLeftX="80" topLeftY="80" width="300" height="160">
|
|
732
|
+
<chartParsedValues>unexpected</chartParsedValues>
|
|
733
|
+
</shape>
|
|
734
|
+
</data>
|
|
735
|
+
</slide>
|
|
736
|
+
"""
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
740
|
+
self.assertEqual(
|
|
741
|
+
[issue["code"] for issue in result["slides"][0]["issues"]],
|
|
742
|
+
["sxsd_unsupported_tag"],
|
|
743
|
+
)
|
|
744
|
+
self.assertEqual(
|
|
745
|
+
result["slides"][0]["issues"][0]["path"],
|
|
746
|
+
"slide/data/shape/chartParsedValues",
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
def test_lint_xml_limits_chart_roundtrip_attrs_to_matching_tags(self) -> None:
|
|
750
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
751
|
+
"""
|
|
752
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
753
|
+
<data>
|
|
754
|
+
<chart isStaticData="true" topLeftX="80" topLeftY="80" width="300" height="160">
|
|
755
|
+
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
|
756
|
+
<chartData updated="true">
|
|
757
|
+
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
|
|
758
|
+
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
|
|
759
|
+
</chartData>
|
|
760
|
+
</chart>
|
|
761
|
+
</data>
|
|
762
|
+
</slide>
|
|
763
|
+
"""
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
self.assertEqual(result["summary"]["error_count"], 2)
|
|
767
|
+
slide_issues = result["slides"][0]["issues"]
|
|
768
|
+
self.assertEqual(
|
|
769
|
+
{(issue["tag"], issue["attr"]) for issue in slide_issues},
|
|
770
|
+
{("chart", "isStaticData"), ("chartData", "updated")},
|
|
771
|
+
)
|
|
772
|
+
self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in slide_issues))
|
|
773
|
+
|
|
774
|
+
def test_lint_xml_reports_gradient_shorthand_attrs_on_fill_color(self) -> None:
|
|
775
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
776
|
+
"""
|
|
777
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
778
|
+
<data>
|
|
779
|
+
<shape type="rect" topLeftX="80" topLeftY="80" width="300" height="160">
|
|
780
|
+
<fill>
|
|
781
|
+
<fillColor
|
|
782
|
+
type="gradient"
|
|
783
|
+
color1="rgba(255, 0, 0, 1)"
|
|
784
|
+
color2="rgba(0, 0, 255, 1)"
|
|
785
|
+
angle="45"
|
|
786
|
+
stop1="0%"
|
|
787
|
+
stop2="100%"/>
|
|
788
|
+
</fill>
|
|
789
|
+
</shape>
|
|
790
|
+
</data>
|
|
791
|
+
</slide>
|
|
792
|
+
"""
|
|
793
|
+
)
|
|
794
|
+
slide_issues = result["slides"][0]["issues"]
|
|
795
|
+
unsupported_attrs = {issue["attr"] for issue in slide_issues}
|
|
796
|
+
self.assertEqual(result["summary"]["error_count"], 6)
|
|
797
|
+
self.assertEqual(
|
|
798
|
+
unsupported_attrs,
|
|
799
|
+
{"type", "color1", "color2", "angle", "stop1", "stop2"},
|
|
800
|
+
)
|
|
801
|
+
self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in slide_issues))
|
|
802
|
+
self.assertTrue(all(issue["tag"] == "fillColor" for issue in slide_issues))
|
|
803
|
+
|
|
804
|
+
def test_lint_xml_accepts_chart_field_simple_content_attrs(self) -> None:
|
|
805
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
806
|
+
"""
|
|
807
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
808
|
+
<data>
|
|
809
|
+
<chart topLeftX="80" topLeftY="80" width="520" height="320">
|
|
810
|
+
<chartPlotArea>
|
|
811
|
+
<chartPlot type="line"/>
|
|
812
|
+
</chartPlotArea>
|
|
813
|
+
<chartData>
|
|
814
|
+
<dim1>
|
|
815
|
+
<chartField name="month" valueType="string">Jan, Feb</chartField>
|
|
816
|
+
</dim1>
|
|
817
|
+
<dim2>
|
|
818
|
+
<chartField name="value" valueType="number">1, 2</chartField>
|
|
819
|
+
</dim2>
|
|
820
|
+
</chartData>
|
|
821
|
+
</chart>
|
|
822
|
+
</data>
|
|
823
|
+
</slide>
|
|
824
|
+
"""
|
|
825
|
+
)
|
|
826
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
827
|
+
self.assertNotIn("issues", result)
|
|
367
828
|
|
|
368
829
|
def test_lint_xml_does_not_load_iconpark_index_without_icons(self) -> None:
|
|
369
830
|
original_loader = xml_text_overlap_lint.load_iconpark_icon_types
|
|
@@ -468,43 +929,6 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
468
929
|
self.assertIn("iconpark-index.json", issue["hint"])
|
|
469
930
|
self.assertIn("iconpark/Base/setting.svg", issue["hint"])
|
|
470
931
|
|
|
471
|
-
def test_lint_xml_allows_namespaced_svg_inside_whiteboard(self) -> None:
|
|
472
|
-
result = xml_text_overlap_lint.lint_xml(
|
|
473
|
-
"""
|
|
474
|
-
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
475
|
-
<data>
|
|
476
|
-
<whiteboard id="wb" topLeftX="80" topLeftY="80" width="300" height="180">
|
|
477
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 180">
|
|
478
|
-
<rect x="10" y="10" width="120" height="60"/>
|
|
479
|
-
</svg>
|
|
480
|
-
</whiteboard>
|
|
481
|
-
</data>
|
|
482
|
-
</slide>
|
|
483
|
-
"""
|
|
484
|
-
)
|
|
485
|
-
self.assertEqual(result["summary"]["error_count"], 0)
|
|
486
|
-
self.assertNotIn("issues", result)
|
|
487
|
-
|
|
488
|
-
def test_lint_xml_reports_unqualified_svg_inside_whiteboard_with_namespace_hint(self) -> None:
|
|
489
|
-
result = xml_text_overlap_lint.lint_xml(
|
|
490
|
-
"""
|
|
491
|
-
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
492
|
-
<data>
|
|
493
|
-
<whiteboard id="wb" topLeftX="80" topLeftY="80" width="300" height="180">
|
|
494
|
-
<svg viewBox="0 0 300 180">
|
|
495
|
-
<rect x="10" y="10" width="120" height="60"/>
|
|
496
|
-
</svg>
|
|
497
|
-
</whiteboard>
|
|
498
|
-
</data>
|
|
499
|
-
</slide>
|
|
500
|
-
"""
|
|
501
|
-
)
|
|
502
|
-
issue = result["issues"][0]
|
|
503
|
-
self.assertEqual(result["summary"]["error_count"], 1)
|
|
504
|
-
self.assertEqual(issue["code"], "sxsd_unsupported_tag")
|
|
505
|
-
self.assertEqual(issue["tag"], "svg")
|
|
506
|
-
self.assertIn('xmlns="http://www.w3.org/2000/svg"', issue["hint"])
|
|
507
|
-
|
|
508
932
|
def test_lint_xml_detects_overlapping_text_boxes(self) -> None:
|
|
509
933
|
result = xml_text_overlap_lint.lint_xml(
|
|
510
934
|
"""
|
|
@@ -523,6 +947,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
523
947
|
"""
|
|
524
948
|
)
|
|
525
949
|
self.assertEqual(result["summary"]["error_count"], 1)
|
|
950
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
526
951
|
self.assertEqual(result["slides"][0]["issues"][0]["code"], "bbox_overlap")
|
|
527
952
|
|
|
528
953
|
def test_lint_xml_detects_current_itinerary_cjk_caption_occlusion(self) -> None:
|
|
@@ -589,7 +1014,7 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
589
1014
|
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
590
1015
|
<data>
|
|
591
1016
|
<shape id="source" type="text" topLeftX="80" topLeftY="100" width="160" height="40">
|
|
592
|
-
<content fontSize="18"><p>这是一个足够长的中文文本用于检测跨越间隙的横向溢出</p></content>
|
|
1017
|
+
<content fontSize="18" wrap="false"><p>这是一个足够长的中文文本用于检测跨越间隙的横向溢出</p></content>
|
|
593
1018
|
</shape>
|
|
594
1019
|
<shape id="target" type="text" topLeftX="260" topLeftY="100" width="160" height="40">
|
|
595
1020
|
<content fontSize="18"><p>目标</p></content>
|
|
@@ -599,10 +1024,35 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
599
1024
|
"""
|
|
600
1025
|
)
|
|
601
1026
|
self.assertEqual(result["summary"]["error_count"], 1)
|
|
602
|
-
self.assertEqual(result["
|
|
603
|
-
|
|
1027
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1028
|
+
issue = result["slides"][0]["issues"][0]
|
|
1029
|
+
self.assertEqual(issue["code"], "bbox_overlap")
|
|
1030
|
+
self.assertEqual(issue["elements"], ["source", "target"])
|
|
1031
|
+
self.assertGreater(issue["measurement"]["intersection_area"], 0)
|
|
1032
|
+
self.assertIsNotNone(issue.get("hint"))
|
|
1033
|
+
|
|
1034
|
+
def test_lint_xml_allows_horizontal_text_with_default_wrap(self) -> None:
|
|
1035
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1036
|
+
"""
|
|
1037
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1038
|
+
<data>
|
|
1039
|
+
<shape id="source" type="text" topLeftX="80" topLeftY="100" width="160" height="40">
|
|
1040
|
+
<content fontSize="18"><p>这是一个足够长的中文文本用于检测默认自动换行</p></content>
|
|
1041
|
+
</shape>
|
|
1042
|
+
<shape id="target" type="text" topLeftX="260" topLeftY="100" width="160" height="40">
|
|
1043
|
+
<content fontSize="18"><p>目标</p></content>
|
|
1044
|
+
</shape>
|
|
1045
|
+
</data>
|
|
1046
|
+
</slide>
|
|
1047
|
+
"""
|
|
1048
|
+
)
|
|
1049
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1050
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1051
|
+
self.assertEqual(result["slides"][0]["issues"][0]["code"], "text_may_overflow_shape")
|
|
1052
|
+
self.assertEqual(result["slides"][0]["issues"][0]["level"], "error")
|
|
1053
|
+
self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["source"])
|
|
604
1054
|
|
|
605
|
-
def
|
|
1055
|
+
def test_lint_xml_reports_text_out_of_canvas_and_warns_for_text_height(self) -> None:
|
|
606
1056
|
result = xml_text_overlap_lint.lint_xml(
|
|
607
1057
|
"""
|
|
608
1058
|
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
@@ -620,515 +1070,658 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
620
1070
|
"""
|
|
621
1071
|
)
|
|
622
1072
|
issue = result["slides"][0]["issues"][0]
|
|
623
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
1073
|
+
self.assertEqual(result["summary"]["error_count"], 2)
|
|
624
1074
|
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
625
1075
|
self.assertEqual(issue["code"], "shape_out_of_canvas")
|
|
626
1076
|
self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 160, "bottom": 40})
|
|
627
1077
|
|
|
628
|
-
def
|
|
1078
|
+
def test_lint_xml_warns_when_text_may_overflow_its_own_shape(self) -> None:
|
|
629
1079
|
result = xml_text_overlap_lint.lint_xml(
|
|
630
|
-
"""
|
|
631
|
-
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
632
|
-
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
633
|
-
<data>
|
|
634
|
-
<img src="tok" topLeftX="-120" topLeftY="20" width="360" height="360"/>
|
|
635
|
-
<shape type="text" topLeftX="40" topLeftY="80" width="180" height="80">
|
|
636
|
-
<content textType="title" fontSize="44"><p>Title</p></content>
|
|
637
|
-
</shape>
|
|
638
|
-
<shape type="text" topLeftX="40" topLeftY="120" width="180" height="40">
|
|
639
|
-
<content textType="sub-headline" fontSize="20"><p>Subtitle</p></content>
|
|
640
|
-
</shape>
|
|
641
|
-
</data>
|
|
642
|
-
</slide>
|
|
643
|
-
</presentation>
|
|
644
|
-
"""
|
|
645
|
-
)
|
|
646
|
-
self.assertEqual(result["summary"]["error_count"], 0)
|
|
647
|
-
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
648
|
-
|
|
649
|
-
def test_extract_elements_preserves_supported_element_geometry_order_and_text_metadata(self) -> None:
|
|
650
|
-
elements = xml_text_overlap_lint.extract_elements(
|
|
651
1080
|
"""
|
|
652
1081
|
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
653
1082
|
<data>
|
|
654
|
-
<
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
<p><![CDATA[Growth & scale]]></p>
|
|
658
|
-
<p>Focused execution</p>
|
|
1083
|
+
<shape id="overflowing" type="text" topLeftX="80" topLeftY="80" width="360" height="80">
|
|
1084
|
+
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit">
|
|
1085
|
+
<p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
|
|
659
1086
|
</content>
|
|
660
1087
|
</shape>
|
|
661
|
-
<
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
1088
|
+
<shape id="fitting" type="text" topLeftX="480" topLeftY="80" width="360" height="120">
|
|
1089
|
+
<content fontSize="20" lineSpacing="multiple:1.5">
|
|
1090
|
+
<p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
|
|
1091
|
+
</content>
|
|
1092
|
+
</shape>
|
|
1093
|
+
<shape id="auto-fit" type="text" topLeftX="80" topLeftY="240" width="360" height="80">
|
|
1094
|
+
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="normal-auto-fit">
|
|
1095
|
+
<p>第一段</p><p>第二段</p><p>第三段</p><p>第四段</p>
|
|
1096
|
+
</content>
|
|
1097
|
+
</shape>
|
|
1098
|
+
<shape id="shape-auto-fit" type="text" topLeftX="480" topLeftY="240" width="360" height="30">
|
|
1099
|
+
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="shape-auto-fit">
|
|
1100
|
+
<p>第一段</p><p>第二段</p><p>第三段</p>
|
|
1101
|
+
</content>
|
|
666
1102
|
</shape>
|
|
667
1103
|
</data>
|
|
668
1104
|
</slide>
|
|
669
1105
|
"""
|
|
670
1106
|
)
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
self.assertEqual([
|
|
674
|
-
|
|
675
|
-
self.
|
|
676
|
-
self.
|
|
677
|
-
self.
|
|
678
|
-
self.
|
|
679
|
-
|
|
1107
|
+
issues = result["slides"][0]["issues"]
|
|
1108
|
+
overflow_issues = [issue for issue in issues if issue["code"] == "text_may_overflow_shape"]
|
|
1109
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1110
|
+
overflow_ids = {issue["elements"][0] for issue in overflow_issues}
|
|
1111
|
+
self.assertIn("overflowing", overflow_ids)
|
|
1112
|
+
self.assertNotIn("auto-fit", overflow_ids)
|
|
1113
|
+
self.assertNotIn("shape-auto-fit", overflow_ids)
|
|
1114
|
+
self.assertNotIn("fitting", overflow_ids)
|
|
1115
|
+
overflowing_issue = next(issue for issue in overflow_issues if issue["elements"] == ["overflowing"])
|
|
1116
|
+
self.assertEqual(overflowing_issue["line_count"], 4)
|
|
1117
|
+
self.assertEqual(overflowing_issue["estimated_height"], 110)
|
|
1118
|
+
self.assertEqual(overflowing_issue["available_height"], 80)
|
|
1119
|
+
self.assertEqual(overflowing_issue["overflow"], 30)
|
|
1120
|
+
self.assertIn('wrap="true" autoFit="normal-auto-fit"', overflowing_issue["message"])
|
|
680
1121
|
|
|
681
|
-
def
|
|
1122
|
+
def test_lint_xml_uses_fixed_line_spacing_for_text_height_warning(self) -> None:
|
|
682
1123
|
result = xml_text_overlap_lint.lint_xml(
|
|
683
1124
|
"""
|
|
684
|
-
<
|
|
685
|
-
<
|
|
686
|
-
<
|
|
687
|
-
<
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1125
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1126
|
+
<data>
|
|
1127
|
+
<shape id="fixed-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="50">
|
|
1128
|
+
<content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
|
|
1129
|
+
<p>第一段</p><p>第二段</p><p>第三段</p>
|
|
1130
|
+
</content>
|
|
1131
|
+
</shape>
|
|
1132
|
+
</data>
|
|
1133
|
+
</slide>
|
|
691
1134
|
"""
|
|
692
1135
|
)
|
|
1136
|
+
issue = result["slides"][0]["issues"][0]
|
|
1137
|
+
self.assertEqual(result["summary"]["warning_count"], 1)
|
|
693
1138
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1139
|
+
self.assertEqual(issue["level"], "warning")
|
|
1140
|
+
self.assertEqual(issue["line_height"], 20)
|
|
1141
|
+
self.assertEqual(issue["estimated_height"], 60)
|
|
1142
|
+
self.assertEqual(issue["overflow"], 10)
|
|
694
1143
|
|
|
695
|
-
def
|
|
1144
|
+
def test_lint_xml_ignores_subpixel_text_height_overflow_tolerance(self) -> None:
|
|
696
1145
|
result = xml_text_overlap_lint.lint_xml(
|
|
697
1146
|
"""
|
|
698
|
-
<
|
|
699
|
-
<
|
|
700
|
-
<
|
|
701
|
-
<
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1147
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1148
|
+
<data>
|
|
1149
|
+
<shape id="minor-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="39.8">
|
|
1150
|
+
<content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
|
|
1151
|
+
<p>第一段</p><p>第二段</p>
|
|
1152
|
+
</content>
|
|
1153
|
+
</shape>
|
|
1154
|
+
</data>
|
|
1155
|
+
</slide>
|
|
706
1156
|
"""
|
|
707
1157
|
)
|
|
708
|
-
|
|
1158
|
+
overflow_issues = [
|
|
1159
|
+
issue
|
|
1160
|
+
for issue in result["slides"][0]["issues"]
|
|
1161
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1162
|
+
]
|
|
1163
|
+
self.assertEqual(overflow_issues, [])
|
|
709
1164
|
|
|
710
|
-
def
|
|
1165
|
+
def test_lint_xml_allows_single_line_width_estimation_jitter(self) -> None:
|
|
711
1166
|
result = xml_text_overlap_lint.lint_xml(
|
|
712
1167
|
"""
|
|
713
|
-
<
|
|
714
|
-
<
|
|
715
|
-
<
|
|
716
|
-
<
|
|
717
|
-
</
|
|
718
|
-
</
|
|
719
|
-
</
|
|
1168
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1169
|
+
<data>
|
|
1170
|
+
<shape id="metric" type="text" topLeftX="80" topLeftY="80" width="152" height="54">
|
|
1171
|
+
<content fontSize="36" lineSpacing="multiple:1.2" autoFit="no-auto-fit"><p>4.16万亿</p></content>
|
|
1172
|
+
</shape>
|
|
1173
|
+
</data>
|
|
1174
|
+
</slide>
|
|
720
1175
|
"""
|
|
721
1176
|
)
|
|
722
|
-
|
|
1177
|
+
overflow_issues = [
|
|
1178
|
+
issue
|
|
1179
|
+
for issue in result["slides"][0]["issues"]
|
|
1180
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1181
|
+
]
|
|
1182
|
+
self.assertEqual(overflow_issues, [])
|
|
723
1183
|
|
|
724
|
-
def
|
|
1184
|
+
def test_lint_xml_allows_short_metric_text_with_separators_as_single_line(self) -> None:
|
|
725
1185
|
result = xml_text_overlap_lint.lint_xml(
|
|
726
1186
|
"""
|
|
727
|
-
<
|
|
728
|
-
<
|
|
729
|
-
<
|
|
730
|
-
<
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
<
|
|
734
|
-
</
|
|
735
|
-
</
|
|
736
|
-
</
|
|
1187
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1188
|
+
<data>
|
|
1189
|
+
<shape id="metric" type="text" topLeftX="80" topLeftY="80" width="150" height="50">
|
|
1190
|
+
<content textType="title" fontSize="36" autoFit="no-auto-fit"><p>4.16万亿</p></content>
|
|
1191
|
+
</shape>
|
|
1192
|
+
<shape id="table-number" type="text" topLeftX="80" topLeftY="160" width="25" height="20">
|
|
1193
|
+
<content fontSize="10" textAlign="center" autoFit="no-auto-fit"><p>1,380</p></content>
|
|
1194
|
+
</shape>
|
|
1195
|
+
</data>
|
|
1196
|
+
</slide>
|
|
737
1197
|
"""
|
|
738
1198
|
)
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
("chart_out_of_canvas", ["outside-chart"], {"left": 0, "top": 0, "right": 40, "bottom": 0}),
|
|
746
|
-
],
|
|
747
|
-
)
|
|
1199
|
+
overflow_issues = [
|
|
1200
|
+
issue
|
|
1201
|
+
for issue in result["slides"][0]["issues"]
|
|
1202
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1203
|
+
]
|
|
1204
|
+
self.assertEqual(overflow_issues, [])
|
|
748
1205
|
|
|
749
|
-
def
|
|
1206
|
+
def test_lint_xml_reports_plain_short_metric_when_it_wraps(self) -> None:
|
|
750
1207
|
result = xml_text_overlap_lint.lint_xml(
|
|
751
1208
|
"""
|
|
752
|
-
<
|
|
753
|
-
<
|
|
754
|
-
<
|
|
755
|
-
<
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
</presentation>
|
|
1209
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1210
|
+
<data>
|
|
1211
|
+
<shape id="plain-age" type="text" topLeftX="80" topLeftY="80" width="50" height="80">
|
|
1212
|
+
<content textType="title" fontSize="36" bold="true" autoFit="no-auto-fit"><p>82岁</p></content>
|
|
1213
|
+
</shape>
|
|
1214
|
+
</data>
|
|
1215
|
+
</slide>
|
|
760
1216
|
"""
|
|
761
1217
|
)
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
self.assertEqual(
|
|
768
|
-
self.
|
|
1218
|
+
overflow_issues = [
|
|
1219
|
+
issue
|
|
1220
|
+
for issue in result["slides"][0]["issues"]
|
|
1221
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1222
|
+
]
|
|
1223
|
+
self.assertEqual(len(overflow_issues), 1)
|
|
1224
|
+
self.assertEqual(overflow_issues[0]["elements"], ["plain-age"])
|
|
769
1225
|
|
|
770
|
-
def
|
|
1226
|
+
def test_lint_xml_allows_centered_short_label_near_fit_as_single_line(self) -> None:
|
|
771
1227
|
result = xml_text_overlap_lint.lint_xml(
|
|
772
1228
|
"""
|
|
773
|
-
<
|
|
774
|
-
<
|
|
775
|
-
<
|
|
776
|
-
<
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
</
|
|
780
|
-
</
|
|
781
|
-
</
|
|
1229
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1230
|
+
<data>
|
|
1231
|
+
<shape id="centered-label" type="text" topLeftX="80" topLeftY="80" width="200" height="30">
|
|
1232
|
+
<content fontSize="14" bold="true" textAlign="center" autoFit="no-auto-fit">
|
|
1233
|
+
<p>参数服务器 (Parameter Server)</p>
|
|
1234
|
+
</content>
|
|
1235
|
+
</shape>
|
|
1236
|
+
</data>
|
|
1237
|
+
</slide>
|
|
782
1238
|
"""
|
|
783
1239
|
)
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1240
|
+
overflow_issues = [
|
|
1241
|
+
issue
|
|
1242
|
+
for issue in result["slides"][0]["issues"]
|
|
1243
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1244
|
+
]
|
|
1245
|
+
self.assertEqual(overflow_issues, [])
|
|
789
1246
|
|
|
790
|
-
def
|
|
1247
|
+
def test_lint_xml_allows_headline_near_fit_as_single_line(self) -> None:
|
|
791
1248
|
result = xml_text_overlap_lint.lint_xml(
|
|
792
1249
|
"""
|
|
793
|
-
<
|
|
794
|
-
<
|
|
795
|
-
<
|
|
796
|
-
<
|
|
797
|
-
<
|
|
798
|
-
</
|
|
799
|
-
</
|
|
800
|
-
</
|
|
801
|
-
</
|
|
1250
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1251
|
+
<data>
|
|
1252
|
+
<shape id="headline" type="text" topLeftX="80" topLeftY="80" width="700" height="50">
|
|
1253
|
+
<content textType="headline" fontSize="26" bold="true" lineSpacing="multiple:1.3" autoFit="no-auto-fit">
|
|
1254
|
+
<p>全球半导体市场规模持续高速增长,AI驱动新一轮景气周期</p>
|
|
1255
|
+
</content>
|
|
1256
|
+
</shape>
|
|
1257
|
+
</data>
|
|
1258
|
+
</slide>
|
|
802
1259
|
"""
|
|
803
1260
|
)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
self.assertEqual(
|
|
1261
|
+
overflow_issues = [
|
|
1262
|
+
issue
|
|
1263
|
+
for issue in result["slides"][0]["issues"]
|
|
1264
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1265
|
+
]
|
|
1266
|
+
self.assertEqual(overflow_issues, [])
|
|
810
1267
|
|
|
811
|
-
def
|
|
1268
|
+
def test_lint_xml_allows_dense_body_line_spacing_estimation_slack(self) -> None:
|
|
812
1269
|
result = xml_text_overlap_lint.lint_xml(
|
|
813
1270
|
"""
|
|
814
|
-
<
|
|
815
|
-
<
|
|
816
|
-
<
|
|
817
|
-
<
|
|
818
|
-
<
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1271
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1272
|
+
<data>
|
|
1273
|
+
<shape id="dense-body" type="text" topLeftX="80" topLeftY="80" width="360" height="140">
|
|
1274
|
+
<content fontSize="13" bold="true" lineSpacing="multiple:1.7" autoFit="no-auto-fit">
|
|
1275
|
+
<p>总体目标:</p>
|
|
1276
|
+
<p>建立深度神经网络高效训练的统一理论框架,实现训练效率与模型性能的协同优化。</p>
|
|
1277
|
+
<p>具体目标:</p>
|
|
1278
|
+
<p>提出自适应优化算法,收敛速度提升 2-3 倍</p>
|
|
1279
|
+
<p>实现结构化压缩方法,模型体积减少 10 倍以上</p>
|
|
1280
|
+
<p>构建分布式训练策略,64 GPU 加速比 > 50x</p>
|
|
1281
|
+
</content>
|
|
1282
|
+
</shape>
|
|
1283
|
+
</data>
|
|
1284
|
+
</slide>
|
|
823
1285
|
"""
|
|
824
1286
|
)
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1287
|
+
overflow_issues = [
|
|
1288
|
+
issue
|
|
1289
|
+
for issue in result["slides"][0]["issues"]
|
|
1290
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1291
|
+
]
|
|
1292
|
+
self.assertEqual(overflow_issues, [])
|
|
829
1293
|
|
|
830
|
-
def
|
|
1294
|
+
def test_lint_xml_reports_dense_body_when_adjusted_height_still_overflows(self) -> None:
|
|
831
1295
|
result = xml_text_overlap_lint.lint_xml(
|
|
832
1296
|
"""
|
|
833
|
-
<
|
|
834
|
-
<
|
|
835
|
-
<
|
|
836
|
-
<
|
|
837
|
-
<
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1297
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1298
|
+
<data>
|
|
1299
|
+
<shape id="dense-body" type="text" topLeftX="80" topLeftY="80" width="360" height="100">
|
|
1300
|
+
<content fontSize="13" bold="true" lineSpacing="multiple:1.7" autoFit="no-auto-fit">
|
|
1301
|
+
<p>总体目标:</p>
|
|
1302
|
+
<p>建立深度神经网络高效训练的统一理论框架,实现训练效率与模型性能的协同优化。</p>
|
|
1303
|
+
<p>具体目标:</p>
|
|
1304
|
+
<p>提出自适应优化算法,收敛速度提升 2-3 倍</p>
|
|
1305
|
+
<p>实现结构化压缩方法,模型体积减少 10 倍以上</p>
|
|
1306
|
+
<p>构建分布式训练策略,64 GPU 加速比 > 50x</p>
|
|
1307
|
+
</content>
|
|
1308
|
+
</shape>
|
|
1309
|
+
</data>
|
|
1310
|
+
</slide>
|
|
842
1311
|
"""
|
|
843
1312
|
)
|
|
844
|
-
|
|
845
|
-
|
|
1313
|
+
overflow_issues = [
|
|
1314
|
+
issue
|
|
1315
|
+
for issue in result["slides"][0]["issues"]
|
|
1316
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1317
|
+
]
|
|
1318
|
+
self.assertEqual(len(overflow_issues), 1)
|
|
1319
|
+
self.assertEqual(overflow_issues[0]["elements"], ["dense-body"])
|
|
846
1320
|
|
|
847
|
-
def
|
|
1321
|
+
def test_lint_xml_reports_letter_spaced_caption_near_fit(self) -> None:
|
|
848
1322
|
result = xml_text_overlap_lint.lint_xml(
|
|
849
1323
|
"""
|
|
850
|
-
<
|
|
851
|
-
<
|
|
852
|
-
<
|
|
853
|
-
<
|
|
854
|
-
<
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
</slide>
|
|
860
|
-
</presentation>
|
|
1324
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1325
|
+
<data>
|
|
1326
|
+
<shape id="caption" type="text" topLeftX="80" topLeftY="80" width="120" height="20">
|
|
1327
|
+
<content textType="caption" fontSize="11" letterSpacing="1" autoFit="no-auto-fit">
|
|
1328
|
+
<p>RISKS & CHALLENGES</p>
|
|
1329
|
+
</content>
|
|
1330
|
+
</shape>
|
|
1331
|
+
</data>
|
|
1332
|
+
</slide>
|
|
861
1333
|
"""
|
|
862
1334
|
)
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1335
|
+
overflow_issues = [
|
|
1336
|
+
issue
|
|
1337
|
+
for issue in result["slides"][0]["issues"]
|
|
1338
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1339
|
+
]
|
|
1340
|
+
self.assertEqual(len(overflow_issues), 1)
|
|
1341
|
+
self.assertEqual(overflow_issues[0]["elements"], ["caption"])
|
|
868
1342
|
|
|
869
|
-
def
|
|
1343
|
+
def test_lint_xml_reports_micro_caption_when_wrapping_overflows(self) -> None:
|
|
870
1344
|
result = xml_text_overlap_lint.lint_xml(
|
|
871
1345
|
"""
|
|
872
|
-
<
|
|
873
|
-
<
|
|
874
|
-
<
|
|
875
|
-
<
|
|
876
|
-
<
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
</presentation>
|
|
1346
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1347
|
+
<data>
|
|
1348
|
+
<shape id="micro-caption" type="text" topLeftX="80" topLeftY="60" width="200" height="16">
|
|
1349
|
+
<content textType="caption" fontSize="3" lineSpacing="multiple:1.3" letterSpacing="160" autoFit="no-auto-fit">
|
|
1350
|
+
<p>MARKET INSIGHT · 市场洞察</p>
|
|
1351
|
+
</content>
|
|
1352
|
+
</shape>
|
|
1353
|
+
</data>
|
|
1354
|
+
</slide>
|
|
882
1355
|
"""
|
|
883
1356
|
)
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
self.assertEqual(
|
|
890
|
-
self.assertEqual(
|
|
891
|
-
self.assertEqual(mismatch_issue["resolved_size"], canvas_issue["bbox"]["width"])
|
|
1357
|
+
overflow_issues = [
|
|
1358
|
+
issue
|
|
1359
|
+
for issue in result["slides"][0]["issues"]
|
|
1360
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1361
|
+
]
|
|
1362
|
+
self.assertEqual(len(overflow_issues), 1)
|
|
1363
|
+
self.assertEqual(overflow_issues[0]["elements"], ["micro-caption"])
|
|
892
1364
|
|
|
893
|
-
def
|
|
1365
|
+
def test_lint_xml_text_may_overflow_shape_upgrades_to_error_above_threshold(self) -> None:
|
|
894
1366
|
result = xml_text_overlap_lint.lint_xml(
|
|
895
1367
|
"""
|
|
896
|
-
<
|
|
897
|
-
<
|
|
898
|
-
<
|
|
899
|
-
<
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1368
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1369
|
+
<data>
|
|
1370
|
+
<shape id="just-warning" type="text" topLeftX="80" topLeftY="80" width="360" height="50">
|
|
1371
|
+
<content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
|
|
1372
|
+
<p>第一段</p><p>第二段</p><p>第三段</p>
|
|
1373
|
+
</content>
|
|
1374
|
+
</shape>
|
|
1375
|
+
<shape id="error-overflow" type="text" topLeftX="80" topLeftY="200" width="360" height="30">
|
|
1376
|
+
<content fontSize="20" lineSpacing="fixed:20" autoFit="no-auto-fit">
|
|
1377
|
+
<p>第一段</p><p>第二段</p><p>第三段</p>
|
|
1378
|
+
</content>
|
|
1379
|
+
</shape>
|
|
1380
|
+
</data>
|
|
1381
|
+
</slide>
|
|
908
1382
|
"""
|
|
909
1383
|
)
|
|
910
|
-
issues = result["slides"][0]["issues"]
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
self.assertEqual(
|
|
914
|
-
self.assertEqual(
|
|
1384
|
+
issues = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
|
|
1385
|
+
self.assertEqual(issues["just-warning"]["level"], "warning")
|
|
1386
|
+
self.assertEqual(issues["just-warning"]["overflow"], 10)
|
|
1387
|
+
self.assertEqual(issues["error-overflow"]["level"], "error")
|
|
1388
|
+
self.assertEqual(issues["error-overflow"]["overflow"], 30)
|
|
1389
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1390
|
+
self.assertEqual(result["summary"]["warning_count"], 1)
|
|
915
1391
|
|
|
916
|
-
def
|
|
1392
|
+
def test_lint_xml_text_may_overflow_shape_downgrades_background_decoration_to_info(self) -> None:
|
|
917
1393
|
result = xml_text_overlap_lint.lint_xml(
|
|
918
1394
|
"""
|
|
919
|
-
<
|
|
920
|
-
<
|
|
921
|
-
<
|
|
922
|
-
<
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
</presentation>
|
|
1395
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1396
|
+
<data>
|
|
1397
|
+
<shape id="bg-deco" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
|
|
1398
|
+
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
|
|
1399
|
+
</shape>
|
|
1400
|
+
<shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
|
|
1401
|
+
<content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
|
|
1402
|
+
</shape>
|
|
1403
|
+
</data>
|
|
1404
|
+
</slide>
|
|
930
1405
|
"""
|
|
931
1406
|
)
|
|
932
|
-
|
|
933
|
-
|
|
1407
|
+
issues = {
|
|
1408
|
+
issue["elements"][0]: issue
|
|
1409
|
+
for issue in result["slides"][0]["issues"]
|
|
1410
|
+
if issue["code"] == "text_may_overflow_shape"
|
|
1411
|
+
}
|
|
1412
|
+
self.assertEqual(issues["bg-deco"]["level"], "info")
|
|
934
1413
|
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
935
|
-
self.assertEqual(result["summary"]["info_count"],
|
|
936
|
-
self.assertEqual(
|
|
937
|
-
self.
|
|
938
|
-
self.assertEqual(issues_by_dimension["width"]["resolved_sizes"], [100, 100, 50])
|
|
939
|
-
self.assertEqual(issues_by_dimension["width"]["resolved_size"], 250)
|
|
940
|
-
self.assertEqual(issues_by_dimension["height"]["resolved_sizes"], [40, 60])
|
|
941
|
-
self.assertEqual(issues_by_dimension["height"]["resolved_size"], 100)
|
|
1414
|
+
self.assertEqual(result["summary"]["info_count"], 1)
|
|
1415
|
+
self.assertEqual(result["slides"][0]["infos"], [issues["bg-deco"]])
|
|
1416
|
+
self.assertIn("background decoration", issues["bg-deco"]["message"])
|
|
942
1417
|
|
|
943
|
-
def
|
|
1418
|
+
def test_lint_xml_reports_shape_alpha_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
|
|
944
1419
|
result = xml_text_overlap_lint.lint_xml(
|
|
945
1420
|
"""
|
|
946
|
-
<
|
|
947
|
-
<
|
|
948
|
-
<
|
|
949
|
-
<
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
</presentation>
|
|
1421
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1422
|
+
<data>
|
|
1423
|
+
<shape id="ghost-number" type="text" topLeftX="-60" topLeftY="30" width="360" height="180" alpha="0.2">
|
|
1424
|
+
<content fontSize="160" lineSpacing="fixed:160" wrap="false"><p>01</p></content>
|
|
1425
|
+
</shape>
|
|
1426
|
+
<shape id="title" type="text" topLeftX="80" topLeftY="80" width="360" height="80">
|
|
1427
|
+
<content fontSize="30" lineSpacing="fixed:36"><p>Annual Review</p></content>
|
|
1428
|
+
</shape>
|
|
1429
|
+
</data>
|
|
1430
|
+
</slide>
|
|
957
1431
|
"""
|
|
958
1432
|
)
|
|
959
|
-
|
|
960
|
-
self.assertEqual(result["summary"]["
|
|
961
|
-
self.
|
|
962
|
-
self.
|
|
1433
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
1434
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1435
|
+
self.assertIn("shape_out_of_canvas", codes)
|
|
1436
|
+
self.assertNotIn("bbox_overlap", codes)
|
|
963
1437
|
|
|
964
|
-
def
|
|
1438
|
+
def test_lint_xml_reports_content_color_alpha_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
|
|
965
1439
|
result = xml_text_overlap_lint.lint_xml(
|
|
966
1440
|
"""
|
|
967
|
-
<
|
|
968
|
-
<
|
|
969
|
-
<
|
|
970
|
-
<
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
</
|
|
974
|
-
</
|
|
975
|
-
</
|
|
976
|
-
</
|
|
1441
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1442
|
+
<data>
|
|
1443
|
+
<shape id="ghost-year" type="text" topLeftX="760" topLeftY="20" width="260" height="160">
|
|
1444
|
+
<content fontSize="140" color="rgba(0,0,0,0.2)" lineSpacing="fixed:140" wrap="false"><p>2026</p></content>
|
|
1445
|
+
</shape>
|
|
1446
|
+
<shape id="headline" type="text" topLeftX="700" topLeftY="70" width="220" height="80">
|
|
1447
|
+
<content fontSize="28" lineSpacing="fixed:34"><p>Forecast</p></content>
|
|
1448
|
+
</shape>
|
|
1449
|
+
</data>
|
|
1450
|
+
</slide>
|
|
977
1451
|
"""
|
|
978
1452
|
)
|
|
979
|
-
|
|
980
|
-
self.assertEqual(
|
|
981
|
-
self.
|
|
982
|
-
self.
|
|
1453
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
1454
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1455
|
+
self.assertIn("shape_out_of_canvas", codes)
|
|
1456
|
+
self.assertNotIn("bbox_overlap", codes)
|
|
983
1457
|
|
|
984
|
-
def
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1458
|
+
def test_lint_xml_reports_faint_medium_ghost_text_out_of_canvas_but_allows_overlap(self) -> None:
|
|
1459
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1460
|
+
"""
|
|
1461
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1462
|
+
<data>
|
|
1463
|
+
<shape id="medium-ghost" type="text" topLeftX="820" topLeftY="300" width="270" height="72" alpha="0.32">
|
|
1464
|
+
<content fontSize="40" lineSpacing="fixed:40" wrap="false"><p>OFF EDGE</p></content>
|
|
1465
|
+
</shape>
|
|
1466
|
+
<shape id="caption" type="text" topLeftX="760" topLeftY="315" width="180" height="36">
|
|
1467
|
+
<content fontSize="16" lineSpacing="fixed:20"><p>Readable caption</p></content>
|
|
1468
|
+
</shape>
|
|
1469
|
+
</data>
|
|
1470
|
+
</slide>
|
|
1471
|
+
"""
|
|
1472
|
+
)
|
|
1473
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
1474
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1475
|
+
self.assertIn("shape_out_of_canvas", codes)
|
|
1476
|
+
self.assertNotIn("bbox_overlap", codes)
|
|
988
1477
|
|
|
989
|
-
def
|
|
990
|
-
|
|
991
|
-
"
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
<
|
|
996
|
-
</
|
|
997
|
-
"""
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
"""
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
script_path = Path(xml_text_overlap_lint.__file__).resolve()
|
|
1029
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
|
1030
|
-
for name, (table_xml, expected_info_count) in cases.items():
|
|
1031
|
-
with self.subTest(case=name):
|
|
1032
|
-
input_path = Path(temp_dir) / f"{name}.xml"
|
|
1033
|
-
input_path.write_text(
|
|
1034
|
-
f"""
|
|
1035
|
-
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
1036
|
-
<slide xmlns="http://www.larkoffice.com/sml/2.0"><data>{table_xml}</data></slide>
|
|
1037
|
-
</presentation>
|
|
1038
|
-
""",
|
|
1039
|
-
encoding="utf-8",
|
|
1040
|
-
)
|
|
1041
|
-
completed = subprocess.run(
|
|
1042
|
-
[sys.executable, str(script_path), "--input", str(input_path)],
|
|
1043
|
-
capture_output=True,
|
|
1044
|
-
check=False,
|
|
1045
|
-
text=True,
|
|
1046
|
-
)
|
|
1047
|
-
result = json.loads(completed.stdout)
|
|
1048
|
-
self.assertEqual(completed.returncode, 0, completed.stderr)
|
|
1049
|
-
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1050
|
-
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1051
|
-
self.assertEqual(result["summary"]["info_count"], expected_info_count)
|
|
1052
|
-
self.assertTrue(
|
|
1053
|
-
all(issue["level"] == "info" for issue in result["slides"][0]["issues"]),
|
|
1054
|
-
result["slides"][0]["issues"],
|
|
1055
|
-
)
|
|
1478
|
+
def test_lint_xml_allows_ghost_text_image_overlap(self) -> None:
|
|
1479
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1480
|
+
"""
|
|
1481
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1482
|
+
<data>
|
|
1483
|
+
<shape id="ghost-label" type="text" topLeftX="100" topLeftY="40" width="560" height="160" alpha="0.2">
|
|
1484
|
+
<content fontSize="120" lineSpacing="fixed:120" wrap="false"><p>2026</p></content>
|
|
1485
|
+
</shape>
|
|
1486
|
+
<img id="photo" src="token" topLeftX="160" topLeftY="70" width="260" height="160"/>
|
|
1487
|
+
<shape id="title" type="text" topLeftX="610" topLeftY="95" width="320" height="60">
|
|
1488
|
+
<content fontSize="28" lineSpacing="fixed:34"><p>Annual Review</p></content>
|
|
1489
|
+
</shape>
|
|
1490
|
+
</data>
|
|
1491
|
+
</slide>
|
|
1492
|
+
"""
|
|
1493
|
+
)
|
|
1494
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
1495
|
+
self.assertNotIn("image_covers_text", codes)
|
|
1496
|
+
self.assertNotIn("bbox_overlap", codes)
|
|
1497
|
+
|
|
1498
|
+
def test_lint_slide_allows_ghost_text_whiteboard_overlap(self) -> None:
|
|
1499
|
+
result = xml_text_overlap_lint.lint_slide(
|
|
1500
|
+
"""
|
|
1501
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1502
|
+
<data>
|
|
1503
|
+
<whiteboard id="board" topLeftX="180" topLeftY="70" width="420" height="300"/>
|
|
1504
|
+
<shape id="ghost-label" type="text" topLeftX="100" topLeftY="40" width="560" height="160" alpha="0.2">
|
|
1505
|
+
<content fontSize="120" lineSpacing="fixed:120" wrap="false"><p>2026</p></content>
|
|
1506
|
+
</shape>
|
|
1507
|
+
<shape id="title" type="text" topLeftX="610" topLeftY="95" width="220" height="60">
|
|
1508
|
+
<content fontSize="28" lineSpacing="fixed:34"><p>Annual Review</p></content>
|
|
1509
|
+
</shape>
|
|
1510
|
+
</data>
|
|
1511
|
+
</slide>
|
|
1512
|
+
""",
|
|
1513
|
+
1,
|
|
1514
|
+
)
|
|
1515
|
+
codes = [issue["code"] for issue in result["issues"]]
|
|
1516
|
+
self.assertNotIn("whiteboard_external_overlap", codes)
|
|
1056
1517
|
|
|
1057
|
-
def
|
|
1518
|
+
def test_lint_xml_reports_faint_ghost_text_out_of_canvas_without_area_threshold(self) -> None:
|
|
1058
1519
|
result = xml_text_overlap_lint.lint_xml(
|
|
1059
1520
|
"""
|
|
1060
|
-
<
|
|
1061
|
-
<
|
|
1062
|
-
<
|
|
1063
|
-
<
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
</shape>
|
|
1068
|
-
<shape id="label" type="text" topLeftX="65" topLeftY="460" width="170" height="20">
|
|
1069
|
-
<content fontSize="12"><p>基础保障</p></content>
|
|
1070
|
-
</shape>
|
|
1071
|
-
</data>
|
|
1072
|
-
</slide>
|
|
1073
|
-
</presentation>
|
|
1521
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1522
|
+
<data>
|
|
1523
|
+
<shape id="small-ghost" type="text" topLeftX="940" topLeftY="300" width="40" height="40" alpha="0.32">
|
|
1524
|
+
<content fontSize="36" lineSpacing="fixed:36" wrap="false"><p>土</p></content>
|
|
1525
|
+
</shape>
|
|
1526
|
+
</data>
|
|
1527
|
+
</slide>
|
|
1074
1528
|
"""
|
|
1075
1529
|
)
|
|
1076
|
-
|
|
1077
|
-
self.assertEqual(result["
|
|
1078
|
-
self.assertEqual(result["summary"]["warning_count"], 1)
|
|
1079
|
-
self.assertEqual(issues[0]["code"], "whiteboard_external_overlap")
|
|
1080
|
-
self.assertIn("static whiteboard container-bbox risk", issues[0]["hint"])
|
|
1081
|
-
self.assertIn("screenshot QA", issues[0]["hint"])
|
|
1082
|
-
self.assertEqual(issues[0]["elements"], ["wb", "card"])
|
|
1083
|
-
self.assertEqual(issues[0]["overlaps"][0]["overlap_height"], 50)
|
|
1530
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1531
|
+
self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
|
|
1084
1532
|
|
|
1085
|
-
def
|
|
1533
|
+
def test_lint_xml_keeps_out_of_canvas_error_for_medium_text_without_faint_alpha(self) -> None:
|
|
1086
1534
|
result = xml_text_overlap_lint.lint_xml(
|
|
1087
1535
|
"""
|
|
1088
|
-
<
|
|
1089
|
-
<
|
|
1090
|
-
<
|
|
1091
|
-
<
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
</data>
|
|
1096
|
-
</slide>
|
|
1097
|
-
</presentation>
|
|
1536
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1537
|
+
<data>
|
|
1538
|
+
<shape id="medium-not-ghost" type="text" topLeftX="820" topLeftY="300" width="270" height="72" alpha="0.36">
|
|
1539
|
+
<content fontSize="54" lineSpacing="fixed:54" wrap="false"><p>OFF EDGE</p></content>
|
|
1540
|
+
</shape>
|
|
1541
|
+
</data>
|
|
1542
|
+
</slide>
|
|
1098
1543
|
"""
|
|
1099
1544
|
)
|
|
1100
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
1101
|
-
self.assertEqual(result["
|
|
1545
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1546
|
+
self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
|
|
1547
|
+
self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["medium-not-ghost"])
|
|
1102
1548
|
|
|
1103
|
-
def
|
|
1549
|
+
def test_lint_xml_keeps_out_of_canvas_error_for_half_alpha_large_text(self) -> None:
|
|
1104
1550
|
result = xml_text_overlap_lint.lint_xml(
|
|
1105
1551
|
"""
|
|
1106
|
-
<
|
|
1107
|
-
<
|
|
1108
|
-
<
|
|
1109
|
-
<
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
</data>
|
|
1114
|
-
</slide>
|
|
1115
|
-
</presentation>
|
|
1552
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1553
|
+
<data>
|
|
1554
|
+
<shape id="half-alpha" type="text" topLeftX="760" topLeftY="20" width="260" height="160">
|
|
1555
|
+
<content fontSize="140" color="rgba(0,0,0,0.5)" lineSpacing="fixed:140" wrap="false"><p>2026</p></content>
|
|
1556
|
+
</shape>
|
|
1557
|
+
</data>
|
|
1558
|
+
</slide>
|
|
1116
1559
|
"""
|
|
1117
1560
|
)
|
|
1118
|
-
self.assertEqual(result["summary"]["error_count"],
|
|
1119
|
-
self.assertEqual(result["
|
|
1561
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
1562
|
+
self.assertEqual(result["slides"][0]["issues"][0]["code"], "shape_out_of_canvas")
|
|
1563
|
+
self.assertEqual(result["slides"][0]["issues"][0]["elements"], ["half-alpha"])
|
|
1564
|
+
|
|
1565
|
+
def test_lint_xml_text_may_overflow_shape_keeps_error_when_alpha_not_low(self) -> None:
|
|
1566
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1567
|
+
"""
|
|
1568
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1569
|
+
<data>
|
|
1570
|
+
<shape id="opaque-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.9">
|
|
1571
|
+
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
|
|
1572
|
+
</shape>
|
|
1573
|
+
<shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
|
|
1574
|
+
<content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
|
|
1575
|
+
</shape>
|
|
1576
|
+
</data>
|
|
1577
|
+
</slide>
|
|
1578
|
+
"""
|
|
1579
|
+
)
|
|
1580
|
+
issue = next(
|
|
1581
|
+
issue
|
|
1582
|
+
for issue in result["slides"][0]["issues"]
|
|
1583
|
+
if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["opaque-big"]
|
|
1584
|
+
)
|
|
1585
|
+
self.assertEqual(issue["level"], "error")
|
|
1586
|
+
|
|
1587
|
+
def test_lint_xml_text_may_overflow_shape_keeps_error_when_no_foreground_text(self) -> None:
|
|
1588
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1589
|
+
"""
|
|
1590
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1591
|
+
<data>
|
|
1592
|
+
<shape id="lonely-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
|
|
1593
|
+
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
|
|
1594
|
+
</shape>
|
|
1595
|
+
</data>
|
|
1596
|
+
</slide>
|
|
1597
|
+
"""
|
|
1598
|
+
)
|
|
1599
|
+
issue = next(
|
|
1600
|
+
issue
|
|
1601
|
+
for issue in result["slides"][0]["issues"]
|
|
1602
|
+
if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["lonely-big"]
|
|
1603
|
+
)
|
|
1604
|
+
self.assertEqual(issue["level"], "error")
|
|
1605
|
+
|
|
1606
|
+
def test_lint_xml_text_may_overflow_shape_keeps_error_when_foreground_alpha_zero(self) -> None:
|
|
1607
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1608
|
+
"""
|
|
1609
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1610
|
+
<data>
|
|
1611
|
+
<shape id="bg-deco" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
|
|
1612
|
+
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
|
|
1613
|
+
</shape>
|
|
1614
|
+
<shape id="transparent-foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60" alpha="0">
|
|
1615
|
+
<content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
|
|
1616
|
+
</shape>
|
|
1617
|
+
</data>
|
|
1618
|
+
</slide>
|
|
1619
|
+
"""
|
|
1620
|
+
)
|
|
1621
|
+
issue = next(
|
|
1622
|
+
issue
|
|
1623
|
+
for issue in result["slides"][0]["issues"]
|
|
1624
|
+
if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["bg-deco"]
|
|
1625
|
+
)
|
|
1626
|
+
self.assertEqual(issue["level"], "error")
|
|
1627
|
+
|
|
1628
|
+
def test_lint_xml_text_may_overflow_shape_keeps_error_when_foreground_is_below_in_order(self) -> None:
|
|
1629
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1630
|
+
"""
|
|
1631
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1632
|
+
<data>
|
|
1633
|
+
<shape id="foreground" type="text" topLeftX="40" topLeftY="20" width="400" height="60">
|
|
1634
|
+
<content fontSize="20" lineSpacing="fixed:24"><p>Annual Report</p></content>
|
|
1635
|
+
</shape>
|
|
1636
|
+
<shape id="top-big" type="text" topLeftX="0" topLeftY="0" width="600" height="80" alpha="0.3">
|
|
1637
|
+
<content fontSize="120" lineSpacing="fixed:120" autoFit="no-auto-fit"><p>2026</p></content>
|
|
1638
|
+
</shape>
|
|
1639
|
+
</data>
|
|
1640
|
+
</slide>
|
|
1641
|
+
"""
|
|
1642
|
+
)
|
|
1643
|
+
issue = next(
|
|
1644
|
+
issue
|
|
1645
|
+
for issue in result["slides"][0]["issues"]
|
|
1646
|
+
if issue["code"] == "text_may_overflow_shape" and issue["elements"] == ["top-big"]
|
|
1647
|
+
)
|
|
1648
|
+
self.assertEqual(issue["level"], "error")
|
|
1649
|
+
|
|
1650
|
+
def test_lint_xml_uses_paragraph_spacing_overrides_for_text_height_warning(self) -> None:
|
|
1651
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1652
|
+
"""
|
|
1653
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1654
|
+
<data>
|
|
1655
|
+
<shape id="paragraph-overflow" type="text" topLeftX="80" topLeftY="80" width="360" height="30">
|
|
1656
|
+
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit">
|
|
1657
|
+
<p lineSpacing="fixed:10" beforeLineSpacing="fixed:5" afterLineSpacing="fixed:5">第一行<br/>第二行</p>
|
|
1658
|
+
</content>
|
|
1659
|
+
</shape>
|
|
1660
|
+
<shape id="paragraph-fitting" type="text" topLeftX="480" topLeftY="80" width="360" height="40">
|
|
1661
|
+
<content fontSize="20" lineSpacing="multiple:1.5">
|
|
1662
|
+
<p lineSpacing="fixed:10">第一行<br/>第二行<br/>第三行</p>
|
|
1663
|
+
</content>
|
|
1664
|
+
</shape>
|
|
1665
|
+
</data>
|
|
1666
|
+
</slide>
|
|
1667
|
+
"""
|
|
1668
|
+
)
|
|
1669
|
+
issues = result["slides"][0]["issues"]
|
|
1670
|
+
self.assertEqual(result["summary"]["warning_count"], 1)
|
|
1671
|
+
self.assertEqual(issues[0]["elements"], ["paragraph-overflow"])
|
|
1672
|
+
self.assertEqual(issues[0]["line_count"], 2)
|
|
1673
|
+
self.assertEqual(issues[0]["line_height"], 10)
|
|
1674
|
+
self.assertEqual(issues[0]["estimated_height"], 40)
|
|
1675
|
+
self.assertEqual(issues[0]["overflow"], 10)
|
|
1676
|
+
|
|
1677
|
+
def test_lint_xml_uses_letter_spacing_for_text_overflow_warning(self) -> None:
|
|
1678
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1679
|
+
"""
|
|
1680
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1681
|
+
<data>
|
|
1682
|
+
<shape id="baseline" type="text" topLeftX="0" topLeftY="0" width="120" height="30">
|
|
1683
|
+
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit"><p>一二三四五六</p></content>
|
|
1684
|
+
</shape>
|
|
1685
|
+
<shape id="content-spaced" type="text" topLeftX="200" topLeftY="0" width="120" height="30">
|
|
1686
|
+
<content fontSize="20" lineSpacing="multiple:1.5" letterSpacing="2" autoFit="no-auto-fit"><p>一二三四五六</p></content>
|
|
1687
|
+
</shape>
|
|
1688
|
+
<shape id="paragraph-spaced" type="text" topLeftX="400" topLeftY="0" width="120" height="30">
|
|
1689
|
+
<content fontSize="20" lineSpacing="multiple:1.5" autoFit="no-auto-fit"><p letterSpacing="2">一二三四五六</p></content>
|
|
1690
|
+
</shape>
|
|
1691
|
+
</data>
|
|
1692
|
+
</slide>
|
|
1693
|
+
"""
|
|
1694
|
+
)
|
|
1695
|
+
issues = result["slides"][0]["issues"]
|
|
1696
|
+
overflow_ids = [issue["elements"][0] for issue in issues if issue["code"] == "text_may_overflow_shape"]
|
|
1697
|
+
self.assertNotIn("baseline", overflow_ids)
|
|
1698
|
+
self.assertIn("content-spaced", overflow_ids)
|
|
1699
|
+
self.assertIn("paragraph-spaced", overflow_ids)
|
|
1700
|
+
by_id = {issue["elements"][0]: issue for issue in issues if issue["code"] == "text_may_overflow_shape"}
|
|
1701
|
+
self.assertEqual(by_id["content-spaced"]["line_count"], 2)
|
|
1702
|
+
self.assertEqual(by_id["content-spaced"]["estimated_height"], 50)
|
|
1703
|
+
self.assertEqual(by_id["content-spaced"]["overflow"], 20)
|
|
1704
|
+
self.assertEqual(by_id["paragraph-spaced"]["line_count"], 2)
|
|
1705
|
+
|
|
1706
|
+
def test_strip_xml_paragraphs_preserves_br_as_hard_line_break(self) -> None:
|
|
1707
|
+
self.assertEqual(
|
|
1708
|
+
xml_text_overlap_lint.strip_xml_paragraphs("<p>第一行<br/>第二行<br />第三行</p>"),
|
|
1709
|
+
"第一行\n第二行\n第三行",
|
|
1710
|
+
)
|
|
1120
1711
|
|
|
1121
|
-
def
|
|
1712
|
+
def test_lint_xml_allows_template_style_images_outside_canvas(self) -> None:
|
|
1122
1713
|
result = xml_text_overlap_lint.lint_xml(
|
|
1123
1714
|
"""
|
|
1124
1715
|
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
1125
1716
|
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1126
1717
|
<data>
|
|
1127
|
-
<
|
|
1128
|
-
|
|
1129
|
-
<content
|
|
1718
|
+
<img src="tok" topLeftX="-120" topLeftY="20" width="360" height="360"/>
|
|
1719
|
+
<shape type="text" topLeftX="40" topLeftY="80" width="180" height="80">
|
|
1720
|
+
<content textType="title" fontSize="44"><p>Title</p></content>
|
|
1721
|
+
</shape>
|
|
1722
|
+
<shape type="text" topLeftX="40" topLeftY="120" width="180" height="40">
|
|
1723
|
+
<content textType="sub-headline" fontSize="20"><p>Subtitle</p></content>
|
|
1130
1724
|
</shape>
|
|
1131
|
-
<whiteboard id="wb" topLeftX="60" topLeftY="120" width="840" height="300"/>
|
|
1132
1725
|
</data>
|
|
1133
1726
|
</slide>
|
|
1134
1727
|
</presentation>
|
|
@@ -1137,33 +1730,2066 @@ class XmlTextOverlapLintTest(unittest.TestCase):
|
|
|
1137
1730
|
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1138
1731
|
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
1139
1732
|
|
|
1140
|
-
def
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
<
|
|
1733
|
+
def test_extract_elements_preserves_supported_element_geometry_order_and_text_metadata(self) -> None:
|
|
1734
|
+
elements = xml_text_overlap_lint.extract_elements(
|
|
1735
|
+
"""
|
|
1736
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1737
|
+
<data>
|
|
1738
|
+
<img id="photo" src="tok" topLeftX="10" topLeftY="20" width="100" height="80"/>
|
|
1739
|
+
<shape id="headline" type="text" topLeftX="40" topLeftY="60" width="320" height="90">
|
|
1740
|
+
<content textType="headline" textAlign="center" autoFit="normal-auto-fit" fontSize="28">
|
|
1741
|
+
<p><![CDATA[Growth & scale]]></p>
|
|
1742
|
+
<p>Focused execution</p>
|
|
1743
|
+
</content>
|
|
1744
|
+
</shape>
|
|
1745
|
+
<table id="table" topLeftX="400" topLeftY="60" width="220" height="120"></table>
|
|
1746
|
+
<chart id="chart" topLeftX="640" topLeftY="60" width="220" height="120"/>
|
|
1747
|
+
<whiteboard id="wb" topLeftX="80" topLeftY="220" width="760" height="240"/>
|
|
1748
|
+
<shape id="missing-height" type="text" topLeftX="80" topLeftY="480" width="320">
|
|
1749
|
+
<content><p>Skipped</p></content>
|
|
1750
|
+
</shape>
|
|
1751
|
+
</data>
|
|
1752
|
+
</slide>
|
|
1753
|
+
"""
|
|
1754
|
+
)
|
|
1755
|
+
self.assertEqual([element["id"] for element in elements], ["photo", "headline", "table", "chart", "wb"])
|
|
1756
|
+
self.assertEqual([element["kind"] for element in elements], ["img", "shape", "table", "chart", "whiteboard"])
|
|
1757
|
+
self.assertEqual([element["order"] for element in elements], [0, 1, 2, 3, 4])
|
|
1758
|
+
self.assertEqual(elements[1]["type"], "text")
|
|
1759
|
+
self.assertEqual(elements[1]["textType"], "headline")
|
|
1760
|
+
self.assertEqual(elements[1]["textAlign"], "center")
|
|
1761
|
+
self.assertEqual(elements[1]["autoFit"], "normal-auto-fit")
|
|
1762
|
+
self.assertEqual(elements[1]["fontSize"], 28)
|
|
1763
|
+
self.assertEqual(elements[1]["text"], "Growth & scale\nFocused execution")
|
|
1764
|
+
|
|
1765
|
+
def test_lint_xml_ignores_small_out_of_bounds_images(self) -> None:
|
|
1766
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1767
|
+
"""
|
|
1768
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
1769
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1770
|
+
<data>
|
|
1771
|
+
<img src="tok" topLeftX="-20" topLeftY="20" width="120" height="120"/>
|
|
1772
|
+
</data>
|
|
1773
|
+
</slide>
|
|
1774
|
+
</presentation>
|
|
1775
|
+
"""
|
|
1776
|
+
)
|
|
1777
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1778
|
+
|
|
1779
|
+
def test_lint_xml_ignores_out_of_canvas_images(self) -> None:
|
|
1780
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1781
|
+
"""
|
|
1782
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
1783
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1784
|
+
<data>
|
|
1785
|
+
<img src="right" topLeftX="780" topLeftY="0" width="500" height="540"/>
|
|
1786
|
+
<img src="bottom" topLeftX="0" topLeftY="430" width="900" height="280"/>
|
|
1787
|
+
</data>
|
|
1788
|
+
</slide>
|
|
1789
|
+
</presentation>
|
|
1790
|
+
"""
|
|
1791
|
+
)
|
|
1792
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1793
|
+
|
|
1794
|
+
def test_lint_xml_ignores_full_bleed_images_outside_canvas(self) -> None:
|
|
1795
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1796
|
+
"""
|
|
1797
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
1798
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1799
|
+
<data>
|
|
1800
|
+
<img src="tok" topLeftX="-80" topLeftY="-20" width="1080" height="600"/>
|
|
1801
|
+
</data>
|
|
1802
|
+
</slide>
|
|
1803
|
+
</presentation>
|
|
1804
|
+
"""
|
|
1805
|
+
)
|
|
1806
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1807
|
+
|
|
1808
|
+
def test_lint_xml_reports_text_and_chart_but_not_image_out_of_canvas(self) -> None:
|
|
1809
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1810
|
+
"""
|
|
1811
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
1812
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1813
|
+
<data>
|
|
1814
|
+
<shape id="outside-shape" type="text" topLeftX="-10" topLeftY="40" width="50" height="50"/>
|
|
1815
|
+
<img id="outside-img" src="token" topLeftX="120" topLeftY="-20" width="50" height="50"/>
|
|
1816
|
+
<chart id="outside-chart" topLeftX="900" topLeftY="100" width="100" height="100">
|
|
1817
|
+
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
|
1818
|
+
<chartData>
|
|
1819
|
+
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
|
|
1820
|
+
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
|
|
1821
|
+
</chartData>
|
|
1822
|
+
</chart>
|
|
1823
|
+
</data>
|
|
1824
|
+
</slide>
|
|
1825
|
+
</presentation>
|
|
1826
|
+
"""
|
|
1827
|
+
)
|
|
1828
|
+
issues = result["slides"][0]["issues"]
|
|
1829
|
+
self.assertEqual(result["summary"]["error_count"], 2)
|
|
1830
|
+
self.assertEqual(
|
|
1831
|
+
[(issue["code"], issue["elements"], issue["overflow"]) for issue in issues],
|
|
1832
|
+
[
|
|
1833
|
+
("shape_out_of_canvas", ["outside-shape"], {"left": 10, "top": 0, "right": 0, "bottom": 0}),
|
|
1834
|
+
("chart_out_of_canvas", ["outside-chart"], {"left": 0, "top": 0, "right": 40, "bottom": 0}),
|
|
1835
|
+
],
|
|
1836
|
+
)
|
|
1837
|
+
|
|
1838
|
+
def test_lint_xml_ignores_line_out_of_canvas(self) -> None:
|
|
1839
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1840
|
+
"""
|
|
1841
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1842
|
+
<data>
|
|
1843
|
+
<shape id="body" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
1844
|
+
<content fontSize="18"><p>Visible content</p></content>
|
|
1845
|
+
</shape>
|
|
1846
|
+
<line id="connector" startX="80" startY="120" endX="980" endY="120"><border/></line>
|
|
1847
|
+
</data>
|
|
1848
|
+
</slide>
|
|
1849
|
+
"""
|
|
1850
|
+
)
|
|
1851
|
+
|
|
1852
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1853
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
1854
|
+
|
|
1855
|
+
def test_lint_xml_reports_horizontal_line_crossing_headline_glyphs(self) -> None:
|
|
1856
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1857
|
+
"""
|
|
1858
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1859
|
+
<data>
|
|
1860
|
+
<shape id="title" type="text" topLeftX="80" topLeftY="200" width="500" height="90">
|
|
1861
|
+
<content fontSize="60"><p>测试文字 ABC</p></content>
|
|
1862
|
+
</shape>
|
|
1863
|
+
<line id="strike" startX="80" startY="245" endX="560" endY="245">
|
|
1864
|
+
<border color="rgb(255, 0, 0)" width="4"/>
|
|
1865
|
+
</line>
|
|
1866
|
+
</data>
|
|
1867
|
+
</slide>
|
|
1868
|
+
"""
|
|
1869
|
+
)
|
|
1870
|
+
crossing = [
|
|
1871
|
+
issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"strike", "title"}
|
|
1872
|
+
]
|
|
1873
|
+
self.assertEqual(len(crossing), 1)
|
|
1874
|
+
self.assertEqual(crossing[0]["code"], "bbox_overlap")
|
|
1875
|
+
|
|
1876
|
+
def test_lint_xml_reports_vertical_line_crossing_multiline_text(self) -> None:
|
|
1877
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1878
|
+
"""
|
|
1879
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1880
|
+
<data>
|
|
1881
|
+
<shape id="col" type="text" topLeftX="700" topLeftY="180" width="240" height="180">
|
|
1882
|
+
<content fontSize="20"><p>第一行文字内容</p><p>第二行文字内容</p><p>第三行文字内容</p></content>
|
|
1883
|
+
</shape>
|
|
1884
|
+
<line id="vbar" startX="740" startY="170" endX="740" endY="360">
|
|
1885
|
+
<border color="rgb(0, 0, 255)" width="3"/>
|
|
1886
|
+
</line>
|
|
1887
|
+
</data>
|
|
1888
|
+
</slide>
|
|
1889
|
+
"""
|
|
1890
|
+
)
|
|
1891
|
+
crossing = [
|
|
1892
|
+
issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"vbar", "col"}
|
|
1893
|
+
]
|
|
1894
|
+
self.assertEqual(len(crossing), 1)
|
|
1895
|
+
|
|
1896
|
+
def test_lint_xml_reports_diagonal_line_crossing_text_block(self) -> None:
|
|
1897
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1898
|
+
"""
|
|
1899
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1900
|
+
<data>
|
|
1901
|
+
<shape id="para" type="text" topLeftX="80" topLeftY="400" width="420" height="140">
|
|
1902
|
+
<content fontSize="18"><p>这是一段测试文字用于验证线条穿过</p></content>
|
|
1903
|
+
</shape>
|
|
1904
|
+
<line id="diag" startX="80" startY="410" endX="500" endY="530">
|
|
1905
|
+
<border color="rgb(255, 0, 0)" width="3"/>
|
|
1906
|
+
</line>
|
|
1907
|
+
</data>
|
|
1908
|
+
</slide>
|
|
1909
|
+
"""
|
|
1910
|
+
)
|
|
1911
|
+
crossing = [
|
|
1912
|
+
issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"diag", "para"}
|
|
1913
|
+
]
|
|
1914
|
+
self.assertEqual(len(crossing), 1)
|
|
1915
|
+
|
|
1916
|
+
def test_lint_xml_ignores_diagonal_line_whose_bbox_but_not_segment_crosses_text(self) -> None:
|
|
1917
|
+
# The diagonal's axis-aligned bounding box overlaps the text, but the segment itself passes
|
|
1918
|
+
# through empty space in the opposite corner -- a naive bbox test would false-positive here.
|
|
1919
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1920
|
+
"""
|
|
1921
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1922
|
+
<data>
|
|
1923
|
+
<shape id="corner-text" type="text" topLeftX="80" topLeftY="80" width="120" height="40">
|
|
1924
|
+
<content fontSize="18"><p>corner</p></content>
|
|
1925
|
+
</shape>
|
|
1926
|
+
<line id="far-diag" startX="700" startY="80" endX="90" endY="500">
|
|
1927
|
+
<border color="rgb(255, 0, 0)" width="3"/>
|
|
1928
|
+
</line>
|
|
1929
|
+
</data>
|
|
1930
|
+
</slide>
|
|
1931
|
+
"""
|
|
1932
|
+
)
|
|
1933
|
+
crossing = [
|
|
1934
|
+
issue for issue in result["slides"][0]["errors"] if set(issue["elements"]) == {"far-diag", "corner-text"}
|
|
1935
|
+
]
|
|
1936
|
+
self.assertEqual(crossing, [])
|
|
1937
|
+
|
|
1938
|
+
def test_lint_xml_ignores_line_touching_text_frame_but_not_glyphs(self) -> None:
|
|
1939
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1940
|
+
"""
|
|
1941
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1942
|
+
<data>
|
|
1943
|
+
<shape id="lbl" type="text" topLeftX="80" topLeftY="80" width="300" height="200">
|
|
1944
|
+
<content fontSize="18" verticalAlign="top"><p>短标签</p></content>
|
|
1945
|
+
</shape>
|
|
1946
|
+
<line id="below" startX="80" startY="270" endX="380" endY="270">
|
|
1947
|
+
<border color="rgb(255, 0, 0)" width="2"/>
|
|
1948
|
+
</line>
|
|
1949
|
+
</data>
|
|
1950
|
+
</slide>
|
|
1951
|
+
"""
|
|
1952
|
+
)
|
|
1953
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1954
|
+
|
|
1955
|
+
def test_lint_xml_ignores_invisible_line_crossing_text(self) -> None:
|
|
1956
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1957
|
+
"""
|
|
1958
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1959
|
+
<data>
|
|
1960
|
+
<shape id="title" type="text" topLeftX="80" topLeftY="200" width="500" height="90">
|
|
1961
|
+
<content fontSize="60"><p>测试文字 ABC</p></content>
|
|
1962
|
+
</shape>
|
|
1963
|
+
<line id="ghost-line" startX="80" startY="245" endX="560" endY="245">
|
|
1964
|
+
<border color="rgba(255, 0, 0, 0.03)" width="4"/>
|
|
1965
|
+
</line>
|
|
1966
|
+
</data>
|
|
1967
|
+
</slide>
|
|
1968
|
+
"""
|
|
1969
|
+
)
|
|
1970
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1971
|
+
|
|
1972
|
+
def test_lint_xml_ignores_vertical_line_grazing_text_left_edge(self) -> None:
|
|
1973
|
+
# Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 4: a vertical line sitting on the text
|
|
1974
|
+
# frame's left edge renders before the first glyph, so it must not be flagged.
|
|
1975
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1976
|
+
"""
|
|
1977
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1978
|
+
<data>
|
|
1979
|
+
<shape width="240" height="60" topLeftX="120" topLeftY="100" type="text" id="bmm">
|
|
1980
|
+
<content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
|
|
1981
|
+
<p>Vertical edge graze</p>
|
|
1982
|
+
</content>
|
|
1983
|
+
</shape>
|
|
1984
|
+
<line id="bmX" startX="120.00000000000001" startY="90" endX="120.00000000000001" endY="150.00833275470998">
|
|
1985
|
+
<border color="rgba(0, 0, 0, 1)"/>
|
|
1986
|
+
</line>
|
|
1987
|
+
</data>
|
|
1988
|
+
</slide>
|
|
1989
|
+
"""
|
|
1990
|
+
)
|
|
1991
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
1992
|
+
|
|
1993
|
+
def test_lint_xml_ignores_polyline_crossing_text(self) -> None:
|
|
1994
|
+
# Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 6: the crossing check is scoped to
|
|
1995
|
+
# <line> only, so a <polyline> over text is not flagged.
|
|
1996
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
1997
|
+
"""
|
|
1998
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
1999
|
+
<data>
|
|
2000
|
+
<shape width="240" height="60" topLeftX="120" topLeftY="100" type="text" id="bmr">
|
|
2001
|
+
<content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
|
|
2002
|
+
<p>Polyline target</p>
|
|
2003
|
+
</content>
|
|
2004
|
+
</shape>
|
|
2005
|
+
<polyline id="bmH" width="270" height="55" topLeftX="110" topLeftY="95">
|
|
2006
|
+
<border color="rgba(0, 0, 0, 1)"/>
|
|
2007
|
+
</polyline>
|
|
2008
|
+
</data>
|
|
2009
|
+
</slide>
|
|
2010
|
+
"""
|
|
2011
|
+
)
|
|
2012
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
2013
|
+
|
|
2014
|
+
def test_lint_xml_ignores_line_below_visual_glyph_height(self) -> None:
|
|
2015
|
+
# Verbatim from deck GpGusGCwplQyK8dFN9LczmBXnwQ slide 7: the shape frame is 80px tall but the
|
|
2016
|
+
# single 20px line of glyphs occupies only its top; a line at the frame's lower region grazes
|
|
2017
|
+
# under the visual glyph box (underline look) and must not be flagged.
|
|
2018
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2019
|
+
"""
|
|
2020
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2021
|
+
<data>
|
|
2022
|
+
<shape width="240" height="80" topLeftX="120" topLeftY="100" type="text" id="bmB">
|
|
2023
|
+
<content fontSize="20" fontFamily="Arial" color="rgba(31, 35, 41, 1)" lineSpacing="fixed:24">
|
|
2024
|
+
<p>Visual height target</p>
|
|
2025
|
+
</content>
|
|
2026
|
+
</shape>
|
|
2027
|
+
<line id="bmQ" startX="110" startY="150" endX="380.00185184550116" endY="150">
|
|
2028
|
+
<border color="rgba(0, 0, 0, 1)"/>
|
|
2029
|
+
</line>
|
|
2030
|
+
</data>
|
|
2031
|
+
</slide>
|
|
2032
|
+
"""
|
|
2033
|
+
)
|
|
2034
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
2035
|
+
|
|
2036
|
+
def test_lint_xml_uses_rotated_text_and_chart_bounds_for_canvas_validation(self) -> None:
|
|
2037
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2038
|
+
"""
|
|
2039
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2040
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2041
|
+
<data>
|
|
2042
|
+
<shape id="rotated-text" type="text" topLeftX="0" topLeftY="0" width="100" height="100" rotation="45"/>
|
|
2043
|
+
<chart id="rotated-chart" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45">
|
|
2044
|
+
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
|
2045
|
+
<chartData>
|
|
2046
|
+
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
|
|
2047
|
+
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
|
|
2048
|
+
</chartData>
|
|
2049
|
+
</chart>
|
|
2050
|
+
</data>
|
|
2051
|
+
</slide>
|
|
2052
|
+
</presentation>
|
|
2053
|
+
"""
|
|
2054
|
+
)
|
|
2055
|
+
issues_by_element = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
|
|
2056
|
+
self.assertEqual(result["summary"]["error_count"], 2)
|
|
2057
|
+
self.assertEqual(issues_by_element["rotated-text"]["code"], "shape_out_of_canvas")
|
|
2058
|
+
self.assertAlmostEqual(issues_by_element["rotated-text"]["overflow"]["left"], 20.710678, places=5)
|
|
2059
|
+
self.assertAlmostEqual(issues_by_element["rotated-text"]["overflow"]["top"], 20.710678, places=5)
|
|
2060
|
+
self.assertEqual(issues_by_element["rotated-chart"]["code"], "chart_out_of_canvas")
|
|
2061
|
+
self.assertAlmostEqual(issues_by_element["rotated-chart"]["overflow"]["right"], 20.710678, places=5)
|
|
2062
|
+
|
|
2063
|
+
def test_lint_xml_uses_declared_bounds_for_rect_and_ignores_images(self) -> None:
|
|
2064
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2065
|
+
"""
|
|
2066
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2067
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2068
|
+
<data>
|
|
2069
|
+
<shape id="rotated-rect" type="rect" topLeftX="900" topLeftY="0" width="100" height="100" rotation="45"/>
|
|
2070
|
+
<img id="rotated-image" src="token" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45"/>
|
|
2071
|
+
</data>
|
|
2072
|
+
</slide>
|
|
2073
|
+
</presentation>
|
|
2074
|
+
"""
|
|
2075
|
+
)
|
|
2076
|
+
issues_by_element = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
|
|
2077
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
2078
|
+
self.assertEqual(issues_by_element["rotated-rect"]["code"], "shape_out_of_canvas")
|
|
2079
|
+
self.assertEqual(issues_by_element["rotated-rect"]["overflow"], {"left": 0, "top": 0, "right": 40, "bottom": 0})
|
|
2080
|
+
self.assertNotIn("rotated-image", issues_by_element)
|
|
2081
|
+
|
|
2082
|
+
def test_detect_elements_out_of_canvas_limits_detection_to_whitelist(self) -> None:
|
|
2083
|
+
issues = xml_text_overlap_lint.detect_elements_out_of_canvas(
|
|
2084
|
+
[
|
|
2085
|
+
{"id": "table", "kind": "table", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 45},
|
|
2086
|
+
{"id": "chart", "kind": "chart", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 0},
|
|
2087
|
+
{
|
|
2088
|
+
"id": "text",
|
|
2089
|
+
"kind": "shape",
|
|
2090
|
+
"type": "text",
|
|
2091
|
+
"x": 95,
|
|
2092
|
+
"y": 0,
|
|
2093
|
+
"width": 10,
|
|
2094
|
+
"height": 10,
|
|
2095
|
+
"rotation": 0,
|
|
2096
|
+
},
|
|
2097
|
+
{
|
|
2098
|
+
"id": "rect",
|
|
2099
|
+
"kind": "shape",
|
|
2100
|
+
"type": "rect",
|
|
2101
|
+
"x": 95,
|
|
2102
|
+
"y": 0,
|
|
2103
|
+
"width": 10,
|
|
2104
|
+
"height": 10,
|
|
2105
|
+
"rotation": 45,
|
|
2106
|
+
},
|
|
2107
|
+
{"id": "image", "kind": "img", "x": 95, "y": 0, "width": 10, "height": 10, "rotation": 0},
|
|
2108
|
+
{
|
|
2109
|
+
"id": "ellipse",
|
|
2110
|
+
"kind": "shape",
|
|
2111
|
+
"type": "ellipse",
|
|
2112
|
+
"x": 95,
|
|
2113
|
+
"y": 0,
|
|
2114
|
+
"width": 10,
|
|
2115
|
+
"height": 10,
|
|
2116
|
+
"rotation": 0,
|
|
2117
|
+
},
|
|
2118
|
+
],
|
|
2119
|
+
100,
|
|
2120
|
+
100,
|
|
2121
|
+
)
|
|
2122
|
+
|
|
2123
|
+
self.assertEqual([issue["elements"] for issue in issues], [["table"], ["chart"], ["text"], ["rect"]])
|
|
2124
|
+
self.assertEqual(issues[-1]["bbox"], {"x": 95, "y": 0, "width": 10, "height": 10})
|
|
2125
|
+
|
|
2126
|
+
def test_lint_xml_rejects_non_finite_rotation_values_from_xsd(self) -> None:
|
|
2127
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2128
|
+
"""
|
|
2129
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2130
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2131
|
+
<data>
|
|
2132
|
+
<shape id="infinite" type="text" topLeftX="-10" topLeftY="0" width="20" height="20" rotation="inf"/>
|
|
2133
|
+
<shape id="negative-infinite" type="text" topLeftX="0" topLeftY="-10" width="20" height="20" rotation="-inf"/>
|
|
2134
|
+
<chart id="not-a-number" topLeftX="950" topLeftY="0" width="20" height="20" rotation="nan">
|
|
2135
|
+
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
|
2136
|
+
<chartData>
|
|
2137
|
+
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
|
|
2138
|
+
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
|
|
2139
|
+
</chartData>
|
|
2140
|
+
</chart>
|
|
2141
|
+
</data>
|
|
2142
|
+
</slide>
|
|
2143
|
+
</presentation>
|
|
2144
|
+
"""
|
|
2145
|
+
)
|
|
2146
|
+
self.assertEqual(result["summary"]["error_count"], 3)
|
|
2147
|
+
slide_issues = result["slides"][0]["issues"]
|
|
2148
|
+
self.assertTrue(all(issue["code"] == "sxsd_invalid_scalar" for issue in slide_issues))
|
|
2149
|
+
self.assertEqual({issue["actual"] for issue in slide_issues}, {"inf", "-inf", "nan"})
|
|
2150
|
+
|
|
2151
|
+
def test_lint_xml_reports_table_bottom_overflow_from_declared_bounds(self) -> None:
|
|
2152
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2153
|
+
"""
|
|
2154
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2155
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2156
|
+
<data>
|
|
2157
|
+
<table id="score-table" topLeftX="54" topLeftY="238" width="414" height="385">
|
|
2158
|
+
<tr><td><content><p>Score</p></content></td></tr>
|
|
2159
|
+
</table>
|
|
2160
|
+
</data>
|
|
2161
|
+
</slide>
|
|
2162
|
+
</presentation>
|
|
2163
|
+
"""
|
|
2164
|
+
)
|
|
2165
|
+
issue = result["slides"][0]["issues"][0]
|
|
2166
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
2167
|
+
self.assertEqual(issue["code"], "table_out_of_canvas")
|
|
2168
|
+
self.assertEqual(issue["elements"], ["score-table"])
|
|
2169
|
+
self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 0, "bottom": 83})
|
|
2170
|
+
self.assertEqual(issue["bbox"], {"x": 54, "y": 238, "width": 414, "height": 385})
|
|
2171
|
+
|
|
2172
|
+
def test_lint_xml_reports_table_right_overflow_from_declared_bounds(self) -> None:
|
|
2173
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2174
|
+
"""
|
|
2175
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2176
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2177
|
+
<data>
|
|
2178
|
+
<table id="wide-table" topLeftX="850" topLeftY="80" width="180" height="120">
|
|
2179
|
+
<tr><td><content><p>Score</p></content></td></tr>
|
|
2180
|
+
</table>
|
|
2181
|
+
</data>
|
|
2182
|
+
</slide>
|
|
2183
|
+
</presentation>
|
|
2184
|
+
"""
|
|
2185
|
+
)
|
|
2186
|
+
issue = result["slides"][0]["issues"][0]
|
|
2187
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
2188
|
+
self.assertEqual(issue["code"], "table_out_of_canvas")
|
|
2189
|
+
self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 70, "bottom": 0})
|
|
2190
|
+
|
|
2191
|
+
def test_lint_xml_allows_table_with_declared_bounds_inside_canvas(self) -> None:
|
|
2192
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2193
|
+
"""
|
|
2194
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2195
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2196
|
+
<data>
|
|
2197
|
+
<table id="inside-table" topLeftX="40" topLeftY="120" width="880" height="360">
|
|
2198
|
+
<tr><td><content><p>Score</p></content></td></tr>
|
|
2199
|
+
</table>
|
|
2200
|
+
</data>
|
|
2201
|
+
</slide>
|
|
2202
|
+
</presentation>
|
|
2203
|
+
"""
|
|
2204
|
+
)
|
|
2205
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
2206
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
2207
|
+
|
|
2208
|
+
def test_lint_xml_reports_resolved_table_bounds_when_declared_sizes_are_missing(self) -> None:
|
|
2209
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2210
|
+
"""
|
|
2211
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2212
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2213
|
+
<data>
|
|
2214
|
+
<table id="implicit-size-table" topLeftX="850" topLeftY="480">
|
|
2215
|
+
<colgroup><col/><col/></colgroup>
|
|
2216
|
+
<tr><td/><td/></tr>
|
|
2217
|
+
<tr><td/><td/></tr>
|
|
2218
|
+
</table>
|
|
2219
|
+
</data>
|
|
2220
|
+
</slide>
|
|
2221
|
+
</presentation>
|
|
2222
|
+
"""
|
|
2223
|
+
)
|
|
2224
|
+
issue = result["slides"][0]["issues"][0]
|
|
2225
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
2226
|
+
self.assertEqual(issue["code"], "table_out_of_canvas")
|
|
2227
|
+
self.assertEqual(issue["bbox"], {"x": 850, "y": 480, "width": 220, "height": 74})
|
|
2228
|
+
self.assertEqual(issue["overflow"], {"left": 0, "top": 0, "right": 110, "bottom": 14})
|
|
2229
|
+
|
|
2230
|
+
def test_lint_xml_uses_resolved_table_bounds_for_canvas_validation(self) -> None:
|
|
2231
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2232
|
+
"""
|
|
2233
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2234
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2235
|
+
<data>
|
|
2236
|
+
<table id="resolved-overflow-table" topLeftX="800" topLeftY="80" width="100" height="40">
|
|
2237
|
+
<colgroup><col width="100"/><col width="100"/></colgroup>
|
|
2238
|
+
<tr height="40"><td/><td/></tr>
|
|
2239
|
+
</table>
|
|
2240
|
+
</data>
|
|
2241
|
+
</slide>
|
|
2242
|
+
</presentation>
|
|
2243
|
+
"""
|
|
2244
|
+
)
|
|
2245
|
+
issues = result["slides"][0]["issues"]
|
|
2246
|
+
canvas_issue = next(issue for issue in issues if issue["code"] == "table_out_of_canvas")
|
|
2247
|
+
mismatch_issue = next(issue for issue in issues if issue["code"] == "table_resolved_size_mismatch")
|
|
2248
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
2249
|
+
self.assertEqual(canvas_issue["bbox"], {"x": 800, "y": 80, "width": 200, "height": 40})
|
|
2250
|
+
self.assertEqual(canvas_issue["overflow"]["right"], 40)
|
|
2251
|
+
self.assertEqual(mismatch_issue["dimension"], "width")
|
|
2252
|
+
self.assertEqual(mismatch_issue["resolved_size"], canvas_issue["bbox"]["width"])
|
|
2253
|
+
|
|
2254
|
+
def test_lint_xml_uses_the_same_anonymous_table_id_for_all_table_diagnostics(self) -> None:
|
|
2255
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2256
|
+
"""
|
|
2257
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2258
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2259
|
+
<data>
|
|
2260
|
+
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="200" height="40"/>
|
|
2261
|
+
<img id="logo" src="token" topLeftX="40" topLeftY="100" width="40" height="40"/>
|
|
2262
|
+
<table topLeftX="900" topLeftY="80" width="100" height="40">
|
|
2263
|
+
<colgroup><col width="100"/><col width="100"/></colgroup>
|
|
2264
|
+
<tr height="40"><td/><td/></tr>
|
|
2265
|
+
</table>
|
|
2266
|
+
</data>
|
|
2267
|
+
</slide>
|
|
2268
|
+
</presentation>
|
|
2269
|
+
"""
|
|
2270
|
+
)
|
|
2271
|
+
issues = result["slides"][0]["issues"]
|
|
2272
|
+
canvas_issue = next(issue for issue in issues if issue["code"] == "table_out_of_canvas")
|
|
2273
|
+
mismatch_issue = next(issue for issue in issues if issue["code"] == "table_resolved_size_mismatch")
|
|
2274
|
+
self.assertEqual(canvas_issue["elements"], ["table-3"])
|
|
2275
|
+
self.assertEqual(mismatch_issue["elements"], ["table-3"])
|
|
2276
|
+
|
|
2277
|
+
def test_lint_xml_reports_info_when_table_target_size_resolves_larger_than_declared(self) -> None:
|
|
2278
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2279
|
+
"""
|
|
2280
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2281
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2282
|
+
<data>
|
|
2283
|
+
<table id="size-mismatch" topLeftX="40" topLeftY="120" width="200" height="80">
|
|
2284
|
+
<colgroup><col span="2" width="100"/><col width="50"/></colgroup>
|
|
2285
|
+
<tr height="40"><td/><td/><td/></tr>
|
|
2286
|
+
<tr height="60"><td/><td/><td/></tr>
|
|
2287
|
+
</table>
|
|
2288
|
+
</data>
|
|
2289
|
+
</slide>
|
|
2290
|
+
</presentation>
|
|
2291
|
+
"""
|
|
2292
|
+
)
|
|
2293
|
+
issues_by_dimension = {issue["dimension"]: issue for issue in result["slides"][0]["issues"]}
|
|
2294
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
2295
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
2296
|
+
self.assertEqual(result["summary"]["info_count"], 2)
|
|
2297
|
+
self.assertEqual(issues_by_dimension["width"]["level"], "info")
|
|
2298
|
+
self.assertEqual(issues_by_dimension["width"]["code"], "table_resolved_size_mismatch")
|
|
2299
|
+
self.assertEqual(issues_by_dimension["width"]["resolved_sizes"], [100, 100, 50])
|
|
2300
|
+
self.assertEqual(issues_by_dimension["width"]["resolved_size"], 250)
|
|
2301
|
+
self.assertEqual(issues_by_dimension["height"]["resolved_sizes"], [40, 60])
|
|
2302
|
+
self.assertEqual(issues_by_dimension["height"]["resolved_size"], 100)
|
|
2303
|
+
|
|
2304
|
+
def test_lint_xml_does_not_report_info_when_table_target_size_is_resolved_exactly(self) -> None:
|
|
2305
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2306
|
+
"""
|
|
2307
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2308
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2309
|
+
<data>
|
|
2310
|
+
<table id="size-match" topLeftX="40" topLeftY="120" width="300" height="100">
|
|
2311
|
+
<colgroup><col width="100"/><col/></colgroup>
|
|
2312
|
+
<tr height="40"><td/><td/></tr>
|
|
2313
|
+
<tr><td/><td/></tr>
|
|
2314
|
+
</table>
|
|
2315
|
+
</data>
|
|
2316
|
+
</slide>
|
|
2317
|
+
</presentation>
|
|
2318
|
+
"""
|
|
2319
|
+
)
|
|
2320
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
2321
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
2322
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
2323
|
+
|
|
2324
|
+
def test_lint_xml_keeps_resolved_table_sizes_positive_when_target_is_too_small(self) -> None:
|
|
2325
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2326
|
+
"""
|
|
2327
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2328
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2329
|
+
<data>
|
|
2330
|
+
<table id="narrow-table" topLeftX="40" topLeftY="120" width="1">
|
|
2331
|
+
<colgroup><col/><col/></colgroup>
|
|
2332
|
+
<tr><td/><td/></tr>
|
|
2333
|
+
</table>
|
|
2334
|
+
</data>
|
|
2335
|
+
</slide>
|
|
2336
|
+
</presentation>
|
|
2337
|
+
"""
|
|
2338
|
+
)
|
|
2339
|
+
issue = result["slides"][0]["issues"][0]
|
|
2340
|
+
self.assertEqual(issue["dimension"], "width")
|
|
2341
|
+
self.assertEqual(issue["resolved_sizes"], [1, 1])
|
|
2342
|
+
self.assertEqual(issue["resolved_size"], 2)
|
|
2343
|
+
|
|
2344
|
+
def test_fill_last_size_gap_preserves_target_when_positive_sizes_are_possible(self) -> None:
|
|
2345
|
+
final_sizes = xml_text_overlap_lint.fill_last_size_gap([10, 10], 3)
|
|
2346
|
+
self.assertEqual(final_sizes, [2, 1])
|
|
2347
|
+
self.assertEqual(sum(final_sizes), 3)
|
|
2348
|
+
|
|
2349
|
+
def test_cli_reports_table_layout_size_info_for_weighted_min_layout_cases(self) -> None:
|
|
2350
|
+
cases = {
|
|
2351
|
+
"target-exact": (
|
|
2352
|
+
"""
|
|
2353
|
+
<table topLeftX="40" topLeftY="120" width="360" height="150">
|
|
2354
|
+
<colgroup><col width="100"/><col width="200"/></colgroup>
|
|
2355
|
+
<tr height="40"><td/><td/></tr><tr height="60"><td/><td/></tr>
|
|
2356
|
+
</table>
|
|
2357
|
+
""",
|
|
2358
|
+
0,
|
|
2359
|
+
),
|
|
2360
|
+
"declared-size-exceeds-target": (
|
|
2361
|
+
"""
|
|
2362
|
+
<table topLeftX="40" topLeftY="120" width="200" height="80">
|
|
2363
|
+
<colgroup><col span="2" width="100"/><col width="50"/></colgroup>
|
|
2364
|
+
<tr height="40"><td/><td/><td/></tr><tr height="60"><td/><td/><td/></tr>
|
|
2365
|
+
</table>
|
|
2366
|
+
""",
|
|
2367
|
+
2,
|
|
2368
|
+
),
|
|
2369
|
+
"remaining-space-insufficient": (
|
|
2370
|
+
"""
|
|
2371
|
+
<table topLeftX="40" topLeftY="120" width="80" height="30">
|
|
2372
|
+
<colgroup><col width="80"/><col/></colgroup>
|
|
2373
|
+
<tr height="40"><td/><td/></tr><tr><td/><td/></tr>
|
|
2374
|
+
</table>
|
|
2375
|
+
""",
|
|
2376
|
+
2,
|
|
2377
|
+
),
|
|
2378
|
+
"no-target-size": (
|
|
2379
|
+
"""
|
|
2380
|
+
<table topLeftX="40" topLeftY="120">
|
|
2381
|
+
<colgroup><col width="80"/><col/></colgroup>
|
|
2382
|
+
<tr height="40"><td/><td/></tr><tr><td/><td/></tr>
|
|
2383
|
+
</table>
|
|
2384
|
+
""",
|
|
2385
|
+
0,
|
|
2386
|
+
),
|
|
2387
|
+
}
|
|
2388
|
+
script_path = Path(xml_text_overlap_lint.__file__).resolve()
|
|
2389
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
2390
|
+
for name, (table_xml, expected_info_count) in cases.items():
|
|
2391
|
+
with self.subTest(case=name):
|
|
2392
|
+
input_path = Path(temp_dir) / f"{name}.xml"
|
|
2393
|
+
input_path.write_text(
|
|
2394
|
+
f"""
|
|
2395
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2396
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0"><data>{table_xml}</data></slide>
|
|
2397
|
+
</presentation>
|
|
2398
|
+
""",
|
|
2399
|
+
encoding="utf-8",
|
|
2400
|
+
)
|
|
2401
|
+
completed = subprocess.run(
|
|
2402
|
+
[sys.executable, str(script_path), "--input", str(input_path)],
|
|
2403
|
+
capture_output=True,
|
|
2404
|
+
check=False,
|
|
2405
|
+
text=True,
|
|
2406
|
+
)
|
|
2407
|
+
result = json.loads(completed.stdout)
|
|
2408
|
+
self.assertEqual(completed.returncode, 0, completed.stderr)
|
|
2409
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
2410
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
2411
|
+
self.assertEqual(result["summary"]["info_count"], expected_info_count)
|
|
2412
|
+
self.assertTrue(
|
|
2413
|
+
all(issue["level"] == "info" for issue in result["slides"][0]["issues"]),
|
|
2414
|
+
result["slides"][0]["issues"],
|
|
2415
|
+
)
|
|
2416
|
+
|
|
2417
|
+
def test_lint_xml_detects_invalid_template_text_stack_overlap(self) -> None:
|
|
2418
|
+
cases = [
|
|
2419
|
+
(
|
|
2420
|
+
"subtitle-too-high",
|
|
2421
|
+
"""
|
|
2422
|
+
<shape type="text" topLeftX="40" topLeftY="80" width="240" height="90">
|
|
1146
2423
|
<content textType="title" fontSize="44"><p>Title</p></content>
|
|
1147
2424
|
</shape>
|
|
1148
|
-
<shape type="text" topLeftX="40" topLeftY="90" width="240" height="80">
|
|
1149
|
-
<content textType="sub-headline" fontSize="20"><p>Subtitle</p></content>
|
|
2425
|
+
<shape type="text" topLeftX="40" topLeftY="90" width="240" height="80">
|
|
2426
|
+
<content textType="sub-headline" fontSize="20"><p>Subtitle</p></content>
|
|
2427
|
+
</shape>
|
|
2428
|
+
""",
|
|
2429
|
+
),
|
|
2430
|
+
]
|
|
2431
|
+
for name, shapes in cases:
|
|
2432
|
+
with self.subTest(name=name):
|
|
2433
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2434
|
+
f"""
|
|
2435
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2436
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2437
|
+
<data>{shapes}</data>
|
|
2438
|
+
</slide>
|
|
2439
|
+
</presentation>
|
|
2440
|
+
"""
|
|
2441
|
+
)
|
|
2442
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
2443
|
+
self.assertEqual(result["slides"][0]["issues"][0]["code"], "bbox_overlap")
|
|
2444
|
+
|
|
2445
|
+
|
|
2446
|
+
def test_lint_xml_reports_vertical_text_image_overlap_as_warning(self) -> None:
|
|
2447
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2448
|
+
"""
|
|
2449
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0"><data>
|
|
2450
|
+
<shape id="text" type="text" vert="vert" topLeftX="100" topLeftY="100" width="100" height="100">
|
|
2451
|
+
<content><p>Vertical</p></content>
|
|
2452
|
+
</shape>
|
|
2453
|
+
<img id="image" src="token" topLeftX="120" topLeftY="120" width="20" height="20"/>
|
|
2454
|
+
</data></slide>
|
|
2455
|
+
"""
|
|
2456
|
+
)
|
|
2457
|
+
issue = next(issue for issue in result["slides"][0]["issues"] if issue["code"] == "image_may_cover_vertical_text")
|
|
2458
|
+
self.assertEqual(issue["level"], "info")
|
|
2459
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
2460
|
+
self.assertEqual(result["summary"]["info_count"], 1)
|
|
2461
|
+
|
|
2462
|
+
|
|
2463
|
+
class XmlTextOverlapLintDensityTest(unittest.TestCase):
|
|
2464
|
+
def test_lint_xml_blocks_blank_slide(self) -> None:
|
|
2465
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2466
|
+
"""
|
|
2467
|
+
<presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
|
|
2468
|
+
<slide id="content-slide">
|
|
2469
|
+
<data>
|
|
2470
|
+
<shape id="title" type="text" topLeftX="60" topLeftY="60" width="400" height="50">
|
|
2471
|
+
<content fontSize="28"><p>Investment report</p></content>
|
|
2472
|
+
</shape>
|
|
2473
|
+
</data>
|
|
2474
|
+
</slide>
|
|
2475
|
+
<slide id="blank-slide">
|
|
2476
|
+
<style><fill><fillColor color="rgba(255, 255, 255, 1)"/></fill></style>
|
|
2477
|
+
<data/>
|
|
2478
|
+
<note><content/></note>
|
|
2479
|
+
</slide>
|
|
2480
|
+
</presentation>
|
|
2481
|
+
"""
|
|
2482
|
+
)
|
|
2483
|
+
|
|
2484
|
+
self.assertEqual(result["summary"]["slide_count"], 2)
|
|
2485
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
2486
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
2487
|
+
self.assertEqual(result["summary"]["status"], "blocked")
|
|
2488
|
+
self.assertFalse(result["summary"]["release_ready"])
|
|
2489
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
2490
|
+
self.assertEqual(result["slides"][1]["element_count"], 0)
|
|
2491
|
+
issue = result["slides"][1]["errors"][0]
|
|
2492
|
+
self.assertEqual(issue["level"], "error")
|
|
2493
|
+
self.assertEqual(issue["code"], "blank_slide")
|
|
2494
|
+
self.assertEqual(issue["element_ids"], [])
|
|
2495
|
+
self.assertEqual(issue["rule"]["id"], "blank_slide")
|
|
2496
|
+
self.assertEqual(issue["measurement"]["visible_element_count"], 0)
|
|
2497
|
+
self.assertEqual(issue["related_objects"], [])
|
|
2498
|
+
|
|
2499
|
+
def test_lint_xml_blocks_blank_slide_with_only_transparent_image(self) -> None:
|
|
2500
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2501
|
+
"""
|
|
2502
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2503
|
+
<data>
|
|
2504
|
+
<img id="ghost" src="token" topLeftX="60" topLeftY="60" width="200" height="200" alpha="0"/>
|
|
2505
|
+
</data>
|
|
2506
|
+
</slide>
|
|
2507
|
+
"""
|
|
2508
|
+
)
|
|
2509
|
+
|
|
2510
|
+
self.assertEqual(result["summary"]["error_count"], 1)
|
|
2511
|
+
issue = result["slides"][0]["errors"][0]
|
|
2512
|
+
self.assertEqual(issue["code"], "blank_slide")
|
|
2513
|
+
|
|
2514
|
+
def test_lint_xml_warns_when_large_container_is_mostly_empty(self) -> None:
|
|
2515
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2516
|
+
"""
|
|
2517
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2518
|
+
<data>
|
|
2519
|
+
<shape id="trend-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="370"/>
|
|
2520
|
+
<shape id="trend-title" type="text" topLeftX="515" topLeftY="147" width="380" height="28">
|
|
2521
|
+
<content fontSize="15"><p>Core trends</p></content>
|
|
2522
|
+
</shape>
|
|
2523
|
+
<shape id="trend-copy" type="text" topLeftX="515" topLeftY="177" width="380" height="315">
|
|
2524
|
+
<content fontSize="12"><p>First point</p><p>Second point</p><p>Third point</p></content>
|
|
2525
|
+
</shape>
|
|
2526
|
+
</data>
|
|
2527
|
+
</slide>
|
|
2528
|
+
"""
|
|
2529
|
+
)
|
|
2530
|
+
|
|
2531
|
+
issue = result["slides"][0]["issues"][0]
|
|
2532
|
+
self.assertEqual(issue["code"], "sparse_container_content")
|
|
2533
|
+
self.assertEqual(issue["target"]["container_id"], "trend-card")
|
|
2534
|
+
self.assertEqual(issue["target"], {
|
|
2535
|
+
"slide_number": 1,
|
|
2536
|
+
"container_id": "trend-card",
|
|
2537
|
+
"container_type": "rect",
|
|
2538
|
+
"bbox": {"x": 500, "y": 135, "width": 410, "height": 370},
|
|
2539
|
+
})
|
|
2540
|
+
self.assertLess(issue["measurement"]["content_coverage_ratio"], 0.15)
|
|
2541
|
+
self.assertEqual(issue["rule"], {
|
|
2542
|
+
"name": "large_container_visible_content_coverage",
|
|
2543
|
+
"threshold": 0.15,
|
|
2544
|
+
"comparison": "content_coverage_ratio < threshold",
|
|
2545
|
+
"id": "sparse_container_content",
|
|
2546
|
+
})
|
|
2547
|
+
self.assertEqual(issue["measurement"]["container_area"], 151700)
|
|
2548
|
+
self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.03)
|
|
2549
|
+
self.assertEqual(issue["elements"], ["trend-card", "trend-title", "trend-copy"])
|
|
2550
|
+
self.assertEqual(issue["element_ids"], ["trend-card", "trend-title", "trend-copy"])
|
|
2551
|
+
self.assertEqual(
|
|
2552
|
+
[obj["element_id"] for obj in issue["related_objects"]],
|
|
2553
|
+
["trend-card", "trend-title", "trend-copy"],
|
|
2554
|
+
)
|
|
2555
|
+
self.assertEqual(result["slides"][0]["status"], "needs_screenshot_review")
|
|
2556
|
+
self.assertEqual(result["slides"][0]["warnings"], result["slides"][0]["issues"])
|
|
2557
|
+
|
|
2558
|
+
def test_lint_xml_warns_for_sparse_short_cards(self) -> None:
|
|
2559
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2560
|
+
"""
|
|
2561
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2562
|
+
<data>
|
|
2563
|
+
<shape id="card-1" type="rect" topLeftX="60" topLeftY="180" width="400" height="105"/>
|
|
2564
|
+
<shape id="text-1" type="text" topLeftX="80" topLeftY="220" width="360" height="30">
|
|
2565
|
+
<content fontSize="14"><p>期待认识大家</p></content>
|
|
2566
|
+
</shape>
|
|
2567
|
+
<shape id="card-2" type="rect" topLeftX="490" topLeftY="180" width="400" height="105"/>
|
|
2568
|
+
<shape id="text-2" type="text" topLeftX="510" topLeftY="220" width="360" height="30">
|
|
2569
|
+
<content fontSize="14"><p>化学一起讨论</p></content>
|
|
2570
|
+
</shape>
|
|
2571
|
+
<shape id="card-3" type="rect" topLeftX="60" topLeftY="310" width="400" height="105"/>
|
|
2572
|
+
<shape id="text-3" type="text" topLeftX="80" topLeftY="350" width="360" height="30">
|
|
2573
|
+
<content fontSize="14"><p>吉他随时交流</p></content>
|
|
2574
|
+
</shape>
|
|
2575
|
+
<shape id="card-4" type="rect" topLeftX="490" topLeftY="310" width="400" height="105"/>
|
|
2576
|
+
<shape id="text-4" type="text" topLeftX="510" topLeftY="350" width="360" height="30">
|
|
2577
|
+
<content fontSize="14"><p>共度美好四年</p></content>
|
|
2578
|
+
</shape>
|
|
2579
|
+
</data>
|
|
2580
|
+
</slide>
|
|
2581
|
+
"""
|
|
2582
|
+
)
|
|
2583
|
+
|
|
2584
|
+
container_issues = [
|
|
2585
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
2586
|
+
]
|
|
2587
|
+
self.assertEqual(
|
|
2588
|
+
[issue["target"]["container_id"] for issue in container_issues],
|
|
2589
|
+
["card-1", "card-2", "card-3", "card-4"],
|
|
2590
|
+
)
|
|
2591
|
+
self.assertTrue(all(issue["target"]["bbox"]["height"] == 105 for issue in container_issues))
|
|
2592
|
+
self.assertTrue(all(issue["measurement"]["content_coverage_ratio"] < 0.15 for issue in container_issues))
|
|
2593
|
+
self.assertEqual(
|
|
2594
|
+
[issue["code"] for issue in result["slides"][0]["issues"]],
|
|
2595
|
+
[
|
|
2596
|
+
"sparse_container_content",
|
|
2597
|
+
"sparse_container_content",
|
|
2598
|
+
"sparse_container_content",
|
|
2599
|
+
"sparse_container_content",
|
|
2600
|
+
"sparse_slide_content",
|
|
2601
|
+
],
|
|
2602
|
+
)
|
|
2603
|
+
|
|
2604
|
+
def test_lint_xml_warns_when_whole_slide_has_too_little_effective_content(self) -> None:
|
|
2605
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2606
|
+
"""
|
|
2607
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2608
|
+
<data>
|
|
2609
|
+
<shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540"/>
|
|
2610
|
+
<shape id="text-1" type="text" topLeftX="60" topLeftY="80" width="200" height="30">
|
|
2611
|
+
<content fontSize="14"><p>One short line</p></content>
|
|
2612
|
+
</shape>
|
|
2613
|
+
<shape id="text-2" type="text" topLeftX="500" topLeftY="180" width="200" height="30">
|
|
2614
|
+
<content fontSize="14"><p>Another line</p></content>
|
|
2615
|
+
</shape>
|
|
2616
|
+
<shape id="text-3" type="text" topLeftX="60" topLeftY="310" width="200" height="30">
|
|
2617
|
+
<content fontSize="14"><p>Third line</p></content>
|
|
2618
|
+
</shape>
|
|
2619
|
+
<shape id="text-4" type="text" topLeftX="500" topLeftY="410" width="200" height="30">
|
|
2620
|
+
<content fontSize="14"><p>Fourth line</p></content>
|
|
2621
|
+
</shape>
|
|
2622
|
+
</data>
|
|
2623
|
+
</slide>
|
|
2624
|
+
"""
|
|
2625
|
+
)
|
|
2626
|
+
|
|
2627
|
+
issues = [issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_slide_content"]
|
|
2628
|
+
self.assertEqual(len(issues), 1)
|
|
2629
|
+
issue = issues[0]
|
|
2630
|
+
self.assertEqual(issue["target"]["bbox"], {"x": 0, "y": 0, "width": 960, "height": 540})
|
|
2631
|
+
self.assertEqual(issue["rule"]["threshold"], 0.035)
|
|
2632
|
+
self.assertLess(issue["measurement"]["content_coverage_ratio"], 0.035)
|
|
2633
|
+
self.assertEqual(issue["measurement"]["content_element_count"], 4)
|
|
2634
|
+
self.assertNotIn("background", issue["elements"])
|
|
2635
|
+
|
|
2636
|
+
def test_lint_xml_ignores_isolated_short_layout_bar(self) -> None:
|
|
2637
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2638
|
+
"""
|
|
2639
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2640
|
+
<data>
|
|
2641
|
+
<shape id="summary-bar" type="rect" topLeftX="52" topLeftY="82" width="856" height="105"/>
|
|
2642
|
+
<shape id="summary" type="text" topLeftX="72" topLeftY="115" width="816" height="30">
|
|
2643
|
+
<content fontSize="14"><p>One concise summary</p></content>
|
|
2644
|
+
</shape>
|
|
2645
|
+
</data>
|
|
2646
|
+
</slide>
|
|
2647
|
+
"""
|
|
2648
|
+
)
|
|
2649
|
+
|
|
2650
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
2651
|
+
|
|
2652
|
+
def test_lint_xml_counts_rect_own_content_as_visible_content(self) -> None:
|
|
2653
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2654
|
+
"""
|
|
2655
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2656
|
+
<data>
|
|
2657
|
+
<shape id="load-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
2658
|
+
<content fontSize="18">
|
|
2659
|
+
<p>被吊物</p>
|
|
2660
|
+
<p><span fontSize="36">32.0 t</span></p>
|
|
2661
|
+
<p>钢结构模块</p>
|
|
2662
|
+
</content>
|
|
2663
|
+
</shape>
|
|
2664
|
+
</data>
|
|
2665
|
+
</slide>
|
|
2666
|
+
"""
|
|
2667
|
+
)
|
|
2668
|
+
|
|
2669
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
2670
|
+
|
|
2671
|
+
def test_lint_xml_reports_nonzero_coverage_for_rect_own_content_reproduction(self) -> None:
|
|
2672
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2673
|
+
"""
|
|
2674
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2675
|
+
<data>
|
|
2676
|
+
<shape id="load-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
2677
|
+
<content fontSize="18">
|
|
2678
|
+
<p>被吊物</p>
|
|
2679
|
+
<p>32.0 t</p>
|
|
2680
|
+
<p>钢结构模块</p>
|
|
2681
|
+
</content>
|
|
2682
|
+
</shape>
|
|
2683
|
+
</data>
|
|
2684
|
+
</slide>
|
|
2685
|
+
"""
|
|
2686
|
+
)
|
|
2687
|
+
|
|
2688
|
+
issue = result["slides"][0]["issues"][0]
|
|
2689
|
+
self.assertGreater(issue["measurement"]["visible_content_area"], 0)
|
|
2690
|
+
self.assertEqual(issue["measurement"]["content_element_count"], 1)
|
|
2691
|
+
self.assertGreater(issue["measurement"]["content_coverage_ratio"], 0)
|
|
2692
|
+
|
|
2693
|
+
def test_lint_xml_still_warns_for_sparse_rect_own_content(self) -> None:
|
|
2694
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2695
|
+
"""
|
|
2696
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2697
|
+
<data>
|
|
2698
|
+
<shape id="sparse-card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
2699
|
+
<content fontSize="12"><p>A</p></content>
|
|
2700
|
+
</shape>
|
|
2701
|
+
</data>
|
|
2702
|
+
</slide>
|
|
2703
|
+
"""
|
|
2704
|
+
)
|
|
2705
|
+
|
|
2706
|
+
issue = result["slides"][0]["issues"][0]
|
|
2707
|
+
self.assertEqual(issue["target"]["container_id"], "sparse-card")
|
|
2708
|
+
self.assertGreater(issue["measurement"]["visible_content_area"], 0)
|
|
2709
|
+
self.assertEqual(issue["measurement"]["content_element_count"], 1)
|
|
2710
|
+
self.assertEqual(issue["elements"], ["sparse-card"])
|
|
2711
|
+
|
|
2712
|
+
def test_lint_xml_unions_rect_own_content_with_child_content(self) -> None:
|
|
2713
|
+
self_only = xml_text_overlap_lint.lint_xml(
|
|
2714
|
+
"""
|
|
2715
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2716
|
+
<data>
|
|
2717
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
2718
|
+
<content fontSize="12"><p>A</p></content>
|
|
2719
|
+
</shape>
|
|
2720
|
+
</data>
|
|
2721
|
+
</slide>
|
|
2722
|
+
"""
|
|
2723
|
+
)
|
|
2724
|
+
with_overlapping_child = xml_text_overlap_lint.lint_xml(
|
|
2725
|
+
"""
|
|
2726
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2727
|
+
<data>
|
|
2728
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
2729
|
+
<content fontSize="12"><p>A</p></content>
|
|
2730
|
+
</shape>
|
|
2731
|
+
<shape id="child" type="text" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
2732
|
+
<content fontSize="12"><p>A</p></content>
|
|
2733
|
+
</shape>
|
|
2734
|
+
</data>
|
|
2735
|
+
</slide>
|
|
2736
|
+
"""
|
|
2737
|
+
)
|
|
2738
|
+
|
|
2739
|
+
self_issue = self_only["slides"][0]["issues"][0]
|
|
2740
|
+
mixed_issue = with_overlapping_child["slides"][0]["issues"][0]
|
|
2741
|
+
self.assertEqual(
|
|
2742
|
+
mixed_issue["measurement"]["visible_content_area"],
|
|
2743
|
+
self_issue["measurement"]["visible_content_area"],
|
|
2744
|
+
)
|
|
2745
|
+
self.assertEqual(mixed_issue["measurement"]["content_element_count"], 2)
|
|
2746
|
+
|
|
2747
|
+
def test_extract_density_elements_reads_nested_font_size_from_rect_content(self) -> None:
|
|
2748
|
+
elements = xml_text_overlap_lint.extract_density_elements(
|
|
2749
|
+
"""
|
|
2750
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2751
|
+
<data>
|
|
2752
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184">
|
|
2753
|
+
<content fontSize="12"><p><span fontSize="36">32.0 t</span></p></content>
|
|
2754
|
+
</shape>
|
|
2755
|
+
</data>
|
|
2756
|
+
</slide>
|
|
2757
|
+
"""
|
|
2758
|
+
)
|
|
2759
|
+
|
|
2760
|
+
self.assertEqual(elements[0]["fontSize"], 36)
|
|
2761
|
+
|
|
2762
|
+
def test_extract_density_elements_does_not_attach_following_text_to_self_closing_rect(self) -> None:
|
|
2763
|
+
elements = xml_text_overlap_lint.extract_density_elements(
|
|
2764
|
+
"""
|
|
2765
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2766
|
+
<data>
|
|
2767
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184"/>
|
|
2768
|
+
<shape id="title" type="text" topLeftX="80" topLeftY="160" width="180" height="30">
|
|
2769
|
+
<content fontSize="18"><p>Following title</p></content>
|
|
2770
|
+
</shape>
|
|
2771
|
+
</data>
|
|
2772
|
+
</slide>
|
|
2773
|
+
"""
|
|
2774
|
+
)
|
|
2775
|
+
|
|
2776
|
+
self.assertEqual(elements[0]["text"], "")
|
|
2777
|
+
self.assertEqual(elements[1]["text"], "Following title")
|
|
2778
|
+
|
|
2779
|
+
def test_lint_xml_allows_container_with_large_visual_child(self) -> None:
|
|
2780
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2781
|
+
"""
|
|
2782
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2783
|
+
<data>
|
|
2784
|
+
<shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
|
|
2785
|
+
<chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220">
|
|
2786
|
+
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
|
2787
|
+
<chartData>
|
|
2788
|
+
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
|
|
2789
|
+
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
|
|
2790
|
+
</chartData>
|
|
2791
|
+
</chart>
|
|
2792
|
+
</data>
|
|
2793
|
+
</slide>
|
|
2794
|
+
"""
|
|
2795
|
+
)
|
|
2796
|
+
|
|
2797
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
2798
|
+
|
|
2799
|
+
def test_lint_xml_does_not_let_transparent_visual_child_suppress_sparse_warning(self) -> None:
|
|
2800
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2801
|
+
"""
|
|
2802
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2803
|
+
<data>
|
|
2804
|
+
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
|
|
2805
|
+
<content fontSize="20"><p>Section title</p></content>
|
|
2806
|
+
</shape>
|
|
2807
|
+
<shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
|
|
2808
|
+
<chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220" alpha="0">
|
|
2809
|
+
<chartPlotArea><chartPlot type="line"/></chartPlotArea>
|
|
2810
|
+
<chartData>
|
|
2811
|
+
<dim1><chartField name="category" valueType="string">A</chartField></dim1>
|
|
2812
|
+
<dim2><chartField name="value" valueType="number">1</chartField></dim2>
|
|
2813
|
+
</chartData>
|
|
2814
|
+
</chart>
|
|
2815
|
+
</data>
|
|
2816
|
+
</slide>
|
|
2817
|
+
"""
|
|
2818
|
+
)
|
|
2819
|
+
|
|
2820
|
+
issue = next(
|
|
2821
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
2822
|
+
)
|
|
2823
|
+
self.assertEqual(issue["target"]["container_id"], "chart-card")
|
|
2824
|
+
|
|
2825
|
+
def test_lint_xml_warns_for_small_empty_visual_placeholder_cards(self) -> None:
|
|
2826
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2827
|
+
"""
|
|
2828
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2829
|
+
<data>
|
|
2830
|
+
<shape id="letter-placeholder" type="rect" topLeftX="520" topLeftY="180" width="200" height="200"/>
|
|
2831
|
+
<shape id="letter" type="text" topLeftX="540" topLeftY="250" width="160" height="70">
|
|
2832
|
+
<content fontSize="46"><p>Z</p></content>
|
|
2833
|
+
</shape>
|
|
2834
|
+
<shape id="empty-placeholder" type="rect" topLeftX="744" topLeftY="180" width="144" height="200"/>
|
|
2835
|
+
</data>
|
|
2836
|
+
</slide>
|
|
2837
|
+
"""
|
|
2838
|
+
)
|
|
2839
|
+
|
|
2840
|
+
issues = result["slides"][0]["issues"]
|
|
2841
|
+
self.assertEqual(
|
|
2842
|
+
[issue["target"]["container_id"] for issue in issues],
|
|
2843
|
+
["letter-placeholder", "empty-placeholder"],
|
|
2844
|
+
)
|
|
2845
|
+
self.assertEqual(issues[1]["measurement"]["content_element_count"], 0)
|
|
2846
|
+
|
|
2847
|
+
def test_lint_xml_applies_global_threshold_to_normal_text_card(self) -> None:
|
|
2848
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2849
|
+
"""
|
|
2850
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2851
|
+
<data>
|
|
2852
|
+
<shape id="card" type="rect" topLeftX="70" topLeftY="184" width="260" height="288"/>
|
|
2853
|
+
<shape id="title" type="text" topLeftX="90" topLeftY="215" width="220" height="30">
|
|
2854
|
+
<content fontSize="18"><p>梦境与现实</p></content>
|
|
2855
|
+
</shape>
|
|
2856
|
+
<shape id="copy" type="text" topLeftX="90" topLeftY="330" width="220" height="70">
|
|
2857
|
+
<content fontSize="13"><p>边界溶解,逻辑失效。观众被拽入潜意识的迷宫。</p></content>
|
|
2858
|
+
</shape>
|
|
2859
|
+
</data>
|
|
2860
|
+
</slide>
|
|
2861
|
+
"""
|
|
2862
|
+
)
|
|
2863
|
+
|
|
2864
|
+
issue = result["slides"][0]["issues"][0]
|
|
2865
|
+
self.assertEqual(issue["target"]["container_id"], "card")
|
|
2866
|
+
self.assertEqual(issue["rule"]["threshold"], 0.15)
|
|
2867
|
+
|
|
2868
|
+
def test_lint_xml_allows_image_overlay_rect(self) -> None:
|
|
2869
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2870
|
+
"""
|
|
2871
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2872
|
+
<data>
|
|
2873
|
+
<img id="hero" topLeftX="560" topLeftY="0" width="400" height="540"/>
|
|
2874
|
+
<shape id="tint" type="rect" topLeftX="560" topLeftY="0" width="400" height="540"/>
|
|
2875
|
+
</data>
|
|
2876
|
+
</slide>
|
|
2877
|
+
"""
|
|
2878
|
+
)
|
|
2879
|
+
|
|
2880
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
2881
|
+
|
|
2882
|
+
def test_lint_xml_does_not_let_transparent_image_overlay_suppress_sparse_warning(self) -> None:
|
|
2883
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2884
|
+
"""
|
|
2885
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2886
|
+
<data>
|
|
2887
|
+
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
|
|
2888
|
+
<content fontSize="20"><p>Section title</p></content>
|
|
2889
|
+
</shape>
|
|
2890
|
+
<shape id="card" type="rect" topLeftX="330" topLeftY="120" width="300" height="300"/>
|
|
2891
|
+
<img id="ghost-overlay" src="token" topLeftX="330" topLeftY="120" width="300" height="300" alpha="0"/>
|
|
2892
|
+
</data>
|
|
2893
|
+
</slide>
|
|
2894
|
+
"""
|
|
2895
|
+
)
|
|
2896
|
+
|
|
2897
|
+
issue = next(
|
|
2898
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
2899
|
+
)
|
|
2900
|
+
self.assertEqual(issue["target"]["container_id"], "card")
|
|
2901
|
+
|
|
2902
|
+
def test_lint_xml_allows_edge_spanning_layout_panel_and_nested_decoration(self) -> None:
|
|
2903
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2904
|
+
"""
|
|
2905
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2906
|
+
<data>
|
|
2907
|
+
<shape id="panel" type="rect" topLeftX="600" topLeftY="0" width="360" height="540"/>
|
|
2908
|
+
<shape id="decoration" type="rect" topLeftX="660" topLeftY="150" width="240" height="240"/>
|
|
2909
|
+
</data>
|
|
2910
|
+
</slide>
|
|
2911
|
+
"""
|
|
2912
|
+
)
|
|
2913
|
+
|
|
2914
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
2915
|
+
|
|
2916
|
+
def test_lint_xml_counts_icons_as_visible_content(self) -> None:
|
|
2917
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2918
|
+
"""
|
|
2919
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2920
|
+
<data>
|
|
2921
|
+
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="320" height="240"/>
|
|
2922
|
+
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="180" height="180">
|
|
2923
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
2924
|
+
</icon>
|
|
2925
|
+
</data>
|
|
2926
|
+
</slide>
|
|
2927
|
+
"""
|
|
2928
|
+
)
|
|
2929
|
+
|
|
2930
|
+
self.assertEqual(result["summary"]["warning_count"], 0)
|
|
2931
|
+
|
|
2932
|
+
def test_lint_xml_does_not_count_transparent_icon_as_visible_content(self) -> None:
|
|
2933
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2934
|
+
"""
|
|
2935
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2936
|
+
<data>
|
|
2937
|
+
<shape id="title" type="text" topLeftX="40" topLeftY="40" width="300" height="40">
|
|
2938
|
+
<content fontSize="20"><p>Section title</p></content>
|
|
2939
|
+
</shape>
|
|
2940
|
+
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="320" height="240"/>
|
|
2941
|
+
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="180" height="180" alpha="0">
|
|
2942
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
2943
|
+
</icon>
|
|
2944
|
+
</data>
|
|
2945
|
+
</slide>
|
|
2946
|
+
"""
|
|
2947
|
+
)
|
|
2948
|
+
|
|
2949
|
+
issue = next(
|
|
2950
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
2951
|
+
)
|
|
2952
|
+
self.assertEqual(issue["target"]["container_id"], "card")
|
|
2953
|
+
self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0)
|
|
2954
|
+
|
|
2955
|
+
def test_lint_xml_warns_when_coverage_is_below_global_threshold(self) -> None:
|
|
2956
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2957
|
+
"""
|
|
2958
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2959
|
+
<data>
|
|
2960
|
+
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="200" height="200"/>
|
|
2961
|
+
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="70" height="70">
|
|
2962
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
2963
|
+
</icon>
|
|
2964
|
+
</data>
|
|
2965
|
+
</slide>
|
|
2966
|
+
"""
|
|
2967
|
+
)
|
|
2968
|
+
|
|
2969
|
+
issue = result["slides"][0]["issues"][0]
|
|
2970
|
+
self.assertEqual(issue["target"]["container_id"], "card")
|
|
2971
|
+
self.assertEqual(issue["measurement"]["content_coverage_ratio"], 0.122)
|
|
2972
|
+
self.assertEqual(issue["rule"]["threshold"], 0.15)
|
|
2973
|
+
|
|
2974
|
+
def test_lint_xml_allows_quarter_coverage_under_lower_threshold(self) -> None:
|
|
2975
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2976
|
+
"""
|
|
2977
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2978
|
+
<data>
|
|
2979
|
+
<shape id="card" type="rect" topLeftX="80" topLeftY="140" width="200" height="200"/>
|
|
2980
|
+
<icon id="visual" iconType="iconpark/Safe/shield.svg" topLeftX="100" topLeftY="160" width="100" height="100">
|
|
2981
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
2982
|
+
</icon>
|
|
2983
|
+
</data>
|
|
2984
|
+
</slide>
|
|
2985
|
+
"""
|
|
2986
|
+
)
|
|
2987
|
+
|
|
2988
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
2989
|
+
|
|
2990
|
+
def test_lint_xml_allows_large_metric_card_above_lower_threshold(self) -> None:
|
|
2991
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
2992
|
+
"""
|
|
2993
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
2994
|
+
<data>
|
|
2995
|
+
<shape id="metric-card" type="rect" topLeftX="80" topLeftY="140" width="360" height="300"/>
|
|
2996
|
+
<shape id="metric" type="text" topLeftX="104" topLeftY="190" width="340" height="90">
|
|
2997
|
+
<content fontSize="12"><p><strong><span fontSize="62">400</span></strong>+ 项</p></content>
|
|
2998
|
+
</shape>
|
|
2999
|
+
</data>
|
|
3000
|
+
</slide>
|
|
3001
|
+
"""
|
|
3002
|
+
)
|
|
3003
|
+
|
|
3004
|
+
self.assertEqual(result["slides"][0]["issues"], [])
|
|
3005
|
+
|
|
3006
|
+
def test_lint_xml_does_not_report_blank_slide_for_line_only_content(self) -> None:
|
|
3007
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
3008
|
+
"""
|
|
3009
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
3010
|
+
<data>
|
|
3011
|
+
<line id="l1" startX="100" startY="100" endX="800" endY="100"><border/></line>
|
|
3012
|
+
<line id="l2" startX="100" startY="200" endX="800" endY="200"><border/></line>
|
|
3013
|
+
<line id="l3" startX="100" startY="300" endX="800" endY="300"><border/></line>
|
|
3014
|
+
<line id="l4" startX="100" startY="400" endX="800" endY="400"><border/></line>
|
|
3015
|
+
</data>
|
|
3016
|
+
</slide>
|
|
3017
|
+
"""
|
|
3018
|
+
)
|
|
3019
|
+
|
|
3020
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
3021
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
3022
|
+
self.assertNotIn("blank_slide", codes)
|
|
3023
|
+
|
|
3024
|
+
def test_lint_xml_reports_bbox_overlap_measurement_from_decision_time_visual_bbox(self) -> None:
|
|
3025
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
3026
|
+
"""
|
|
3027
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
3028
|
+
<data>
|
|
3029
|
+
<shape id="left" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
|
|
3030
|
+
<content fontSize="14"><p>overlap text <span fontSize="96">big</span></p></content>
|
|
3031
|
+
</shape>
|
|
3032
|
+
<shape id="right" type="text" topLeftX="80" topLeftY="80" width="300" height="80">
|
|
3033
|
+
<content fontSize="14"><p>other overlap text</p></content>
|
|
3034
|
+
</shape>
|
|
3035
|
+
</data>
|
|
3036
|
+
</slide>
|
|
3037
|
+
"""
|
|
3038
|
+
)
|
|
3039
|
+
|
|
3040
|
+
issue = result["slides"][0]["issues"][0]
|
|
3041
|
+
self.assertEqual(issue["code"], "bbox_overlap")
|
|
3042
|
+
# Must match the visual bbox that should_flag_overlap actually decided with (fontSize=14
|
|
3043
|
+
# from extract_elements), not the fontSize=96 max-descendant value that
|
|
3044
|
+
# extract_density_elements computes for the same "left" element id.
|
|
3045
|
+
self.assertEqual(issue["measurement"]["intersection_width"], 109.2)
|
|
3046
|
+
self.assertEqual(issue["measurement"]["intersection_height"], 6.8)
|
|
3047
|
+
self.assertEqual(issue["measurement"]["intersection_area"], 742.56)
|
|
3048
|
+
|
|
3049
|
+
def test_has_similar_short_card_peer_excludes_the_element_itself(self) -> None:
|
|
3050
|
+
card_a = {"kind": "shape", "type": "rect", "x": 0, "y": 0, "width": 300, "height": 100}
|
|
3051
|
+
card_b = {"kind": "shape", "type": "rect", "x": 400, "y": 0, "width": 300, "height": 100}
|
|
3052
|
+
card_c = {"kind": "shape", "type": "rect", "x": 0, "y": 200, "width": 300, "height": 100}
|
|
3053
|
+
|
|
3054
|
+
self.assertFalse(
|
|
3055
|
+
xml_text_overlap_lint.has_similar_short_card_peer(card_a, [card_a, card_b])
|
|
3056
|
+
)
|
|
3057
|
+
self.assertTrue(
|
|
3058
|
+
xml_text_overlap_lint.has_similar_short_card_peer(card_a, [card_a, card_b, card_c])
|
|
3059
|
+
)
|
|
3060
|
+
|
|
3061
|
+
def test_lint_xml_reports_schema_version_2_for_sparse_issues(self) -> None:
|
|
3062
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
3063
|
+
"""
|
|
3064
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
3065
|
+
<data>
|
|
3066
|
+
<shape id="card" type="rect" topLeftX="60" topLeftY="140" width="220" height="184"/>
|
|
3067
|
+
</data>
|
|
3068
|
+
</slide>
|
|
3069
|
+
"""
|
|
3070
|
+
)
|
|
3071
|
+
|
|
3072
|
+
issue = next(
|
|
3073
|
+
issue for issue in result["slides"][0]["issues"] if issue["code"] == "sparse_container_content"
|
|
3074
|
+
)
|
|
3075
|
+
self.assertEqual(issue["schema_version"], "2.0")
|
|
3076
|
+
|
|
3077
|
+
def test_lint_xml_does_not_report_blank_slide_for_textless_decorative_shapes(self) -> None:
|
|
3078
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
3079
|
+
"""
|
|
3080
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
3081
|
+
<data>
|
|
3082
|
+
<shape id="deco1" type="ellipse" topLeftX="60" topLeftY="60" width="300" height="300">
|
|
3083
|
+
<fill><fillColor color="rgba(37, 99, 235, 1)"/></fill>
|
|
3084
|
+
</shape>
|
|
3085
|
+
<shape id="deco2" type="triangle" topLeftX="500" topLeftY="200" width="200" height="200">
|
|
3086
|
+
<fill><fillColor color="rgba(220, 38, 38, 1)"/></fill>
|
|
3087
|
+
</shape>
|
|
3088
|
+
</data>
|
|
3089
|
+
</slide>
|
|
3090
|
+
"""
|
|
3091
|
+
)
|
|
3092
|
+
|
|
3093
|
+
self.assertEqual(result["summary"]["error_count"], 0)
|
|
3094
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
3095
|
+
self.assertNotIn("blank_slide", codes)
|
|
3096
|
+
|
|
3097
|
+
def test_lint_xml_still_warns_for_sparse_slide_content_despite_full_bleed_background(self) -> None:
|
|
3098
|
+
# A plain textless shape now counts as "not blank" (see the test above), but a
|
|
3099
|
+
# full-bleed background rect must still NOT count toward sparse_slide_content's
|
|
3100
|
+
# meaningful-content coverage ratio -- otherwise every slide with a background would
|
|
3101
|
+
# trivially "pass" that density check.
|
|
3102
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
3103
|
+
"""
|
|
3104
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
3105
|
+
<data>
|
|
3106
|
+
<shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540"/>
|
|
3107
|
+
<shape id="text-1" type="text" topLeftX="60" topLeftY="80" width="200" height="30">
|
|
3108
|
+
<content fontSize="14"><p>One short line</p></content>
|
|
3109
|
+
</shape>
|
|
3110
|
+
<shape id="text-2" type="text" topLeftX="500" topLeftY="180" width="200" height="30">
|
|
3111
|
+
<content fontSize="14"><p>Another line</p></content>
|
|
3112
|
+
</shape>
|
|
3113
|
+
<shape id="text-3" type="text" topLeftX="60" topLeftY="310" width="200" height="30">
|
|
3114
|
+
<content fontSize="14"><p>Third line</p></content>
|
|
3115
|
+
</shape>
|
|
3116
|
+
<shape id="text-4" type="text" topLeftX="500" topLeftY="410" width="200" height="30">
|
|
3117
|
+
<content fontSize="14"><p>Fourth line</p></content>
|
|
3118
|
+
</shape>
|
|
3119
|
+
</data>
|
|
3120
|
+
</slide>
|
|
3121
|
+
"""
|
|
3122
|
+
)
|
|
3123
|
+
|
|
3124
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
3125
|
+
self.assertIn("sparse_slide_content", codes)
|
|
3126
|
+
|
|
3127
|
+
def test_lint_xml_accepts_whitespace_around_attribute_equals_sign(self) -> None:
|
|
3128
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
3129
|
+
"""
|
|
3130
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
3131
|
+
<data>
|
|
3132
|
+
<shape id="visible" type="text" topLeftX = "80" topLeftY = "80" width = "300" height = "60">
|
|
3133
|
+
<content><p>hello</p></content>
|
|
3134
|
+
</shape>
|
|
3135
|
+
</data>
|
|
3136
|
+
</slide>
|
|
3137
|
+
"""
|
|
3138
|
+
)
|
|
3139
|
+
|
|
3140
|
+
self.assertEqual(result["slides"][0]["element_count"], 1)
|
|
3141
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
3142
|
+
self.assertNotIn("blank_slide", codes)
|
|
3143
|
+
|
|
3144
|
+
def test_lint_xml_reports_blank_slide_for_full_canvas_background_only(self) -> None:
|
|
3145
|
+
result = xml_text_overlap_lint.lint_xml(
|
|
3146
|
+
"""
|
|
3147
|
+
<slide xmlns="http://www.larkoffice.com/sml/2.0">
|
|
3148
|
+
<data>
|
|
3149
|
+
<shape id="background" type="rect" topLeftX="0" topLeftY="0" width="960" height="540">
|
|
3150
|
+
<fill><fillColor color="rgba(240, 235, 220, 1)"/></fill>
|
|
3151
|
+
</shape>
|
|
3152
|
+
</data>
|
|
3153
|
+
</slide>
|
|
3154
|
+
"""
|
|
3155
|
+
)
|
|
3156
|
+
|
|
3157
|
+
codes = [issue["code"] for issue in result["slides"][0]["issues"]]
|
|
3158
|
+
self.assertIn("blank_slide", codes)
|
|
3159
|
+
|
|
3160
|
+
def test_has_similar_short_card_peer_ignores_invisible_peers(self) -> None:
|
|
3161
|
+
visible_card = {"kind": "shape", "type": "rect", "x": 0, "y": 0, "width": 300, "height": 100}
|
|
3162
|
+
ghost_1 = {
|
|
3163
|
+
"kind": "shape", "type": "rect", "x": 400, "y": 0, "width": 300, "height": 100, "alpha": 0,
|
|
3164
|
+
}
|
|
3165
|
+
ghost_2 = {
|
|
3166
|
+
"kind": "shape", "type": "rect", "x": 800, "y": 0, "width": 300, "height": 100, "alpha": 0,
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
self.assertFalse(
|
|
3170
|
+
xml_text_overlap_lint.has_similar_short_card_peer(
|
|
3171
|
+
visible_card, [visible_card, ghost_1, ghost_2]
|
|
3172
|
+
)
|
|
3173
|
+
)
|
|
3174
|
+
|
|
3175
|
+
|
|
3176
|
+
SML_NAMESPACE = "http://www.larkoffice.com/sml/2.0"
|
|
3177
|
+
|
|
3178
|
+
|
|
3179
|
+
class SxsdSyntaxTestCase(unittest.TestCase):
|
|
3180
|
+
def validate(self, xml: str) -> list[dict[str, object]]:
|
|
3181
|
+
result = xml_text_overlap_lint.lint_xml(xml)
|
|
3182
|
+
return [
|
|
3183
|
+
*result.get("issues", []),
|
|
3184
|
+
*(issue for slide in result["slides"] for issue in slide["issues"]),
|
|
3185
|
+
]
|
|
3186
|
+
|
|
3187
|
+
def assert_issue(
|
|
3188
|
+
self,
|
|
3189
|
+
issues: list[dict[str, object]],
|
|
3190
|
+
code: str,
|
|
3191
|
+
*,
|
|
3192
|
+
path: str | None = None,
|
|
3193
|
+
attr: str | None = None,
|
|
3194
|
+
) -> dict[str, object]:
|
|
3195
|
+
for issue in issues:
|
|
3196
|
+
if issue.get("code") != code:
|
|
3197
|
+
continue
|
|
3198
|
+
if path is not None and issue.get("path") != path:
|
|
3199
|
+
continue
|
|
3200
|
+
if attr is not None and issue.get("attr") != attr:
|
|
3201
|
+
continue
|
|
3202
|
+
return issue
|
|
3203
|
+
self.fail(f"missing issue code={code!r} path={path!r} attr={attr!r}: {issues!r}")
|
|
3204
|
+
|
|
3205
|
+
def assert_no_issue(self, issues: list[dict[str, object]], code: str) -> None:
|
|
3206
|
+
self.assertNotIn(code, [issue.get("code") for issue in issues])
|
|
3207
|
+
|
|
3208
|
+
|
|
3209
|
+
class SxsdSyntaxAttributeTest(SxsdSyntaxTestCase):
|
|
3210
|
+
|
|
3211
|
+
def test_xsd_pattern_translation_only_expands_whitespace_classes(self) -> None:
|
|
3212
|
+
self.assertEqual(
|
|
3213
|
+
sxsd_validator.python_pattern_for_xsd(r"\s+\S+\w+\d+"),
|
|
3214
|
+
"[ \\t\\n\\r]+[^ \\t\\n\\r]+\\w+\\d+",
|
|
3215
|
+
)
|
|
3216
|
+
|
|
3217
|
+
def test_href_domain_pattern_does_not_use_backtracking_regex(self) -> None:
|
|
3218
|
+
pattern = r"[\w.-]+[.:]\S*"
|
|
3219
|
+
adversarial_value = ("a." * 20_000) + " "
|
|
3220
|
+
original_fullmatch = sxsd_validator.re.fullmatch
|
|
3221
|
+
translated_pattern = sxsd_validator.python_pattern_for_xsd(pattern)
|
|
3222
|
+
|
|
3223
|
+
def reject_unsafe_pattern(candidate: str, value: str):
|
|
3224
|
+
if candidate == translated_pattern:
|
|
3225
|
+
raise AssertionError("href domain pattern must not use re.fullmatch")
|
|
3226
|
+
return original_fullmatch(candidate, value)
|
|
3227
|
+
|
|
3228
|
+
with mock.patch.object(sxsd_validator.re, "fullmatch", side_effect=reject_unsafe_pattern):
|
|
3229
|
+
self.assertFalse(sxsd_validator.xsd_pattern_matches(pattern, adversarial_value))
|
|
3230
|
+
|
|
3231
|
+
def test_href_domain_pattern_keeps_xsd_matching_behavior(self) -> None:
|
|
3232
|
+
pattern = r"[\w.-]+[.:]\S*"
|
|
3233
|
+
reference_pattern = sxsd_validator.re.compile(
|
|
3234
|
+
sxsd_validator.python_pattern_for_xsd(pattern)
|
|
3235
|
+
)
|
|
3236
|
+
|
|
3237
|
+
for length in range(5):
|
|
3238
|
+
for characters in itertools.product("a.:-/ ©", repeat=length):
|
|
3239
|
+
value = "".join(characters)
|
|
3240
|
+
with self.subTest(value=value):
|
|
3241
|
+
self.assertEqual(
|
|
3242
|
+
sxsd_validator.xsd_pattern_matches(pattern, value),
|
|
3243
|
+
reference_pattern.fullmatch(value) is not None,
|
|
3244
|
+
)
|
|
3245
|
+
|
|
3246
|
+
def test_accepts_valid_shape_attributes(self) -> None:
|
|
3247
|
+
issues = self.validate(
|
|
3248
|
+
f"""
|
|
3249
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3250
|
+
<data>
|
|
3251
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3252
|
+
<content textType="body"><p>Valid</p></content>
|
|
3253
|
+
</shape>
|
|
3254
|
+
</data>
|
|
3255
|
+
</slide>
|
|
3256
|
+
"""
|
|
3257
|
+
)
|
|
3258
|
+
|
|
3259
|
+
self.assertEqual(issues, [])
|
|
3260
|
+
|
|
3261
|
+
def test_reports_missing_required_shape_attribute(self) -> None:
|
|
3262
|
+
issues = self.validate(
|
|
3263
|
+
f"""
|
|
3264
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3265
|
+
<data><shape type="text" topLeftX="10" topLeftY="20" width="300"/></data>
|
|
3266
|
+
</slide>
|
|
3267
|
+
"""
|
|
3268
|
+
)
|
|
3269
|
+
|
|
3270
|
+
issue = self.assert_issue(
|
|
3271
|
+
issues,
|
|
3272
|
+
"sxsd_missing_required_attr",
|
|
3273
|
+
path="slide/data/shape",
|
|
3274
|
+
attr="height",
|
|
3275
|
+
)
|
|
3276
|
+
self.assertEqual(issue["expected"], "required attribute of type PositiveSize")
|
|
3277
|
+
self.assertIsNone(issue["actual"])
|
|
3278
|
+
|
|
3279
|
+
def test_reports_invalid_scalar_value(self) -> None:
|
|
3280
|
+
issues = self.validate(
|
|
3281
|
+
f"""
|
|
3282
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3283
|
+
<data>
|
|
3284
|
+
<shape type="text" topLeftX="NaN" topLeftY="20" width="300" height="80"/>
|
|
3285
|
+
</data>
|
|
3286
|
+
</slide>
|
|
3287
|
+
"""
|
|
3288
|
+
)
|
|
3289
|
+
|
|
3290
|
+
issue = self.assert_issue(issues, "sxsd_invalid_scalar", attr="topLeftX")
|
|
3291
|
+
self.assertEqual(issue["actual"], "NaN")
|
|
3292
|
+
|
|
3293
|
+
def test_rejects_python_only_numeric_separator(self) -> None:
|
|
3294
|
+
issues = self.validate(
|
|
3295
|
+
f"""
|
|
3296
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3297
|
+
<data>
|
|
3298
|
+
<shape type="rect" topLeftX="1_0" topLeftY="20" width="300" height="80"/>
|
|
3299
|
+
</data>
|
|
3300
|
+
</slide>
|
|
3301
|
+
"""
|
|
3302
|
+
)
|
|
3303
|
+
|
|
3304
|
+
self.assert_issue(issues, "sxsd_invalid_scalar", attr="topLeftX")
|
|
3305
|
+
|
|
3306
|
+
def test_accepts_xsd_double_lexical_forms(self) -> None:
|
|
3307
|
+
for top_left_x in ("10", "-0.5", ".5", "1.", "1e2"):
|
|
3308
|
+
with self.subTest(top_left_x=top_left_x):
|
|
3309
|
+
issues = self.validate(
|
|
3310
|
+
f"""
|
|
3311
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3312
|
+
<data>
|
|
3313
|
+
<shape type="rect" topLeftX="{top_left_x}" topLeftY="20" width="300" height="80"/>
|
|
3314
|
+
</data>
|
|
3315
|
+
</slide>
|
|
3316
|
+
"""
|
|
3317
|
+
)
|
|
3318
|
+
|
|
3319
|
+
self.assertEqual(issues, [])
|
|
3320
|
+
|
|
3321
|
+
def test_accepts_bullet_char_length_boundaries(self) -> None:
|
|
3322
|
+
for bullet_char in ("A", "12345678"):
|
|
3323
|
+
with self.subTest(bullet_char=bullet_char):
|
|
3324
|
+
issues = self.validate(
|
|
3325
|
+
f"""
|
|
3326
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3327
|
+
<data>
|
|
3328
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3329
|
+
<content bulletChar="{bullet_char}"><p>Text</p></content>
|
|
3330
|
+
</shape>
|
|
3331
|
+
</data>
|
|
3332
|
+
</slide>
|
|
3333
|
+
"""
|
|
3334
|
+
)
|
|
3335
|
+
|
|
3336
|
+
self.assertEqual(issues, [])
|
|
3337
|
+
|
|
3338
|
+
def test_rejects_bullet_char_outside_length_boundaries(self) -> None:
|
|
3339
|
+
for bullet_char in ("", "123456789"):
|
|
3340
|
+
with self.subTest(bullet_char=bullet_char):
|
|
3341
|
+
issues = self.validate(
|
|
3342
|
+
f"""
|
|
3343
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3344
|
+
<data>
|
|
3345
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3346
|
+
<content bulletChar="{bullet_char}"><p>Text</p></content>
|
|
3347
|
+
</shape>
|
|
3348
|
+
</data>
|
|
3349
|
+
</slide>
|
|
3350
|
+
"""
|
|
3351
|
+
)
|
|
3352
|
+
|
|
3353
|
+
self.assert_issue(issues, "sxsd_value_out_of_range", attr="bulletChar")
|
|
3354
|
+
|
|
3355
|
+
def test_rejects_zero_size_that_violates_xsd(self) -> None:
|
|
3356
|
+
issues = self.validate(
|
|
3357
|
+
f"""
|
|
3358
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3359
|
+
<data>
|
|
3360
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="0" height="80"/>
|
|
3361
|
+
</data>
|
|
3362
|
+
</slide>
|
|
3363
|
+
"""
|
|
3364
|
+
)
|
|
3365
|
+
|
|
3366
|
+
self.assert_issue(issues, "sxsd_value_out_of_range", attr="width")
|
|
3367
|
+
|
|
3368
|
+
def test_reports_negative_size_rejected_by_xsd(self) -> None:
|
|
3369
|
+
issues = self.validate(
|
|
3370
|
+
f"""
|
|
3371
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3372
|
+
<data>
|
|
3373
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="-1" height="80"/>
|
|
3374
|
+
</data>
|
|
3375
|
+
</slide>
|
|
3376
|
+
"""
|
|
3377
|
+
)
|
|
3378
|
+
|
|
3379
|
+
self.assert_issue(issues, "sxsd_value_out_of_range", attr="width")
|
|
3380
|
+
|
|
3381
|
+
def test_rejects_shape_enum_that_violates_xsd(self) -> None:
|
|
3382
|
+
issues = self.validate(
|
|
3383
|
+
f"""
|
|
3384
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3385
|
+
<data>
|
|
3386
|
+
<shape type="not-a-shape" topLeftX="10" topLeftY="20" width="300" height="80"/>
|
|
3387
|
+
</data>
|
|
3388
|
+
</slide>
|
|
3389
|
+
"""
|
|
3390
|
+
)
|
|
3391
|
+
|
|
3392
|
+
issue = self.assert_issue(issues, "sxsd_invalid_enum", attr="type")
|
|
3393
|
+
self.assertLess(len(str(issue["message"])), 300)
|
|
3394
|
+
self.assertEqual(issue["actual"], "not-a-shape")
|
|
3395
|
+
|
|
3396
|
+
def test_rejects_rotation_upper_bound_that_violates_xsd(self) -> None:
|
|
3397
|
+
issues = self.validate(
|
|
3398
|
+
f"""
|
|
3399
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3400
|
+
<data>
|
|
3401
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80" rotation="360"/>
|
|
3402
|
+
</data>
|
|
3403
|
+
</slide>
|
|
3404
|
+
"""
|
|
3405
|
+
)
|
|
3406
|
+
|
|
3407
|
+
self.assert_issue(issues, "sxsd_value_out_of_range", attr="rotation")
|
|
3408
|
+
|
|
3409
|
+
def test_rejects_fill_color_that_violates_xsd(self) -> None:
|
|
3410
|
+
issues = self.validate(
|
|
3411
|
+
f"""
|
|
3412
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3413
|
+
<style><fill><fillColor color="red"/></fill></style>
|
|
3414
|
+
<data/>
|
|
3415
|
+
</slide>
|
|
3416
|
+
"""
|
|
3417
|
+
)
|
|
3418
|
+
|
|
3419
|
+
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="color")
|
|
3420
|
+
|
|
3421
|
+
def test_reports_missing_required_image_src(self) -> None:
|
|
3422
|
+
issues = self.validate(
|
|
3423
|
+
f"""
|
|
3424
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3425
|
+
<data><img topLeftX="10" topLeftY="20" width="300" height="80"/></data>
|
|
3426
|
+
</slide>
|
|
3427
|
+
"""
|
|
3428
|
+
)
|
|
3429
|
+
|
|
3430
|
+
self.assert_issue(issues, "sxsd_missing_required_attr", attr="src")
|
|
3431
|
+
|
|
3432
|
+
def test_accepts_inline_attribute_simple_type(self) -> None:
|
|
3433
|
+
issues = self.validate(
|
|
3434
|
+
f"""
|
|
3435
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3436
|
+
<data>
|
|
3437
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3438
|
+
<content><p><a href="https://example.com">Link</a></p></content>
|
|
1150
3439
|
</shape>
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
3440
|
+
</data>
|
|
3441
|
+
</slide>
|
|
3442
|
+
"""
|
|
3443
|
+
)
|
|
3444
|
+
|
|
3445
|
+
self.assertEqual(issues, [])
|
|
3446
|
+
|
|
3447
|
+
def test_reports_inline_attribute_pattern_mismatch(self) -> None:
|
|
3448
|
+
issues = self.validate(
|
|
3449
|
+
f"""
|
|
3450
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3451
|
+
<data>
|
|
3452
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3453
|
+
<content><p><a href="not a uri">Link</a></p></content>
|
|
3454
|
+
</shape>
|
|
3455
|
+
</data>
|
|
3456
|
+
</slide>
|
|
3457
|
+
"""
|
|
3458
|
+
)
|
|
3459
|
+
|
|
3460
|
+
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="href")
|
|
3461
|
+
|
|
3462
|
+
def test_accepts_values_matching_inline_union_members(self) -> None:
|
|
3463
|
+
for bullet_size in ("25%", "100%", "400%", "6", "14", "400"):
|
|
3464
|
+
with self.subTest(bullet_size=bullet_size):
|
|
3465
|
+
issues = self.validate(
|
|
1157
3466
|
f"""
|
|
1158
|
-
<
|
|
1159
|
-
<
|
|
1160
|
-
<
|
|
1161
|
-
|
|
1162
|
-
|
|
3467
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3468
|
+
<data>
|
|
3469
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3470
|
+
<content bulletSize="{bullet_size}"><p>Text</p></content>
|
|
3471
|
+
</shape>
|
|
3472
|
+
</data>
|
|
3473
|
+
</slide>
|
|
1163
3474
|
"""
|
|
1164
3475
|
)
|
|
1165
|
-
|
|
1166
|
-
self.assertEqual(
|
|
3476
|
+
|
|
3477
|
+
self.assertEqual(issues, [])
|
|
3478
|
+
|
|
3479
|
+
def test_rejects_values_outside_inline_union_members(self) -> None:
|
|
3480
|
+
for bullet_size in ("24%", "401%", "5", "401", "abc"):
|
|
3481
|
+
with self.subTest(bullet_size=bullet_size):
|
|
3482
|
+
issues = self.validate(
|
|
3483
|
+
f"""
|
|
3484
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3485
|
+
<data>
|
|
3486
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3487
|
+
<content bulletSize="{bullet_size}"><p>Text</p></content>
|
|
3488
|
+
</shape>
|
|
3489
|
+
</data>
|
|
3490
|
+
</slide>
|
|
3491
|
+
"""
|
|
3492
|
+
)
|
|
3493
|
+
|
|
3494
|
+
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="bulletSize")
|
|
3495
|
+
|
|
3496
|
+
def test_rejects_symbol_outside_python_word_semantics_in_href(self) -> None:
|
|
3497
|
+
issues = self.validate(
|
|
3498
|
+
f"""
|
|
3499
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3500
|
+
<data>
|
|
3501
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3502
|
+
<content><p><a href="©:resource">Link</a></p></content>
|
|
3503
|
+
</shape>
|
|
3504
|
+
</data>
|
|
3505
|
+
</slide>
|
|
3506
|
+
"""
|
|
3507
|
+
)
|
|
3508
|
+
|
|
3509
|
+
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="href")
|
|
3510
|
+
|
|
3511
|
+
def test_accepts_common_email_href_with_python_regex_semantics(self) -> None:
|
|
3512
|
+
issues = self.validate(
|
|
3513
|
+
f"""
|
|
3514
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3515
|
+
<data>
|
|
3516
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3517
|
+
<content><p><a href="mailto:user@example.com">Email</a></p></content>
|
|
3518
|
+
</shape>
|
|
3519
|
+
</data>
|
|
3520
|
+
</slide>
|
|
3521
|
+
"""
|
|
3522
|
+
)
|
|
3523
|
+
|
|
3524
|
+
self.assertEqual(issues, [])
|
|
3525
|
+
|
|
3526
|
+
def test_accepts_common_gradient_with_python_regex_semantics(self) -> None:
|
|
3527
|
+
issues = self.validate(
|
|
3528
|
+
f"""
|
|
3529
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3530
|
+
<style>
|
|
3531
|
+
<fill>
|
|
3532
|
+
<fillColor color="linear-gradient(90deg, rgb(255, 0, 0) 0%, rgb(0, 0, 255) 100%)"/>
|
|
3533
|
+
</fill>
|
|
3534
|
+
</style>
|
|
3535
|
+
<data>
|
|
3536
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3537
|
+
<content><p>Gradient</p></content>
|
|
3538
|
+
</shape>
|
|
3539
|
+
</data>
|
|
3540
|
+
</slide>
|
|
3541
|
+
"""
|
|
3542
|
+
)
|
|
3543
|
+
|
|
3544
|
+
self.assertEqual(issues, [])
|
|
3545
|
+
|
|
3546
|
+
def test_rejects_non_xsd_whitespace_in_color_pattern(self) -> None:
|
|
3547
|
+
issues = self.validate(
|
|
3548
|
+
f"""
|
|
3549
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3550
|
+
<style><fill><fillColor color="rgb(1,\u00a02,3)"/></fill></style>
|
|
3551
|
+
<data/>
|
|
3552
|
+
</slide>
|
|
3553
|
+
"""
|
|
3554
|
+
)
|
|
3555
|
+
|
|
3556
|
+
self.assert_issue(issues, "sxsd_pattern_mismatch", attr="color")
|
|
3557
|
+
|
|
3558
|
+
class SxsdSyntaxStructureTest(SxsdSyntaxTestCase):
|
|
3559
|
+
def test_accepts_nested_content_in_referenced_rich_text_shadow(self) -> None:
|
|
3560
|
+
issues = self.validate(
|
|
3561
|
+
f"""
|
|
3562
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3563
|
+
<data>
|
|
3564
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3565
|
+
<content>
|
|
3566
|
+
<p><span><shadow color="rgba(0, 0, 0, 1)"><strong>Text</strong></shadow></span></p>
|
|
3567
|
+
</content>
|
|
3568
|
+
</shape>
|
|
3569
|
+
</data>
|
|
3570
|
+
</slide>
|
|
3571
|
+
"""
|
|
3572
|
+
)
|
|
3573
|
+
|
|
3574
|
+
self.assertEqual(issues, [])
|
|
3575
|
+
|
|
3576
|
+
def test_keeps_shape_effect_shadow_as_childless_local_type(self) -> None:
|
|
3577
|
+
issues = self.validate(
|
|
3578
|
+
f"""
|
|
3579
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3580
|
+
<data>
|
|
3581
|
+
<shape type="rect" topLeftX="10" topLeftY="20" width="300" height="80">
|
|
3582
|
+
<shadow><strong>Not rich text</strong></shadow>
|
|
3583
|
+
</shape>
|
|
3584
|
+
</data>
|
|
3585
|
+
</slide>
|
|
3586
|
+
"""
|
|
3587
|
+
)
|
|
3588
|
+
|
|
3589
|
+
self.assert_issue(
|
|
3590
|
+
issues,
|
|
3591
|
+
"sxsd_unexpected_child",
|
|
3592
|
+
path="slide/data/shape/shadow/strong",
|
|
3593
|
+
)
|
|
3594
|
+
|
|
3595
|
+
def test_accepts_standalone_slide_fragment_without_namespace(self) -> None:
|
|
3596
|
+
issues = self.validate(
|
|
3597
|
+
'<slide><data><shape type="text" topLeftX="10" topLeftY="20" width="300" height="80">'
|
|
3598
|
+
'<content><p>Text</p></content></shape></data></slide>'
|
|
3599
|
+
)
|
|
3600
|
+
|
|
3601
|
+
self.assertEqual(issues, [])
|
|
3602
|
+
|
|
3603
|
+
def test_rejects_presentation_without_namespace(self) -> None:
|
|
3604
|
+
issues = self.validate(
|
|
3605
|
+
'<presentation width="960" height="540"><slide/></presentation>'
|
|
3606
|
+
)
|
|
3607
|
+
|
|
3608
|
+
self.assert_issue(issues, "sxsd_invalid_namespace", path="presentation")
|
|
3609
|
+
|
|
3610
|
+
def test_rejects_wrong_namespace_that_violates_xsd(self) -> None:
|
|
3611
|
+
issues = self.validate('<slide xmlns="https://example.com/not-sml"><data/></slide>')
|
|
3612
|
+
|
|
3613
|
+
self.assert_issue(issues, "sxsd_invalid_namespace", path="slide")
|
|
3614
|
+
|
|
3615
|
+
def test_rejects_descendant_outside_document_namespace(self) -> None:
|
|
3616
|
+
issues = self.validate(
|
|
3617
|
+
f"""
|
|
3618
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3619
|
+
<data xmlns="">
|
|
3620
|
+
<shape xmlns="{SML_NAMESPACE}" type="rect" topLeftX="10" topLeftY="20" width="300" height="80"/>
|
|
3621
|
+
</data>
|
|
3622
|
+
</slide>
|
|
3623
|
+
"""
|
|
3624
|
+
)
|
|
3625
|
+
|
|
3626
|
+
self.assert_issue(issues, "sxsd_invalid_namespace", path="slide/data")
|
|
3627
|
+
|
|
3628
|
+
def test_rejects_unexpected_child_that_violates_xsd(self) -> None:
|
|
3629
|
+
issues = self.validate(
|
|
3630
|
+
f"""
|
|
3631
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3632
|
+
<shape type="text" topLeftX="10" topLeftY="20" width="300" height="80"/>
|
|
3633
|
+
</slide>
|
|
3634
|
+
"""
|
|
3635
|
+
)
|
|
3636
|
+
|
|
3637
|
+
self.assert_issue(issues, "sxsd_unexpected_child", path="slide/shape")
|
|
3638
|
+
|
|
3639
|
+
def test_rejects_child_order_that_violates_xsd(self) -> None:
|
|
3640
|
+
issues = self.validate(
|
|
3641
|
+
f"""
|
|
3642
|
+
<presentation xmlns="{SML_NAMESPACE}" width="1920" height="1080">
|
|
3643
|
+
<slide/>
|
|
3644
|
+
<title>Late title</title>
|
|
3645
|
+
</presentation>
|
|
3646
|
+
"""
|
|
3647
|
+
)
|
|
3648
|
+
|
|
3649
|
+
self.assert_issue(issues, "sxsd_invalid_child_order", path="presentation/title")
|
|
3650
|
+
|
|
3651
|
+
def test_enforces_presentation_slide_minimum_from_xsd(self) -> None:
|
|
3652
|
+
issues = self.validate(
|
|
3653
|
+
f'<presentation xmlns="{SML_NAMESPACE}" width="1920" height="1080"/>'
|
|
3654
|
+
)
|
|
3655
|
+
|
|
3656
|
+
self.assert_issue(issues, "sxsd_missing_required_child", path="presentation")
|
|
3657
|
+
|
|
3658
|
+
def test_enforces_presentation_slide_maximum_from_xsd(self) -> None:
|
|
3659
|
+
slides = "".join("<slide/>" for _ in range(101))
|
|
3660
|
+
issues = self.validate(
|
|
3661
|
+
f'<presentation xmlns="{SML_NAMESPACE}" width="1920" height="1080">{slides}</presentation>'
|
|
3662
|
+
)
|
|
3663
|
+
|
|
3664
|
+
self.assert_issue(issues, "sxsd_too_many_children", path="presentation/slide")
|
|
3665
|
+
|
|
3666
|
+
def test_rejects_multiple_choice_children_that_violate_xsd(self) -> None:
|
|
3667
|
+
issues = self.validate(
|
|
3668
|
+
f"""
|
|
3669
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3670
|
+
<style>
|
|
3671
|
+
<fill><fillColor/><fillImg src="token"/></fill>
|
|
3672
|
+
</style>
|
|
3673
|
+
</slide>
|
|
3674
|
+
"""
|
|
3675
|
+
)
|
|
3676
|
+
|
|
3677
|
+
self.assert_issue(issues, "sxsd_too_many_children", path="slide/style/fill")
|
|
3678
|
+
|
|
3679
|
+
def test_rejects_line_without_required_border_from_xsd(self) -> None:
|
|
3680
|
+
issues = self.validate(
|
|
3681
|
+
f"""
|
|
3682
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3683
|
+
<data><line startX="0" startY="0" endX="100" endY="100"/></data>
|
|
3684
|
+
</slide>
|
|
3685
|
+
"""
|
|
3686
|
+
)
|
|
3687
|
+
|
|
3688
|
+
self.assert_issue(issues, "sxsd_missing_required_child", path="slide/data/line")
|
|
3689
|
+
|
|
3690
|
+
def test_reports_missing_required_chart_structure(self) -> None:
|
|
3691
|
+
issues = self.validate(
|
|
3692
|
+
f"""
|
|
3693
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3694
|
+
<data><chart topLeftX="0" topLeftY="0" width="300" height="200"/></data>
|
|
3695
|
+
</slide>
|
|
3696
|
+
"""
|
|
3697
|
+
)
|
|
3698
|
+
|
|
3699
|
+
self.assert_issue(issues, "sxsd_missing_required_child", path="slide/data/chart")
|
|
3700
|
+
|
|
3701
|
+
def test_reports_missing_required_nested_sequence_child(self) -> None:
|
|
3702
|
+
issues = self.validate(
|
|
3703
|
+
f"""
|
|
3704
|
+
<slide xmlns="{SML_NAMESPACE}">
|
|
3705
|
+
<data><table topLeftX="0" topLeftY="0"><tr/></table></data>
|
|
3706
|
+
</slide>
|
|
3707
|
+
"""
|
|
3708
|
+
)
|
|
3709
|
+
|
|
3710
|
+
issue = self.assert_issue(issues, "sxsd_missing_required_child", path="slide/data/table/tr")
|
|
3711
|
+
self.assertEqual(issue["expected"], "td (at least 1)")
|
|
3712
|
+
|
|
3713
|
+
|
|
3714
|
+
class SxsdSchemaModelTest(unittest.TestCase):
|
|
3715
|
+
def test_reports_unsupported_xsd_pattern_without_crashing(self) -> None:
|
|
3716
|
+
schema = rf"""
|
|
3717
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
3718
|
+
xmlns:sml="{SML_NAMESPACE}"
|
|
3719
|
+
targetNamespace="{SML_NAMESPACE}"
|
|
3720
|
+
elementFormDefault="qualified">
|
|
3721
|
+
<xs:simpleType name="UnsupportedPatternType">
|
|
3722
|
+
<xs:union>
|
|
3723
|
+
<xs:simpleType>
|
|
3724
|
+
<xs:restriction base="xs:string"><xs:pattern value="[\S]"/></xs:restriction>
|
|
3725
|
+
</xs:simpleType>
|
|
3726
|
+
<xs:simpleType>
|
|
3727
|
+
<xs:restriction base="xs:string"><xs:pattern value="z+"/></xs:restriction>
|
|
3728
|
+
</xs:simpleType>
|
|
3729
|
+
</xs:union>
|
|
3730
|
+
</xs:simpleType>
|
|
3731
|
+
<xs:complexType name="SlideType">
|
|
3732
|
+
<xs:attribute name="value" type="sml:UnsupportedPatternType"/>
|
|
3733
|
+
</xs:complexType>
|
|
3734
|
+
</xs:schema>
|
|
3735
|
+
"""
|
|
3736
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
3737
|
+
schema_path = Path(temp_dir) / "schema.xsd"
|
|
3738
|
+
schema_path.write_text(schema, encoding="utf-8")
|
|
3739
|
+
try:
|
|
3740
|
+
issues = sxsd_validator.validate_sxsd(
|
|
3741
|
+
ET.fromstring(f'<slide xmlns="{SML_NAMESPACE}" value="A"/>'),
|
|
3742
|
+
schema_path,
|
|
3743
|
+
)
|
|
3744
|
+
except (ValueError, sxsd_validator.re.error) as error:
|
|
3745
|
+
self.fail(f"SXSD pattern capability errors must be reported, not raised: {error}")
|
|
3746
|
+
|
|
3747
|
+
self.assertEqual([issue["code"] for issue in issues], ["sxsd_unsupported_pattern"])
|
|
3748
|
+
self.assertEqual(issues[0]["attr"], "value")
|
|
3749
|
+
self.assertIn("pattern interpreter", str(issues[0]["hint"]).lower())
|
|
3750
|
+
|
|
3751
|
+
def test_standalone_slide_uses_slide_type_without_global_element(self) -> None:
|
|
3752
|
+
schema = f"""
|
|
3753
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
3754
|
+
xmlns:sml="{SML_NAMESPACE}"
|
|
3755
|
+
targetNamespace="{SML_NAMESPACE}"
|
|
3756
|
+
elementFormDefault="qualified">
|
|
3757
|
+
<xs:complexType name="SlideType"><xs:sequence/></xs:complexType>
|
|
3758
|
+
<xs:complexType name="PresentationType">
|
|
3759
|
+
<xs:sequence><xs:element name="slide" type="sml:SlideType"/></xs:sequence>
|
|
3760
|
+
</xs:complexType>
|
|
3761
|
+
<xs:element name="presentation" type="sml:PresentationType"/>
|
|
3762
|
+
</xs:schema>
|
|
3763
|
+
"""
|
|
3764
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
3765
|
+
schema_path = Path(temp_dir) / "schema.xsd"
|
|
3766
|
+
schema_path.write_text(schema, encoding="utf-8")
|
|
3767
|
+
issues = sxsd_validator.validate_sxsd(
|
|
3768
|
+
ET.fromstring(f'<slide xmlns="{SML_NAMESPACE}"/>'),
|
|
3769
|
+
schema_path,
|
|
3770
|
+
)
|
|
3771
|
+
|
|
3772
|
+
self.assertEqual(issues, [])
|
|
3773
|
+
|
|
3774
|
+
def test_standalone_slide_requires_slide_type_in_xsd(self) -> None:
|
|
3775
|
+
schema = f"""
|
|
3776
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
3777
|
+
xmlns:sml="{SML_NAMESPACE}"
|
|
3778
|
+
targetNamespace="{SML_NAMESPACE}"
|
|
3779
|
+
elementFormDefault="qualified">
|
|
3780
|
+
<xs:complexType name="PresentationType"><xs:sequence/></xs:complexType>
|
|
3781
|
+
<xs:element name="presentation" type="sml:PresentationType"/>
|
|
3782
|
+
</xs:schema>
|
|
3783
|
+
"""
|
|
3784
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
3785
|
+
schema_path = Path(temp_dir) / "schema.xsd"
|
|
3786
|
+
schema_path.write_text(schema, encoding="utf-8")
|
|
3787
|
+
issues = sxsd_validator.validate_sxsd(
|
|
3788
|
+
ET.fromstring(f'<slide xmlns="{SML_NAMESPACE}"/>'),
|
|
3789
|
+
schema_path,
|
|
3790
|
+
)
|
|
3791
|
+
|
|
3792
|
+
self.assertEqual([issue["code"] for issue in issues], ["sxsd_unexpected_root"])
|
|
1167
3793
|
|
|
1168
3794
|
|
|
1169
3795
|
if __name__ == "__main__":
|