@amaster.ai/pi-lark 0.1.2-beta.48 → 0.1.2-beta.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +2 -2
  2. package/skills/lark-apps/SKILL.md +1 -0
  3. package/skills/lark-apps/references/lark-apps-cache.md +61 -0
  4. package/skills/lark-base/SKILL.md +12 -5
  5. package/skills/lark-base/references/lark-base-data-query.md +11 -4
  6. package/skills/lark-base/references/lark-base-form-questions-create.md +6 -3
  7. package/skills/lark-base/references/lark-base-role-guide.md +11 -0
  8. package/skills/lark-base/references/role-config.md +31 -5
  9. package/skills/lark-calendar/SKILL.md +2 -2
  10. package/skills/lark-calendar/references/lark-calendar-recurring.md +1 -0
  11. package/skills/lark-contact/SKILL.md +1 -1
  12. package/skills/lark-drive/SKILL.md +16 -43
  13. package/skills/lark-drive/references/lark-drive-add-comment.md +2 -4
  14. package/skills/lark-drive/references/lark-drive-add-reply.md +47 -0
  15. package/skills/lark-drive/references/lark-drive-apply-permission.md +2 -2
  16. package/skills/lark-drive/references/lark-drive-batch-query-comments.md +46 -0
  17. package/skills/lark-drive/references/lark-drive-comment-content.md +50 -0
  18. package/skills/lark-drive/references/lark-drive-comment-location.md +7 -13
  19. package/skills/lark-drive/references/lark-drive-delete-reply.md +48 -0
  20. package/skills/lark-drive/references/lark-drive-download.md +5 -1
  21. package/skills/lark-drive/references/lark-drive-list-comments.md +25 -68
  22. package/skills/lark-drive/references/lark-drive-list-replies.md +54 -0
  23. package/skills/lark-drive/references/lark-drive-member-add.md +2 -2
  24. package/skills/lark-drive/references/lark-drive-member-list.md +2 -2
  25. package/skills/lark-drive/references/lark-drive-permission-get-setting.md +2 -2
  26. package/skills/lark-drive/references/lark-drive-preview.md +11 -1
  27. package/skills/lark-drive/references/lark-drive-react-reply.md +51 -0
  28. package/skills/lark-drive/references/lark-drive-reactions.md +27 -25
  29. package/skills/lark-drive/references/lark-drive-resolve-comment.md +45 -0
  30. package/skills/lark-drive/references/lark-drive-restore-comment.md +46 -0
  31. package/skills/lark-drive/references/lark-drive-update-reply.md +46 -0
  32. package/skills/lark-drive/references/lark-drive-workflow-permission-governance.md +1 -1
  33. package/skills/lark-slides/references/lark-slides-media-upload.md +1 -1
  34. package/skills/lark-slides/scripts/sxsd_validator.py +908 -0
  35. package/skills/lark-slides/scripts/xml_text_overlap_lint.py +209 -92
  36. package/skills/lark-slides/scripts/xml_text_overlap_lint_test.py +1079 -42
  37. package/skills/lark-drive/references/lark-drive-comments-guide.md +0 -80
@@ -2,13 +2,17 @@
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
 
@@ -47,6 +51,37 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
47
51
  f"xml-text-overlap-lint error: unexpected argument: {input_path}, need --input\n",
48
52
  )
49
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
+
50
85
  def test_xml_text_overlap_lint_accepts_inline_fixture_xml_samples(self) -> None:
51
86
  samples = {
52
87
  "image-led-cover": """
@@ -173,12 +208,12 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
173
208
  self.assertEqual(issues[0]["tag"], "ns0:slide")
174
209
  self.assertIn("default namespace", issues[0]["hint"])
175
210
 
176
- def test_lint_xml_allows_unprefixed_tags_without_namespace(self) -> None:
211
+ def test_lint_xml_accepts_server_readback_slide_without_namespace(self) -> None:
177
212
  result = xml_text_overlap_lint.lint_xml(
178
213
  """
179
- <slide>
214
+ <slide id="server-slide-id">
180
215
  <data>
181
- <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">
182
217
  <content textType="body"><p>Unprefixed SML</p></content>
183
218
  </shape>
184
219
  </data>
@@ -187,6 +222,90 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
187
222
  )
188
223
  self.assertEqual(result["summary"]["error_count"], 0)
189
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
+
190
309
  def test_lint_xml_accepts_escaped_entities_without_suspicious_entity_warning(self) -> None:
191
310
  result = xml_text_overlap_lint.lint_xml(
192
311
  """
@@ -250,8 +369,8 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
250
369
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
251
370
  <data>
252
371
  <img id="second" src="tok" topLeftX="100" topLeftY="120" width="240" height="160"/>
253
- <shape id="skipped" type="text" topLeftX="80" topLeftY="80" width="300">
254
- <content textType="body"><p>Missing height</p></content>
372
+ <shape id="second-shape" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
373
+ <content textType="body"><p>Second slide</p></content>
255
374
  </shape>
256
375
  </data>
257
376
  </slide>
@@ -261,10 +380,151 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
261
380
  self.assertEqual(result["slide_size"], {"width": 1280, "height": 720})
262
381
  self.assertEqual(result["summary"]["slide_count"], 2)
263
382
  self.assertEqual([slide["slide_number"] for slide in result["slides"]], [1, 2])
264
- self.assertEqual([slide["element_count"] for slide in result["slides"]], [1, 1])
383
+ self.assertEqual([slide["element_count"] for slide in result["slides"]], [1, 2])
265
384
  self.assertEqual(result["summary"]["error_count"], 0)
266
385
  self.assertEqual(result["summary"]["warning_count"], 0)
267
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
+
268
528
  def test_lint_xml_reports_sxsd_unsupported_tag_with_alias_hint(self) -> None:
269
529
  cases = [
270
530
  ("textbox", '<textbox topLeftX="80" topLeftY="80" width="300" height="60">Text</textbox>', '<shape type="text">'),
@@ -279,8 +539,15 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
279
539
  </slide>
280
540
  """
281
541
  )
282
- issue = result["issues"][0]
542
+ slide_issues = result["slides"][0]["issues"]
543
+ issue = next(
544
+ issue for issue in slide_issues if issue["code"] == "sxsd_unsupported_tag"
545
+ )
283
546
  self.assertEqual(result["summary"]["error_count"], 1)
547
+ self.assertEqual(
548
+ [reported["code"] for reported in slide_issues],
549
+ ["sxsd_unsupported_tag"],
550
+ )
284
551
  self.assertEqual(issue["code"], "sxsd_unsupported_tag")
285
552
  self.assertEqual(issue["tag"], tag_name)
286
553
  self.assertIn(expected_hint, issue["hint"])
@@ -288,6 +555,7 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
288
555
  def test_lint_xml_reports_sxsd_unsupported_attr_with_alias_hint(self) -> None:
289
556
  cases = [
290
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>'),
291
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>'),
292
560
  ]
293
561
  for tag_name, attr_name, expected_attr, element_xml in cases:
@@ -299,13 +567,75 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
299
567
  </slide>
300
568
  """
301
569
  )
302
- issue = result["issues"][0]
570
+ slide_issues = result["slides"][0]["issues"]
571
+ issue = next(
572
+ issue for issue in slide_issues if issue["code"] == "sxsd_unsupported_attr"
573
+ )
303
574
  self.assertEqual(result["summary"]["error_count"], 1)
575
+ self.assertEqual(
576
+ [reported["code"] for reported in slide_issues],
577
+ ["sxsd_unsupported_attr"],
578
+ )
304
579
  self.assertEqual(issue["code"], "sxsd_unsupported_attr")
305
580
  self.assertEqual(issue["tag"], tag_name)
306
581
  self.assertEqual(issue["attr"], attr_name)
307
582
  self.assertIn(expected_attr, issue["hint"])
308
583
 
584
+ def test_lint_xml_keeps_unrelated_unsupported_and_missing_attrs(self) -> None:
585
+ result = xml_text_overlap_lint.lint_xml(
586
+ """
587
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
588
+ <data>
589
+ <shape type="text" bogus="x" topLeftX="80" topLeftY="80" width="300"/>
590
+ </data>
591
+ </slide>
592
+ """
593
+ )
594
+
595
+ self.assertEqual(
596
+ [issue["code"] for issue in result["slides"][0]["issues"]],
597
+ ["sxsd_unsupported_attr", "sxsd_missing_required_attr"],
598
+ )
599
+
600
+ def test_lint_xml_keeps_missing_attrs_when_suggestion_is_ambiguous(self) -> None:
601
+ result = xml_text_overlap_lint.lint_xml(
602
+ """
603
+ <slide xmlns="http://www.larkoffice.com/sml/2.0">
604
+ <data>
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
+
309
639
  def test_lint_xml_ignores_server_filled_id_attrs(self) -> None:
310
640
  result = xml_text_overlap_lint.lint_xml(
311
641
  """
@@ -322,10 +652,10 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
322
652
  )
323
653
 
324
654
  self.assertEqual(result["summary"]["error_count"], 1)
325
- issue = result["issues"][0]
326
- self.assertEqual(issue["code"], "sxsd_unsupported_attr")
327
- self.assertEqual(issue["tag"], "fill")
328
- self.assertEqual(issue["attr"], "unexpected")
655
+ self.assertEqual(
656
+ [(issue["tag"], issue["attr"]) for issue in result["slides"][0]["issues"]],
657
+ [("fill", "unexpected")],
658
+ )
329
659
 
330
660
  def test_lint_xml_ignores_chart_roundtrip_attrs(self) -> None:
331
661
  result = xml_text_overlap_lint.lint_xml(
@@ -333,7 +663,34 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
333
663
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
334
664
  <data>
335
665
  <chart updated="true" topLeftX="80" topLeftY="80" width="300" height="160">
336
- <chartData isStaticData="true"/>
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>
670
+ </chartData>
671
+ </chart>
672
+ </data>
673
+ </slide>
674
+ """
675
+ )
676
+
677
+ self.assertEqual(result["summary"]["error_count"], 0)
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>
337
694
  </chart>
338
695
  </data>
339
696
  </slide>
@@ -341,7 +698,6 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
341
698
  )
342
699
 
343
700
  self.assertEqual(result["summary"]["error_count"], 0)
344
- self.assertNotIn("issues", result)
345
701
 
346
702
  def test_lint_xml_ignores_chart_parsed_values_roundtrip_tag(self) -> None:
347
703
  result = xml_text_overlap_lint.lint_xml(
@@ -349,10 +705,14 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
349
705
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
350
706
  <data>
351
707
  <chart topLeftX="80" topLeftY="80" width="300" height="160">
708
+ <chartPlotArea><chartPlot type="line"/></chartPlotArea>
352
709
  <chartData>
353
- <chartField>
354
- <chartParsedValues>Africa</chartParsedValues>
355
- </chartField>
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>
356
716
  </chartData>
357
717
  </chart>
358
718
  </data>
@@ -363,13 +723,40 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
363
723
  self.assertEqual(result["summary"]["error_count"], 0)
364
724
  self.assertNotIn("issues", result)
365
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
+
366
749
  def test_lint_xml_limits_chart_roundtrip_attrs_to_matching_tags(self) -> None:
367
750
  result = xml_text_overlap_lint.lint_xml(
368
751
  """
369
752
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
370
753
  <data>
371
754
  <chart isStaticData="true" topLeftX="80" topLeftY="80" width="300" height="160">
372
- <chartData updated="true"/>
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>
373
760
  </chart>
374
761
  </data>
375
762
  </slide>
@@ -377,11 +764,12 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
377
764
  )
378
765
 
379
766
  self.assertEqual(result["summary"]["error_count"], 2)
767
+ slide_issues = result["slides"][0]["issues"]
380
768
  self.assertEqual(
381
- {(issue["tag"], issue["attr"]) for issue in result["issues"]},
769
+ {(issue["tag"], issue["attr"]) for issue in slide_issues},
382
770
  {("chart", "isStaticData"), ("chartData", "updated")},
383
771
  )
384
- self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in result["issues"]))
772
+ self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in slide_issues))
385
773
 
386
774
  def test_lint_xml_reports_gradient_shorthand_attrs_on_fill_color(self) -> None:
387
775
  result = xml_text_overlap_lint.lint_xml(
@@ -403,14 +791,15 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
403
791
  </slide>
404
792
  """
405
793
  )
406
- unsupported_attrs = {issue["attr"] for issue in result["issues"]}
794
+ slide_issues = result["slides"][0]["issues"]
795
+ unsupported_attrs = {issue["attr"] for issue in slide_issues}
407
796
  self.assertEqual(result["summary"]["error_count"], 6)
408
797
  self.assertEqual(
409
798
  unsupported_attrs,
410
799
  {"type", "color1", "color2", "angle", "stop1", "stop2"},
411
800
  )
412
- self.assertTrue(all(issue["code"] == "sxsd_unsupported_attr" for issue in result["issues"]))
413
- self.assertTrue(all(issue["tag"] == "fillColor" for issue in result["issues"]))
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))
414
803
 
415
804
  def test_lint_xml_accepts_chart_field_simple_content_attrs(self) -> None:
416
805
  result = xml_text_overlap_lint.lint_xml(
@@ -1424,7 +1813,13 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1424
1813
  <data>
1425
1814
  <shape id="outside-shape" type="text" topLeftX="-10" topLeftY="40" width="50" height="50"/>
1426
1815
  <img id="outside-img" src="token" topLeftX="120" topLeftY="-20" width="50" height="50"/>
1427
- <chart id="outside-chart" topLeftX="900" topLeftY="100" width="100" height="100"/>
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>
1428
1823
  </data>
1429
1824
  </slide>
1430
1825
  </presentation>
@@ -1448,7 +1843,7 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1448
1843
  <shape id="body" type="text" topLeftX="80" topLeftY="80" width="300" height="60">
1449
1844
  <content fontSize="18"><p>Visible content</p></content>
1450
1845
  </shape>
1451
- <line id="connector" startX="80" startY="120" endX="980" endY="120"/>
1846
+ <line id="connector" startX="80" startY="120" endX="980" endY="120"><border/></line>
1452
1847
  </data>
1453
1848
  </slide>
1454
1849
  """
@@ -1645,7 +2040,13 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1645
2040
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
1646
2041
  <data>
1647
2042
  <shape id="rotated-text" type="text" topLeftX="0" topLeftY="0" width="100" height="100" rotation="45"/>
1648
- <chart id="rotated-chart" topLeftX="860" topLeftY="200" 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>
1649
2050
  </data>
1650
2051
  </slide>
1651
2052
  </presentation>
@@ -1666,7 +2067,7 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1666
2067
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
1667
2068
  <data>
1668
2069
  <shape id="rotated-rect" type="rect" topLeftX="900" topLeftY="0" width="100" height="100" rotation="45"/>
1669
- <img id="rotated-image" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45"/>
2070
+ <img id="rotated-image" src="token" topLeftX="860" topLeftY="200" width="100" height="100" rotation="45"/>
1670
2071
  </data>
1671
2072
  </slide>
1672
2073
  </presentation>
@@ -1722,7 +2123,7 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1722
2123
  self.assertEqual([issue["elements"] for issue in issues], [["table"], ["chart"], ["text"], ["rect"]])
1723
2124
  self.assertEqual(issues[-1]["bbox"], {"x": 95, "y": 0, "width": 10, "height": 10})
1724
2125
 
1725
- def test_lint_xml_treats_non_finite_rotations_as_zero(self) -> None:
2126
+ def test_lint_xml_rejects_non_finite_rotation_values_from_xsd(self) -> None:
1726
2127
  result = xml_text_overlap_lint.lint_xml(
1727
2128
  """
1728
2129
  <presentation xmlns="http://www.larkoffice.com/sml/2.0" width="960" height="540">
@@ -1730,17 +2131,22 @@ class XmlTextOverlapLintGeometryTest(unittest.TestCase):
1730
2131
  <data>
1731
2132
  <shape id="infinite" type="text" topLeftX="-10" topLeftY="0" width="20" height="20" rotation="inf"/>
1732
2133
  <shape id="negative-infinite" type="text" topLeftX="0" topLeftY="-10" width="20" height="20" rotation="-inf"/>
1733
- <chart id="not-a-number" topLeftX="950" topLeftY="0" width="20" height="20" rotation="nan"/>
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>
1734
2141
  </data>
1735
2142
  </slide>
1736
2143
  </presentation>
1737
2144
  """
1738
2145
  )
1739
- issues_by_element = {issue["elements"][0]: issue for issue in result["slides"][0]["issues"]}
1740
2146
  self.assertEqual(result["summary"]["error_count"], 3)
1741
- self.assertEqual(issues_by_element["infinite"]["overflow"], {"left": 10, "top": 0, "right": 0, "bottom": 0})
1742
- self.assertEqual(issues_by_element["negative-infinite"]["overflow"], {"left": 0, "top": 10, "right": 0, "bottom": 0})
1743
- self.assertEqual(issues_by_element["not-a-number"]["overflow"], {"left": 0, "top": 0, "right": 10, "bottom": 0})
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"})
1744
2150
 
1745
2151
  def test_lint_xml_reports_table_bottom_overflow_from_declared_bounds(self) -> None:
1746
2152
  result = xml_text_overlap_lint.lint_xml(
@@ -2095,7 +2501,7 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
2095
2501
  """
2096
2502
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
2097
2503
  <data>
2098
- <img id="ghost" topLeftX="60" topLeftY="60" width="200" height="200" alpha="0"/>
2504
+ <img id="ghost" src="token" topLeftX="60" topLeftY="60" width="200" height="200" alpha="0"/>
2099
2505
  </data>
2100
2506
  </slide>
2101
2507
  """
@@ -2376,7 +2782,13 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
2376
2782
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
2377
2783
  <data>
2378
2784
  <shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
2379
- <chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220"/>
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>
2380
2792
  </data>
2381
2793
  </slide>
2382
2794
  """
@@ -2393,7 +2805,13 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
2393
2805
  <content fontSize="20"><p>Section title</p></content>
2394
2806
  </shape>
2395
2807
  <shape id="chart-card" type="rect" topLeftX="500" topLeftY="135" width="410" height="300"/>
2396
- <chart id="chart" topLeftX="525" topLeftY="170" width="350" height="220" alpha="0"/>
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>
2397
2815
  </data>
2398
2816
  </slide>
2399
2817
  """
@@ -2470,7 +2888,7 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
2470
2888
  <content fontSize="20"><p>Section title</p></content>
2471
2889
  </shape>
2472
2890
  <shape id="card" type="rect" topLeftX="330" topLeftY="120" width="300" height="300"/>
2473
- <img id="ghost-overlay" topLeftX="330" topLeftY="120" width="300" height="300" alpha="0"/>
2891
+ <img id="ghost-overlay" src="token" topLeftX="330" topLeftY="120" width="300" height="300" alpha="0"/>
2474
2892
  </data>
2475
2893
  </slide>
2476
2894
  """
@@ -2576,7 +2994,7 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
2576
2994
  <data>
2577
2995
  <shape id="metric-card" type="rect" topLeftX="80" topLeftY="140" width="360" height="300"/>
2578
2996
  <shape id="metric" type="text" topLeftX="104" topLeftY="190" width="340" height="90">
2579
- <content fontSize="12.4"><p><strong><span fontSize="62">400</span></strong>+ 项</p></content>
2997
+ <content fontSize="12"><p><strong><span fontSize="62">400</span></strong>+ 项</p></content>
2580
2998
  </shape>
2581
2999
  </data>
2582
3000
  </slide>
@@ -2590,10 +3008,10 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
2590
3008
  """
2591
3009
  <slide xmlns="http://www.larkoffice.com/sml/2.0">
2592
3010
  <data>
2593
- <line id="l1" startX="100" startY="100" endX="800" endY="100"/>
2594
- <line id="l2" startX="100" startY="200" endX="800" endY="200"/>
2595
- <line id="l3" startX="100" startY="300" endX="800" endY="300"/>
2596
- <line id="l4" startX="100" startY="400" endX="800" endY="400"/>
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>
2597
3015
  </data>
2598
3016
  </slide>
2599
3017
  """
@@ -2755,5 +3173,624 @@ class XmlTextOverlapLintDensityTest(unittest.TestCase):
2755
3173
  )
2756
3174
 
2757
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>
3439
+ </shape>
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(
3466
+ f"""
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>
3474
+ """
3475
+ )
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"])
3793
+
3794
+
2758
3795
  if __name__ == "__main__":
2759
3796
  unittest.main()