mkdocs2confluence 0.10.1__tar.gz → 0.10.2__tar.gz

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 (87) hide show
  1. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/PKG-INFO +1 -1
  2. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/pyproject.toml +1 -1
  3. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs2confluence.egg-info/PKG-INFO +1 -1
  4. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/emitter/xhtml.py +13 -9
  5. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/parser/markdown.py +62 -8
  6. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_emitter.py +26 -0
  7. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_parser.py +43 -0
  8. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/LICENSE +0 -0
  9. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/README.md +0 -0
  10. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/setup.cfg +0 -0
  11. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs2confluence.egg-info/SOURCES.txt +0 -0
  12. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs2confluence.egg-info/dependency_links.txt +0 -0
  13. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs2confluence.egg-info/entry_points.txt +0 -0
  14. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs2confluence.egg-info/requires.txt +0 -0
  15. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs2confluence.egg-info/top_level.txt +0 -0
  16. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/__init__.py +0 -0
  17. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/cli.py +0 -0
  18. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/emitter/__init__.py +0 -0
  19. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/ir/__init__.py +0 -0
  20. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/ir/document.py +0 -0
  21. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/ir/nodes.py +0 -0
  22. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/ir/treeutil.py +0 -0
  23. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/loader/__init__.py +0 -0
  24. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/loader/config.py +0 -0
  25. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/loader/extra_css.py +0 -0
  26. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/loader/nav.py +0 -0
  27. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/loader/page.py +0 -0
  28. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/parser/__init__.py +0 -0
  29. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/pdf/__init__.py +0 -0
  30. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/pdf/generator.py +0 -0
  31. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/pdf/render.py +0 -0
  32. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preprocess/__init__.py +0 -0
  33. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preprocess/abbrevs.py +0 -0
  34. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preprocess/fence.py +0 -0
  35. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preprocess/frontmatter.py +0 -0
  36. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preprocess/icons.py +0 -0
  37. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preprocess/includes.py +0 -0
  38. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preprocess/linkdefs.py +0 -0
  39. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preview/__init__.py +0 -0
  40. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preview/render.py +0 -0
  41. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/preview/server.py +0 -0
  42. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/publisher/__init__.py +0 -0
  43. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/publisher/client.py +0 -0
  44. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/publisher/pipeline.py +0 -0
  45. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/sync/__init__.py +0 -0
  46. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/sync/anchoring.py +0 -0
  47. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/sync/command.py +0 -0
  48. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/sync/comments.py +0 -0
  49. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/sync/github.py +0 -0
  50. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/sync/platform.py +0 -0
  51. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/sync/state.py +0 -0
  52. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/transforms/__init__.py +0 -0
  53. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/transforms/abbrevs.py +0 -0
  54. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/transforms/assets.py +0 -0
  55. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/transforms/editlink.py +0 -0
  56. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/transforms/footer.py +0 -0
  57. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/transforms/images.py +0 -0
  58. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/transforms/internallinks.py +0 -0
  59. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/src/mkdocs_to_confluence/transforms/mermaid.py +0 -0
  60. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_abbrevs.py +0 -0
  61. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_children_macro.py +0 -0
  62. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_cli.py +0 -0
  63. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_editlink.py +0 -0
  64. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_extra_css.py +0 -0
  65. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_footer.py +0 -0
  66. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_frontmatter.py +0 -0
  67. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_icons.py +0 -0
  68. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_images.py +0 -0
  69. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_internallinks.py +0 -0
  70. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_ir.py +0 -0
  71. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_linkdefs.py +0 -0
  72. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_loader.py +0 -0
  73. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_mermaid.py +0 -0
  74. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_page_loader.py +0 -0
  75. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_pdf.py +0 -0
  76. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_preprocess.py +0 -0
  77. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_preview.py +0 -0
  78. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_publish_client.py +0 -0
  79. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_publish_config.py +0 -0
  80. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_publish_pipeline.py +0 -0
  81. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_server.py +0 -0
  82. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_sync_anchoring.py +0 -0
  83. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_sync_command.py +0 -0
  84. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_sync_comments.py +0 -0
  85. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_sync_github.py +0 -0
  86. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_sync_state.py +0 -0
  87. {mkdocs2confluence-0.10.1 → mkdocs2confluence-0.10.2}/tests/test_treeutil.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.10.1
3
+ Version: 0.10.2
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mkdocs2confluence"
3
- version = "0.10.1"
3
+ version = "0.10.2"
4
4
  description = "Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more"
5
5
  readme = "README.md"
6
6
  license = { text = "GPL-3.0-or-later" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mkdocs2confluence
3
- Version: 0.10.1
3
+ Version: 0.10.2
4
4
  Summary: Publish MkDocs Material pages to Confluence Cloud — admonitions, Mermaid diagrams, tabs, page properties and more
5
5
  Author: Anders Hybertz
6
6
  License: GPL-3.0-or-later
@@ -422,15 +422,19 @@ def _emit_task_item(item: ListItem) -> str:
422
422
 
423
423
 
424
424
  def _emit_list_item(item: ListItem) -> str:
425
- if any(isinstance(c, _LIST_BLOCK_TYPES) for c in item.children):
426
- # Loose list item: children are block nodes (e.g. nested list or paragraph).
427
- inner = emit(item.children)
428
- return f" <li>{inner.strip()}</li>\n"
429
- # Tight list item: children are inline nodes. Confluence requires a <p>
430
- # wrapper inside <li> for structured inline macros (e.g. <ac:link>) to
431
- # render — without it they are silently stripped by the storage parser.
432
- inner = _emit_inlines(item.children)
433
- return f" <li><p>{inner}</p></li>\n"
425
+ first_block = next(
426
+ (i for i, c in enumerate(item.children) if isinstance(c, _LIST_BLOCK_TYPES)), None
427
+ )
428
+ if first_block is None:
429
+ # All inline standard tight rendering.
430
+ inner = _emit_inlines(item.children)
431
+ return f" <li><p>{inner}</p></li>\n"
432
+ # Mixed: inline prefix (if any) wrapped in <p>, followed by block children.
433
+ parts: list[str] = []
434
+ if first_block > 0:
435
+ parts.append(f"<p>{_emit_inlines(item.children[:first_block])}</p>\n")
436
+ parts.append(emit(item.children[first_block:]))
437
+ return f" <li>{''.join(parts).strip()}</li>\n"
434
438
 
435
439
 
436
440
  def _emit_table(node: Table) -> str:
@@ -158,6 +158,7 @@ class _BlockQuoteToken:
158
158
  class _ListItemData:
159
159
  text: str
160
160
  task: bool | None = None # None=regular, True=checked, False=unchecked
161
+ sub_tokens: list[_Token] = field(default_factory=list)
161
162
 
162
163
 
163
164
  @dataclass
@@ -501,7 +502,8 @@ def _tokenize(text: str) -> list[_Token]:
501
502
  task = task_m.group("state").lower() == "x"
502
503
  item_text = task_m.group("rest")
503
504
  i += 1
504
- # Collect continuation lines (non-blank, non-list) into this item.
505
+ # Collect continuation and indented sub-list lines.
506
+ sub_lines: list[str] = []
505
507
  while i < len(lines) and lines[i].strip():
506
508
  cont = lines[i]
507
509
  bullet_m = _BULLET_RE.match(cont)
@@ -510,9 +512,18 @@ def _tokenize(text: str) -> list[_Token]:
510
512
  ordered_m2 and not ordered_m2.group("indent")
511
513
  ):
512
514
  break
513
- item_text = item_text.rstrip() + " " + cont.strip()
515
+ if (bullet_m and bullet_m.group("indent")) or (
516
+ ordered_m2 and ordered_m2.group("indent")
517
+ ) or sub_lines:
518
+ sub_lines.append(cont)
519
+ else:
520
+ item_text = item_text.rstrip() + " " + cont.strip()
514
521
  i += 1
515
- list_items.append(_ListItemData(text=item_text, task=task))
522
+ _sub: list[_Token] = []
523
+ if sub_lines:
524
+ _ind = min(len(ln) - len(ln.lstrip()) for ln in sub_lines if ln.strip())
525
+ _sub = _tokenize("\n".join(ln[_ind:] for ln in sub_lines))
526
+ list_items.append(_ListItemData(text=item_text, task=task, sub_tokens=_sub))
516
527
  tokens.append(_BulletListToken(items=list_items))
517
528
  continue
518
529
 
@@ -537,7 +548,8 @@ def _tokenize(text: str) -> list[_Token]:
537
548
  break
538
549
  item_text = om.group("text")
539
550
  i += 1
540
- # Collect continuation lines (non-blank, non-list) into this item.
551
+ # Collect continuation and indented sub-list lines.
552
+ sub_lines_ord: list[str] = []
541
553
  while i < len(lines) and lines[i].strip():
542
554
  cont = lines[i]
543
555
  ordered_m3 = _ORDERED_RE.match(cont)
@@ -546,9 +558,18 @@ def _tokenize(text: str) -> list[_Token]:
546
558
  bullet_m2 and not bullet_m2.group("indent")
547
559
  ):
548
560
  break
549
- item_text = item_text.rstrip() + " " + cont.strip()
561
+ if (ordered_m3 and ordered_m3.group("indent")) or (
562
+ bullet_m2 and bullet_m2.group("indent")
563
+ ) or sub_lines_ord:
564
+ sub_lines_ord.append(cont)
565
+ else:
566
+ item_text = item_text.rstrip() + " " + cont.strip()
550
567
  i += 1
551
- ord_items.append(_ListItemData(text=item_text))
568
+ _sub_ord: list[_Token] = []
569
+ if sub_lines_ord:
570
+ _ind_ord = min(len(ln) - len(ln.lstrip()) for ln in sub_lines_ord if ln.strip())
571
+ _sub_ord = _tokenize("\n".join(ln[_ind_ord:] for ln in sub_lines_ord))
572
+ ord_items.append(_ListItemData(text=item_text, sub_tokens=_sub_ord))
552
573
  tokens.append(_OrderedListToken(start=start, items=ord_items))
553
574
  continue
554
575
 
@@ -1030,6 +1051,32 @@ class _OpenSection:
1030
1051
  children: list[IRNode] = field(default_factory=list)
1031
1052
 
1032
1053
 
1054
+ def _build_sub_list_nodes(tokens: list[_Token], fn_map: dict[str, int] | None) -> tuple[IRNode, ...]:
1055
+ """Recursively convert sub-list tokens (from indented continuation) to IR nodes."""
1056
+ nodes: list[IRNode] = []
1057
+ for token in tokens:
1058
+ if isinstance(token, _BulletListToken):
1059
+ items = tuple(
1060
+ ListItem(
1061
+ children=_parse_inline(item.text, fn_map=fn_map)
1062
+ + _build_sub_list_nodes(item.sub_tokens, fn_map),
1063
+ task=item.task,
1064
+ )
1065
+ for item in token.items
1066
+ )
1067
+ nodes.append(BulletList(items=items))
1068
+ elif isinstance(token, _OrderedListToken):
1069
+ items = tuple(
1070
+ ListItem(
1071
+ children=_parse_inline(item.text, fn_map=fn_map)
1072
+ + _build_sub_list_nodes(item.sub_tokens, fn_map),
1073
+ )
1074
+ for item in token.items
1075
+ )
1076
+ nodes.append(OrderedList(items=items, start=token.start))
1077
+ return tuple(nodes)
1078
+
1079
+
1033
1080
  def _build_tree(
1034
1081
  tokens: list[_Token],
1035
1082
  fn_map: dict[str, int] | None = None,
@@ -1101,14 +1148,21 @@ def _build_tree(
1101
1148
 
1102
1149
  elif isinstance(token, _BulletListToken):
1103
1150
  items = tuple(
1104
- ListItem(children=_parse_inline(item.text, fn_map=_fn), task=item.task)
1151
+ ListItem(
1152
+ children=_parse_inline(item.text, fn_map=_fn)
1153
+ + _build_sub_list_nodes(item.sub_tokens, _fn),
1154
+ task=item.task,
1155
+ )
1105
1156
  for item in token.items
1106
1157
  )
1107
1158
  _append_content(BulletList(items=items), stack, root)
1108
1159
 
1109
1160
  elif isinstance(token, _OrderedListToken):
1110
1161
  items = tuple(
1111
- ListItem(children=_parse_inline(item.text, fn_map=_fn))
1162
+ ListItem(
1163
+ children=_parse_inline(item.text, fn_map=_fn)
1164
+ + _build_sub_list_nodes(item.sub_tokens, _fn),
1165
+ )
1112
1166
  for item in token.items
1113
1167
  )
1114
1168
  _append_content(OrderedList(items=items, start=token.start), stack, root)
@@ -235,6 +235,32 @@ class TestListEmitters:
235
235
  assert "<ul>" in out
236
236
  assert "<ac:task-list>" not in out
237
237
 
238
+ def test_nested_bullet_list(self) -> None:
239
+ nested = BulletList(items=(ListItem((TextNode("child"),)),))
240
+ items = (ListItem((TextNode("parent"), nested)),)
241
+ out = emit((BulletList(items=items),))
242
+ assert out.count("<ul>") == 2
243
+ assert out.count("</ul>") == 2
244
+ assert "<p>parent</p>" in out
245
+ assert "<p>child</p>" in out
246
+
247
+ def test_nested_ordered_list(self) -> None:
248
+ nested = OrderedList(items=(ListItem((TextNode("sub"),)),))
249
+ items = (ListItem((TextNode("top"), nested)),)
250
+ out = emit((OrderedList(items=items),))
251
+ assert "<ol>" in out
252
+ assert "<ul>" not in out
253
+ assert "<p>top</p>" in out
254
+ assert "<p>sub</p>" in out
255
+
256
+ def test_nested_list_no_inline_prefix(self) -> None:
257
+ """A list item with only a nested sub-list (no inline text) renders cleanly."""
258
+ nested = BulletList(items=(ListItem((TextNode("only child"),)),))
259
+ items = (ListItem((nested,)),)
260
+ out = emit((BulletList(items=items),))
261
+ assert out.count("<ul>") == 2
262
+ assert "<p>only child</p>" in out
263
+
238
264
 
239
265
  class TestHorizontalRule:
240
266
  def test_hr(self) -> None:
@@ -1021,6 +1021,49 @@ class TestListParsing:
1021
1021
  )
1022
1022
  assert "Continuation" in item_text or "First point" in item_text
1023
1023
 
1024
+ def test_nested_bullet_list(self) -> None:
1025
+ md = "- item 1\n - nested 1\n - nested 2\n- item 2\n"
1026
+ nodes = parse(md)
1027
+ bl = first(nodes, BulletList)
1028
+ assert isinstance(bl, BulletList)
1029
+ assert len(bl.items) == 2
1030
+ # First item must have a nested BulletList as a child.
1031
+ nested = next((c for c in bl.items[0].children if isinstance(c, BulletList)), None)
1032
+ assert nested is not None, "expected nested BulletList in first item"
1033
+ assert len(nested.items) == 2
1034
+ assert nested.items[0].children[0].text == "nested 1" # type: ignore[union-attr]
1035
+ assert nested.items[1].children[0].text == "nested 2" # type: ignore[union-attr]
1036
+
1037
+ def test_nested_ordered_list(self) -> None:
1038
+ md = "1. first\n 1. sub one\n 2. sub two\n2. second\n"
1039
+ nodes = parse(md)
1040
+ ol = first(nodes, OrderedList)
1041
+ assert isinstance(ol, OrderedList)
1042
+ nested = next((c for c in ol.items[0].children if isinstance(c, OrderedList)), None)
1043
+ assert nested is not None, "expected nested OrderedList in first item"
1044
+ assert len(nested.items) == 2
1045
+
1046
+ def test_deeply_nested_bullet_list(self) -> None:
1047
+ md = "- a\n - b\n - c\n"
1048
+ nodes = parse(md)
1049
+ bl = first(nodes, BulletList)
1050
+ assert isinstance(bl, BulletList)
1051
+ level2 = next((c for c in bl.items[0].children if isinstance(c, BulletList)), None)
1052
+ assert level2 is not None
1053
+ level3 = next((c for c in level2.items[0].children if isinstance(c, BulletList)), None)
1054
+ assert level3 is not None
1055
+ assert level3.items[0].children[0].text == "c" # type: ignore[union-attr]
1056
+
1057
+ def test_mixed_nested_list(self) -> None:
1058
+ """Bullet list item containing an ordered sub-list."""
1059
+ md = "- item\n 1. sub one\n 2. sub two\n"
1060
+ nodes = parse(md)
1061
+ bl = first(nodes, BulletList)
1062
+ assert isinstance(bl, BulletList)
1063
+ nested_ol = next((c for c in bl.items[0].children if isinstance(c, OrderedList)), None)
1064
+ assert nested_ol is not None
1065
+ assert len(nested_ol.items) == 2
1066
+
1024
1067
 
1025
1068
  # ── Definition list parsing ───────────────────────────────────────────────────
1026
1069