Skip to content

Commit de79335

Browse files
Merge pull request #180 from adamtheturtle/add-numbered-list-support
Add support for numbered lists
2 parents 7173d08 + 6a32613 commit de79335

File tree

3 files changed

+212
-0
lines changed

3 files changed

+212
-0
lines changed

sample/index.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,25 @@ Some key features:
158158
* Supports images with URLs
159159
* Supports videos with URLs and local files
160160

161+
Numbered Lists
162+
~~~~~~~~~~~~~~
163+
164+
The builder now supports numbered lists:
165+
166+
1. First numbered item
167+
2. Second numbered item with **bold text**
168+
3. Third numbered item with nested content
169+
170+
1. First nested numbered item
171+
2. Second nested numbered item
172+
173+
1. Deeply nested numbered item
174+
2. Another deeply nested item
175+
176+
3. Back to second level
177+
178+
4. Fourth top-level item
179+
161180
Heading 2 with *italic*
162181
-----------------------
163182

src/sphinx_notion/__init__.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
Heading3 as UnoHeading3,
3434
)
3535
from ultimate_notion.blocks import Image as UnoImage
36+
from ultimate_notion.blocks import NumberedItem as UnoNumberedItem
3637
from ultimate_notion.blocks import (
3738
Paragraph as UnoParagraph,
3839
)
@@ -343,6 +344,44 @@ def _process_list_item_recursively(
343344
parent_path=[*parent_path, (block, id(block))],
344345
)
345346

347+
@beartype
348+
def _process_numbered_list_item_recursively(
349+
self,
350+
*,
351+
node: nodes.list_item,
352+
parent_path: list[tuple[Block, int]],
353+
) -> None:
354+
"""
355+
Recursively process a numbered list item node and return a
356+
NumberedItem.
357+
"""
358+
paragraph = node.children[0]
359+
assert isinstance(paragraph, nodes.paragraph)
360+
rich_text = _create_rich_text_from_children(node=paragraph)
361+
block = UnoNumberedItem(text=rich_text)
362+
self._add_block_to_tree(
363+
block=block,
364+
parent_path=parent_path,
365+
)
366+
367+
numbered_only_msg = (
368+
"The only thing Notion supports within a numbered list is a "
369+
f"numbered list. Given {type(node).__name__} on line {node.line} "
370+
f"in {node.source}"
371+
)
372+
assert isinstance(node, nodes.list_item)
373+
374+
for child in node.children[1:]:
375+
assert isinstance(child, nodes.enumerated_list), numbered_only_msg
376+
for nested_list_item in child.children:
377+
assert isinstance(nested_list_item, nodes.list_item), (
378+
numbered_only_msg
379+
)
380+
self._process_numbered_list_item_recursively(
381+
node=nested_list_item,
382+
parent_path=[*parent_path, (block, id(block))],
383+
)
384+
346385
@singledispatchmethod
347386
@beartype
348387
def _process_node_to_blocks( # pylint: disable=no-self-use
@@ -476,6 +515,30 @@ def _(
476515
parent_path=parent_path,
477516
)
478517

518+
@_process_node_to_blocks.register
519+
def _(
520+
self,
521+
node: nodes.enumerated_list,
522+
*,
523+
section_level: int,
524+
parent_path: list[tuple[Block, int]],
525+
) -> None:
526+
"""
527+
Process enumerated list nodes by creating Notion NumberedItem blocks.
528+
"""
529+
del section_level
530+
numbered_only_msg = (
531+
"The only thing Notion supports within a numbered list is a "
532+
f"numbered list. Given {type(node).__name__} on line {node.line} "
533+
f"in {node.source}"
534+
)
535+
for list_item in node.children:
536+
assert isinstance(list_item, nodes.list_item), numbered_only_msg
537+
self._process_numbered_list_item_recursively(
538+
node=list_item,
539+
parent_path=parent_path,
540+
)
541+
479542
@_process_node_to_blocks.register
480543
def _(
481544
self,
@@ -836,6 +899,18 @@ def visit_bullet_list(self, node: nodes.Element) -> None:
836899

837900
raise nodes.SkipNode
838901

902+
def visit_enumerated_list(self, node: nodes.Element) -> None:
903+
"""
904+
Handle enumerated list nodes by processing each list item.
905+
"""
906+
self._process_node_to_blocks(
907+
node,
908+
section_level=self._section_level,
909+
parent_path=[],
910+
)
911+
912+
raise nodes.SkipNode
913+
839914
def visit_topic(self, node: nodes.Element) -> None:
840915
"""
841916
Handle topic nodes, specifically for table of contents.

tests/test_integration.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
Heading3 as UnoHeading3,
2828
)
2929
from ultimate_notion.blocks import Image as UnoImage
30+
from ultimate_notion.blocks import NumberedItem as UnoNumberedItem
3031
from ultimate_notion.blocks import (
3132
Paragraph as UnoParagraph,
3233
)
@@ -64,6 +65,7 @@ def _reconstruct_nested_structure(
6465
"paragraph",
6566
"callout",
6667
"bulleted_list_item",
68+
"numbered_list_item",
6769
"toggle",
6870
}:
6971
children = item.get("children", [])
@@ -1041,6 +1043,122 @@ def test_nested_bullet_list(
10411043
)
10421044

10431045

1046+
def test_flat_numbered_list(
1047+
*,
1048+
make_app: Callable[..., SphinxTestApp],
1049+
tmp_path: Path,
1050+
) -> None:
1051+
"""
1052+
Flat numbered lists become separate Notion NumberedItem blocks.
1053+
"""
1054+
rst_content = """
1055+
1. First numbered point
1056+
2. Second numbered point
1057+
3. Third numbered point with longer text
1058+
"""
1059+
expected_objects: list[Block] = [
1060+
UnoNumberedItem(text=text(text="First numbered point")),
1061+
UnoNumberedItem(text=text(text="Second numbered point")),
1062+
UnoNumberedItem(
1063+
text=text(text="Third numbered point with longer text")
1064+
),
1065+
]
1066+
_assert_rst_converts_to_notion_objects(
1067+
rst_content=rst_content,
1068+
expected_objects=expected_objects,
1069+
make_app=make_app,
1070+
tmp_path=tmp_path,
1071+
)
1072+
1073+
1074+
def test_numbered_list_with_inline_formatting(
1075+
*,
1076+
make_app: Callable[..., SphinxTestApp],
1077+
tmp_path: Path,
1078+
) -> None:
1079+
"""
1080+
Numbered lists preserve inline formatting in rich text.
1081+
"""
1082+
rst_content = """
1083+
1. This is **bold text** in a numbered list
1084+
"""
1085+
numbered_item = UnoNumberedItem(
1086+
text=(
1087+
text(text="This is ", bold=False, italic=False, code=False)
1088+
+ text(text="bold text", bold=True, italic=False, code=False)
1089+
+ text(
1090+
text=" in a numbered list",
1091+
bold=False,
1092+
italic=False,
1093+
code=False,
1094+
)
1095+
)
1096+
)
1097+
1098+
expected_objects: list[Block] = [
1099+
numbered_item,
1100+
]
1101+
1102+
_assert_rst_converts_to_notion_objects(
1103+
rst_content=rst_content,
1104+
expected_objects=expected_objects,
1105+
make_app=make_app,
1106+
tmp_path=tmp_path,
1107+
)
1108+
1109+
1110+
def test_nested_numbered_list(
1111+
*,
1112+
make_app: Callable[..., SphinxTestApp],
1113+
tmp_path: Path,
1114+
) -> None:
1115+
"""
1116+
Deeply nested numbered lists create hierarchical block structures.
1117+
"""
1118+
rst_content = """
1119+
1. Top level item
1120+
2. Top level with children
1121+
1122+
1. Second level item
1123+
2. Second level with children
1124+
1125+
1. Third level item (now allowed!)
1126+
1127+
3. Another top level item
1128+
"""
1129+
1130+
third_level_1 = UnoNumberedItem(
1131+
text=text(text="Third level item (now allowed!)")
1132+
)
1133+
1134+
second_level_1 = UnoNumberedItem(text=text(text="Second level item"))
1135+
second_level_2 = UnoNumberedItem(
1136+
text=text(text="Second level with children")
1137+
)
1138+
1139+
top_level_1 = UnoNumberedItem(text=text(text="Top level item"))
1140+
top_level_2 = UnoNumberedItem(text=text(text="Top level with children"))
1141+
1142+
second_level_2.append(blocks=[third_level_1])
1143+
top_level_2.append(blocks=[second_level_1])
1144+
top_level_2.append(blocks=[second_level_2])
1145+
1146+
top_level_3 = UnoNumberedItem(text=text(text="Another top level item"))
1147+
1148+
expected_objects: list[Block] = [
1149+
top_level_1,
1150+
top_level_2,
1151+
top_level_3,
1152+
]
1153+
1154+
_assert_rst_converts_to_notion_objects(
1155+
rst_content=rst_content,
1156+
expected_objects=expected_objects,
1157+
make_app=make_app,
1158+
tmp_path=tmp_path,
1159+
)
1160+
1161+
10441162
def test_collapse_block(
10451163
*,
10461164
make_app: Callable[..., SphinxTestApp],

0 commit comments

Comments
 (0)