notionary 0.2.17__py3-none-any.whl → 0.2.19__py3-none-any.whl

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 (113) hide show
  1. notionary/__init__.py +3 -2
  2. notionary/blocks/__init__.py +54 -25
  3. notionary/blocks/audio/__init__.py +7 -0
  4. notionary/blocks/audio/audio_element.py +152 -0
  5. notionary/blocks/audio/audio_markdown_node.py +29 -0
  6. notionary/blocks/audio/audio_models.py +59 -0
  7. notionary/blocks/bookmark/__init__.py +7 -0
  8. notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
  9. notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
  10. notionary/blocks/bookmark/bookmark_models.py +0 -0
  11. notionary/blocks/bulleted_list/__init__.py +7 -0
  12. notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
  13. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
  14. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
  15. notionary/blocks/callout/__init__.py +7 -0
  16. notionary/blocks/callout/callout_element.py +132 -0
  17. notionary/blocks/callout/callout_markdown_node.py +31 -0
  18. notionary/blocks/callout/callout_models.py +0 -0
  19. notionary/blocks/code/__init__.py +7 -0
  20. notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
  21. notionary/blocks/code/code_markdown_node.py +43 -0
  22. notionary/blocks/code/code_models.py +0 -0
  23. notionary/blocks/column/__init__.py +5 -0
  24. notionary/blocks/{column_element.py → column/column_element.py} +24 -55
  25. notionary/blocks/column/column_models.py +0 -0
  26. notionary/blocks/divider/__init__.py +7 -0
  27. notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
  28. notionary/blocks/divider/divider_markdown_node.py +24 -0
  29. notionary/blocks/divider/divider_models.py +0 -0
  30. notionary/blocks/document/__init__.py +7 -0
  31. notionary/blocks/document/document_element.py +102 -0
  32. notionary/blocks/document/document_markdown_node.py +31 -0
  33. notionary/blocks/document/document_models.py +0 -0
  34. notionary/blocks/embed/__init__.py +7 -0
  35. notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
  36. notionary/blocks/embed/embed_markdown_node.py +30 -0
  37. notionary/blocks/embed/embed_models.py +0 -0
  38. notionary/blocks/heading/__init__.py +7 -0
  39. notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
  40. notionary/blocks/heading/heading_markdown_node.py +29 -0
  41. notionary/blocks/heading/heading_models.py +0 -0
  42. notionary/blocks/image/__init__.py +7 -0
  43. notionary/blocks/{image_element.py → image/image_element.py} +62 -42
  44. notionary/blocks/image/image_markdown_node.py +33 -0
  45. notionary/blocks/image/image_models.py +0 -0
  46. notionary/blocks/markdown_builder.py +356 -0
  47. notionary/blocks/markdown_node.py +29 -0
  48. notionary/blocks/mention/__init__.py +7 -0
  49. notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
  50. notionary/blocks/mention/mention_markdown_node.py +38 -0
  51. notionary/blocks/mention/mention_models.py +0 -0
  52. notionary/blocks/numbered_list/__init__.py +7 -0
  53. notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
  54. notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
  55. notionary/blocks/numbered_list/numbered_list_models.py +0 -0
  56. notionary/blocks/paragraph/__init__.py +7 -0
  57. notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
  58. notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
  59. notionary/blocks/paragraph/paragraph_models.py +0 -0
  60. notionary/blocks/quote/__init__.py +7 -0
  61. notionary/blocks/quote/quote_element.py +92 -0
  62. notionary/blocks/quote/quote_markdown_node.py +23 -0
  63. notionary/blocks/quote/quote_models.py +0 -0
  64. notionary/blocks/registry/block_registry.py +17 -3
  65. notionary/blocks/registry/block_registry_builder.py +90 -178
  66. notionary/blocks/shared/__init__.py +0 -0
  67. notionary/blocks/shared/block_client.py +256 -0
  68. notionary/blocks/shared/models.py +713 -0
  69. notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
  70. notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
  71. notionary/blocks/shared/text_inline_formatter_new.py +139 -0
  72. notionary/blocks/table/__init__.py +7 -0
  73. notionary/blocks/{table_element.py → table/table_element.py} +23 -11
  74. notionary/blocks/table/table_markdown_node.py +40 -0
  75. notionary/blocks/table/table_models.py +0 -0
  76. notionary/blocks/todo/__init__.py +7 -0
  77. notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
  78. notionary/blocks/todo/todo_markdown_node.py +31 -0
  79. notionary/blocks/todo/todo_models.py +0 -0
  80. notionary/blocks/toggle/__init__.py +4 -0
  81. notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
  82. notionary/blocks/toggle/toggle_markdown_node.py +35 -0
  83. notionary/blocks/toggle/toggle_models.py +0 -0
  84. notionary/blocks/toggleable_heading/__init__.py +9 -0
  85. notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
  86. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
  87. notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  88. notionary/blocks/video/__init__.py +7 -0
  89. notionary/blocks/{video_element.py → video/video_element.py} +82 -57
  90. notionary/blocks/video/video_markdown_node.py +30 -0
  91. notionary/file_upload/notion_file_upload.py +1 -1
  92. notionary/page/content/markdown_whitespace_processor.py +80 -0
  93. notionary/page/content/notion_text_length_utils.py +87 -0
  94. notionary/page/content/page_content_retriever.py +18 -10
  95. notionary/page/content/page_content_writer.py +97 -148
  96. notionary/page/formatting/line_processor.py +153 -0
  97. notionary/page/formatting/markdown_to_notion_converter.py +104 -425
  98. notionary/page/notion_page.py +9 -11
  99. notionary/page/notion_to_markdown_converter.py +9 -13
  100. notionary/util/factory_decorator.py +0 -0
  101. notionary/workspace.py +0 -1
  102. {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/METADATA +1 -1
  103. notionary-0.2.19.dist-info/RECORD +150 -0
  104. notionary/blocks/audio_element.py +0 -144
  105. notionary/blocks/callout_element.py +0 -122
  106. notionary/blocks/document_element.py +0 -194
  107. notionary/blocks/notion_block_client.py +0 -26
  108. notionary/blocks/qoute_element.py +0 -169
  109. notionary/page/content/notion_page_content_chunker.py +0 -84
  110. notionary/page/formatting/spacer_rules.py +0 -483
  111. notionary-0.2.17.dist-info/RECORD +0 -85
  112. {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/LICENSE +0 -0
  113. {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/WHEEL +0 -0
@@ -1,18 +1,21 @@
1
- from typing import Dict, Any, Optional
1
+ from typing import Optional, Any, TypeAlias, Union
2
2
  from abc import ABC
3
3
 
4
4
  from notionary.blocks.prompts.element_prompt_content import ElementPromptContent
5
5
 
6
+ NotionBlock: TypeAlias = dict[str, Any]
7
+ NotionBlockResult: TypeAlias = Optional[Union[list[dict[str, Any]], dict[str, Any]]]
8
+
6
9
 
7
10
  class NotionBlockElement(ABC):
8
11
  """Base class for elements that can be converted between Markdown and Notion."""
9
12
 
10
13
  @classmethod
11
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
12
- """Convert markdown to Notion block."""
14
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
15
+ """Convert markdown to Notion blocks (can return multiple blocks or single block)."""
13
16
 
14
17
  @classmethod
15
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
18
+ def notion_to_markdown(cls, block: dict[str, any]) -> Optional[str]:
16
19
  """Convert Notion block to markdown."""
17
20
 
18
21
  @classmethod
@@ -21,7 +24,7 @@ class NotionBlockElement(ABC):
21
24
  return bool(cls.markdown_to_notion(text)) # Now calls the class's version
22
25
 
23
26
  @classmethod
24
- def match_notion(cls, block: Dict[str, Any]) -> bool:
27
+ def match_notion(cls, block: dict[str, any]) -> bool:
25
28
  """Check if this element can handle the given Notion block."""
26
29
  return bool(cls.notion_to_markdown(block)) # Now calls the class's version
27
30
 
@@ -1,4 +1,4 @@
1
- from typing import Dict, Any, List, Tuple
1
+ from typing import Any
2
2
  import re
3
3
 
4
4
  from notionary.blocks import ElementPromptBuilder, ElementPromptContent
@@ -30,7 +30,7 @@ class TextInlineFormatter:
30
30
  ]
31
31
 
32
32
  @classmethod
33
- def parse_inline_formatting(cls, text: str) -> List[Dict[str, Any]]:
33
+ def parse_inline_formatting(cls, text: str) -> list[dict[str, Any]]:
34
34
  """
35
35
  Parse inline text formatting into Notion rich_text format.
36
36
 
@@ -38,7 +38,7 @@ class TextInlineFormatter:
38
38
  text: Markdown text with inline formatting
39
39
 
40
40
  Returns:
41
- List of Notion rich_text objects
41
+ list of Notion rich_text objects
42
42
  """
43
43
  if not text:
44
44
  return []
@@ -47,17 +47,17 @@ class TextInlineFormatter:
47
47
 
48
48
  @classmethod
49
49
  def _split_text_into_segments(
50
- cls, text: str, format_patterns: List[Tuple]
51
- ) -> List[Dict[str, Any]]:
50
+ cls, text: str, format_patterns: list[tuple]
51
+ ) -> list[dict[str, Any]]:
52
52
  """
53
53
  Split text into segments by formatting markers and convert to Notion rich_text format.
54
54
 
55
55
  Args:
56
56
  text: Text to split
57
- format_patterns: List of (regex pattern, formatting dict) tuples
57
+ format_patterns: list of (regex pattern, formatting dict) tuples
58
58
 
59
59
  Returns:
60
- List of Notion rich_text objects
60
+ list of Notion rich_text objects
61
61
  """
62
62
  segments = []
63
63
  remaining_text = text
@@ -107,8 +107,8 @@ class TextInlineFormatter:
107
107
 
108
108
  @classmethod
109
109
  def _create_text_element(
110
- cls, text: str, formatting: Dict[str, Any]
111
- ) -> Dict[str, Any]:
110
+ cls, text: str, formatting: dict[str, Any]
111
+ ) -> dict[str, Any]:
112
112
  """
113
113
  Create a Notion text element with formatting.
114
114
 
@@ -136,7 +136,7 @@ class TextInlineFormatter:
136
136
  }
137
137
 
138
138
  @classmethod
139
- def _create_link_element(cls, text: str, url: str) -> Dict[str, Any]:
139
+ def _create_link_element(cls, text: str, url: str) -> dict[str, Any]:
140
140
  """
141
141
  Create a Notion link element.
142
142
 
@@ -155,7 +155,7 @@ class TextInlineFormatter:
155
155
  }
156
156
 
157
157
  @classmethod
158
- def _create_mention_element(cls, id: str) -> Dict[str, Any]:
158
+ def _create_mention_element(cls, id: str) -> dict[str, Any]:
159
159
  """
160
160
  Create a Notion mention element.
161
161
 
@@ -172,12 +172,12 @@ class TextInlineFormatter:
172
172
  }
173
173
 
174
174
  @classmethod
175
- def extract_text_with_formatting(cls, rich_text: List[Dict[str, Any]]) -> str:
175
+ def extract_text_with_formatting(cls, rich_text: list[dict[str, Any]]) -> str:
176
176
  """
177
177
  Convert Notion rich_text elements back to Markdown formatted text.
178
178
 
179
179
  Args:
180
- rich_text: List of Notion rich_text elements
180
+ rich_text: list of Notion rich_text elements
181
181
 
182
182
  Returns:
183
183
  Markdown formatted text
@@ -214,7 +214,7 @@ class TextInlineFormatter:
214
214
  return "".join(formatted_parts)
215
215
 
216
216
  @classmethod
217
- def _default_annotations(cls) -> Dict[str, bool]:
217
+ def _default_annotations(cls) -> dict[str, bool]:
218
218
  """
219
219
  Create default annotations object.
220
220
 
@@ -0,0 +1,139 @@
1
+ from typing import Optional
2
+ import re
3
+
4
+ # TODO: Use this inline formatting here
5
+ from notionary.blocks.shared.models import (
6
+ MentionRichText,
7
+ RichTextObject,
8
+ TextAnnotations,
9
+ TextContent,
10
+ )
11
+
12
+ FORMAT_PATTERNS = [
13
+ (r"\*\*(.+?)\*\*", {"bold": True}),
14
+ (r"\*(.+?)\*", {"italic": True}),
15
+ (r"_(.+?)_", {"italic": True}),
16
+ (r"__(.+?)__", {"underline": True}),
17
+ (r"~~(.+?)~~", {"strikethrough": True}),
18
+ (r"`(.+?)`", {"code": True}),
19
+ (r"\[(.+?)\]\((.+?)\)", {"link": True}),
20
+ (r"@\[([0-9a-f-]+)\]", {"mention": True}),
21
+ ]
22
+
23
+
24
+ def parse_inline_formatting(text: str) -> list[dict[str, any]]:
25
+ """Parse inline text formatting into Notion rich_text format."""
26
+ if not text:
27
+ return []
28
+
29
+ return _split_text_into_segments(text)
30
+
31
+
32
+ def _split_text_into_segments(text: str) -> list[dict[str, any]]:
33
+ """Split text into segments by formatting markers."""
34
+ segments = []
35
+ remaining_text = text
36
+
37
+ while remaining_text:
38
+ match_info = _find_earliest_match(remaining_text)
39
+
40
+ # No more formatting found - add remaining text and exit
41
+ if not match_info:
42
+ segments.append(_create_plain_text(remaining_text))
43
+ break
44
+
45
+ match, formatting, pos = match_info
46
+
47
+ # Add text before match if exists
48
+ if pos > 0:
49
+ segments.append(_create_plain_text(remaining_text[:pos]))
50
+
51
+ # Add formatted segment
52
+ segments.append(_create_formatted_segment(match, formatting))
53
+
54
+ # Update remaining text
55
+ remaining_text = remaining_text[pos + len(match.group(0)) :]
56
+
57
+ return segments
58
+
59
+
60
+ def _find_earliest_match(text: str) -> Optional[tuple]:
61
+ """Find the earliest formatting match in text."""
62
+ earliest_match = None
63
+ earliest_format = None
64
+ earliest_pos = len(text)
65
+
66
+ for pattern, formatting in FORMAT_PATTERNS:
67
+ match = re.search(pattern, text)
68
+ if match and match.start() < earliest_pos:
69
+ earliest_match = match
70
+ earliest_format = formatting
71
+ earliest_pos = match.start()
72
+
73
+ return (earliest_match, earliest_format, earliest_pos) if earliest_match else None
74
+
75
+
76
+ def _create_formatted_segment(match: re.Match, formatting: dict) -> dict[str, any]:
77
+ """Create a formatted segment based on match and formatting."""
78
+ if "link" in formatting:
79
+ return _create_link_text(match.group(1), match.group(2))
80
+ elif "mention" in formatting:
81
+ return _create_mention_text(match.group(1))
82
+ else:
83
+ return _create_formatted_text(match.group(1), **formatting)
84
+
85
+
86
+ def _create_plain_text(content: str) -> dict[str, any]:
87
+ """Create plain text rich text object."""
88
+ return RichTextObject.from_plain_text(content).model_dump()
89
+
90
+
91
+ def _create_formatted_text(content: str, **formatting) -> dict[str, any]:
92
+ """Create formatted text rich text object."""
93
+ return RichTextObject.from_plain_text(content, **formatting).model_dump()
94
+
95
+
96
+ def _create_link_text(content: str, url: str) -> dict[str, any]:
97
+ """Create link text rich text object."""
98
+ text_content = TextContent(content=content, link=url)
99
+ annotations = TextAnnotations()
100
+
101
+ rich_text = RichTextObject(
102
+ text=text_content, annotations=annotations, plain_text=content, href=url
103
+ )
104
+ return rich_text.model_dump()
105
+
106
+
107
+ def _create_mention_text(page_id: str) -> dict[str, any]:
108
+ """Create mention rich text object."""
109
+ return MentionRichText.from_page_id(page_id).model_dump()
110
+
111
+
112
+ def extract_text_with_formatting(rich_text: list[dict[str, any]]) -> str:
113
+ """Convert Notion rich_text elements back to Markdown."""
114
+ return "".join(_rich_text_to_markdown(item) for item in rich_text)
115
+
116
+
117
+ def _rich_text_to_markdown(text_obj: dict[str, any]) -> str:
118
+ """Convert single rich text object to markdown."""
119
+ content = text_obj.get("plain_text", text_obj.get("text", {}).get("content", ""))
120
+ annotations = text_obj.get("annotations", {})
121
+
122
+ # Apply formatting in reverse order
123
+ if annotations.get("code", False):
124
+ content = f"`{content}`"
125
+ if annotations.get("strikethrough", False):
126
+ content = f"~~{content}~~"
127
+ if annotations.get("underline", False):
128
+ content = f"__{content}__"
129
+ if annotations.get("italic", False):
130
+ content = f"*{content}*"
131
+ if annotations.get("bold", False):
132
+ content = f"**{content}**"
133
+
134
+ # Handle links
135
+ link_data = text_obj.get("text", {}).get("link")
136
+ if link_data and link_data.get("url"):
137
+ content = f"[{content}]({link_data['url']})"
138
+
139
+ return content
@@ -0,0 +1,7 @@
1
+ from .table_element import TableElement
2
+ from .table_markdown_node import TableMarkdownNode
3
+
4
+ __all__ = [
5
+ "TableElement",
6
+ "TableMarkdownNode",
7
+ ]
@@ -1,9 +1,13 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
3
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks.text_inline_formatter import TextInlineFormatter
6
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
4
+ from notionary.blocks import (
5
+ NotionBlockElement,
6
+ NotionBlockResult,
7
+ ElementPromptContent,
8
+ ElementPromptBuilder,
9
+ )
10
+ from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
7
11
 
8
12
 
9
13
  class TableElement(NotionBlockElement):
@@ -24,17 +28,20 @@ class TableElement(NotionBlockElement):
24
28
 
25
29
  @classmethod
26
30
  def match_markdown(cls, text: str) -> bool:
27
- """Check if text contains a markdown table."""
31
+ """
32
+ Check if text contains a markdown table.
33
+ Accepts tables with only header + separator, as well as header + separator + data rows.
34
+ """
28
35
  lines = text.split("\n")
29
36
 
30
- if len(lines) < 3:
37
+ if len(lines) < 2:
31
38
  return False
32
39
 
33
- for i, line in enumerate(lines[:-2]):
40
+ # Akzeptiere Header + Separator auch ohne Datenzeile
41
+ for i, line in enumerate(lines[:-1]):
34
42
  if (
35
- TableElement.ROW_PATTERN.match(line)
36
- and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
37
- and TableElement.ROW_PATTERN.match(lines[i + 2])
43
+ cls.ROW_PATTERN.match(line)
44
+ and cls.SEPARATOR_PATTERN.match(lines[i + 1])
38
45
  ):
39
46
  return True
40
47
 
@@ -46,7 +53,7 @@ class TableElement(NotionBlockElement):
46
53
  return block.get("type") == "table"
47
54
 
48
55
  @classmethod
49
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
56
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
50
57
  """Convert markdown table to Notion table block."""
51
58
  if not TableElement.match_markdown(text):
52
59
  return None
@@ -67,7 +74,7 @@ class TableElement(NotionBlockElement):
67
74
  column_count = len(rows[0])
68
75
  TableElement._normalize_row_lengths(rows, column_count)
69
76
 
70
- return {
77
+ table_block = {
71
78
  "type": "table",
72
79
  "table": {
73
80
  "table_width": column_count,
@@ -77,6 +84,11 @@ class TableElement(NotionBlockElement):
77
84
  },
78
85
  }
79
86
 
87
+ # Leerer Paragraph nach der Tabelle
88
+ empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
89
+
90
+ return [table_block, empty_paragraph]
91
+
80
92
  @classmethod
81
93
  def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
82
94
  """Convert Notion table block to markdown table."""
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+ from notionary.blocks.markdown_node import MarkdownNode
5
+
6
+
7
+ class TableMarkdownBlockParams(BaseModel):
8
+ headers: list[str]
9
+ rows: list[list[str]]
10
+
11
+
12
+ class TableMarkdownNode(MarkdownNode):
13
+ """
14
+ Programmatic interface for creating Markdown tables.
15
+ Example:
16
+ | Header 1 | Header 2 | Header 3 |
17
+ | -------- | -------- | -------- |
18
+ | Cell 1 | Cell 2 | Cell 3 |
19
+ | Cell 4 | Cell 5 | Cell 6 |
20
+ """
21
+
22
+ def __init__(self, headers: list[str], rows: list[list[str]]):
23
+ if not headers or not all(isinstance(row, list) for row in rows):
24
+ raise ValueError("headers must be a list and rows must be a list of lists")
25
+ self.headers = headers
26
+ self.rows = rows
27
+
28
+ @classmethod
29
+ def from_params(cls, params: TableMarkdownBlockParams) -> TableMarkdownNode:
30
+ return cls(headers=params.headers, rows=params.rows)
31
+
32
+ def to_markdown(self) -> str:
33
+ col_count = len(self.headers)
34
+ # Header row
35
+ header = "| " + " | ".join(self.headers) + " |"
36
+ # Separator row
37
+ separator = "| " + " | ".join(["--------"] * col_count) + " |"
38
+ # Data rows
39
+ data_rows = ["| " + " | ".join(row) + " |" for row in self.rows]
40
+ return "\n".join([header, separator] + data_rows)
File without changes
@@ -0,0 +1,7 @@
1
+ from .todo_element import TodoElement
2
+ from .todo_markdown_node import TodoMarkdownNode
3
+
4
+ __all__ = [
5
+ "TodoElement",
6
+ "TodoMarkdownNode",
7
+ ]
@@ -1,9 +1,13 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
3
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
6
- from notionary.blocks.text_inline_formatter import TextInlineFormatter
4
+ from notionary.blocks import (
5
+ ElementPromptContent,
6
+ ElementPromptBuilder,
7
+ NotionBlockResult,
8
+ NotionBlockElement,
9
+ )
10
+ from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
7
11
 
8
12
 
9
13
  class TodoElement(NotionBlockElement):
@@ -34,7 +38,7 @@ class TodoElement(NotionBlockElement):
34
38
  return block.get("type") == "to_do"
35
39
 
36
40
  @classmethod
37
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
41
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
38
42
  """Convert markdown todo item to Notion to_do block."""
39
43
  done_match = TodoElement.DONE_PATTERN.match(text)
40
44
  if done_match:
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+ from notionary.blocks.markdown_node import MarkdownNode
5
+
6
+
7
+ class TodoMarkdownBlockParams(BaseModel):
8
+ text: str
9
+ checked: bool = False
10
+ marker: str = "-"
11
+
12
+
13
+ class TodoMarkdownNode(MarkdownNode):
14
+ """
15
+ Programmatic interface for creating Markdown todo items (checkboxes).
16
+ Supports checked and unchecked states.
17
+ Example: - [ ] Task, - [x] Done
18
+ """
19
+
20
+ def __init__(self, text: str, checked: bool = False, marker: str = "-"):
21
+ self.text = text
22
+ self.checked = checked
23
+ self.marker = marker if marker in {"-", "*", "+"} else "-"
24
+
25
+ @classmethod
26
+ def from_params(cls, params: TodoMarkdownBlockParams) -> TodoMarkdownNode:
27
+ return cls(text=params.text, checked=params.checked, marker=params.marker)
28
+
29
+ def to_markdown(self) -> str:
30
+ checkbox = "[x]" if self.checked else "[ ]"
31
+ return f"{self.marker} {checkbox} {self.text}"
File without changes
@@ -0,0 +1,4 @@
1
+ from .toggle_element import ToggleElement
2
+ from .toggle_markdown_node import ToggleMarkdownNode, ToggleMarkdownBlockParams
3
+
4
+ __all__ = ["ToggleElement", "ToggleMarkdownNode", "ToggleMarkdownBlockParams"]
@@ -1,8 +1,12 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple, Callable
3
3
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
4
+ from notionary.blocks import (
5
+ NotionBlockElement,
6
+ NotionBlockResult,
7
+ ElementPromptContent,
8
+ ElementPromptBuilder,
9
+ )
6
10
 
7
11
 
8
12
  class ToggleElement(NotionBlockElement):
@@ -26,7 +30,7 @@ class ToggleElement(NotionBlockElement):
26
30
  return block.get("type") == "toggle"
27
31
 
28
32
  @classmethod
29
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
33
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
30
34
  """Convert markdown toggle line to Notion toggle block."""
31
35
  toggle_match = ToggleElement.TOGGLE_PATTERN.match(text.strip())
32
36
  if not toggle_match:
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, List
4
+ from pydantic import BaseModel
5
+ from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+
8
+ class ToggleMarkdownBlockParams(BaseModel):
9
+ title: str
10
+ content: Optional[List[str]] = None
11
+
12
+
13
+ class ToggleMarkdownNode(MarkdownNode):
14
+ """
15
+ Programmatic interface for creating Notion-style Markdown toggle blocks
16
+ with pipe-prefixed nested content.
17
+ Example:
18
+ +++ Details
19
+ | Here are the details.
20
+ | You can add more lines.
21
+ """
22
+
23
+ def __init__(self, title: str, content: Optional[List[str]] = None):
24
+ self.title = title
25
+ self.content = content or []
26
+
27
+ @classmethod
28
+ def from_params(cls, params: ToggleMarkdownBlockParams) -> ToggleMarkdownNode:
29
+ return cls(title=params.title, content=params.content)
30
+
31
+ def to_markdown(self) -> str:
32
+ result = f"+++ {self.title}"
33
+ if self.content:
34
+ result += "\n" + "\n".join([f"| {line}" for line in self.content])
35
+ return result
File without changes
@@ -0,0 +1,9 @@
1
+ from .toggleable_heading_element import ToggleableHeadingElement
2
+ from .toggleable_heading_markdown_node import (
3
+ ToggleableHeadingMarkdownNode,
4
+ )
5
+
6
+ __all__ = [
7
+ "ToggleableHeadingElement",
8
+ "ToggleableHeadingMarkdownNode",
9
+ ]
@@ -1,9 +1,13 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional, List, Tuple, Callable
3
3
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
6
- from notionary.blocks.text_inline_formatter import TextInlineFormatter
4
+ from notionary.blocks import (
5
+ ElementPromptContent,
6
+ ElementPromptBuilder,
7
+ NotionBlockElement,
8
+ NotionBlockResult,
9
+ )
10
+ from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
7
11
 
8
12
 
9
13
  class ToggleableHeadingElement(NotionBlockElement):
@@ -29,7 +33,7 @@ class ToggleableHeadingElement(NotionBlockElement):
29
33
  return heading_data.get("is_toggleable", False) is True
30
34
 
31
35
  @staticmethod
32
- def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
36
+ def markdown_to_notion(text: str) -> NotionBlockResult:
33
37
  """Convert markdown collapsible heading to Notion toggleable heading block."""
34
38
  header_match = ToggleableHeadingElement.PATTERN.match(text)
35
39
  if not header_match:
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, List
4
+ from pydantic import BaseModel
5
+ from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+
8
+ class ToggleableHeadingMarkdownBlockParams(BaseModel):
9
+ text: str
10
+ level: int = 1
11
+ content: Optional[List[str]] = None
12
+
13
+
14
+ class ToggleableHeadingMarkdownNode(MarkdownNode):
15
+ """
16
+ Programmatic interface for creating collapsible Markdown headings (toggleable headings).
17
+ Pipe-prefixed lines are used for the collapsible content.
18
+ Example:
19
+ +# Section
20
+ | Hidden content
21
+ +## Subsection
22
+ | Details
23
+ """
24
+
25
+ def __init__(self, text: str, level: int = 1, content: Optional[list[str]] = None):
26
+ if not (1 <= level <= 3):
27
+ raise ValueError("Only heading levels 1-3 are supported (H1, H2, H3)")
28
+ self.text = text
29
+ self.level = level
30
+ self.content = content or []
31
+
32
+ @classmethod
33
+ def from_params(
34
+ cls, params: ToggleableHeadingMarkdownBlockParams
35
+ ) -> ToggleableHeadingMarkdownNode:
36
+ return cls(text=params.text, level=params.level, content=params.content)
37
+
38
+ def to_markdown(self) -> str:
39
+ prefix = "+" + ("#" * self.level)
40
+ result = f"{prefix} {self.text}"
41
+ if self.content:
42
+ result += "\n" + "\n".join([f"| {line}" for line in self.content])
43
+ return result
@@ -0,0 +1,7 @@
1
+ from .video_element import VideoElement
2
+ from .video_markdown_node import VideoMarkdownNode
3
+
4
+ __all__ = [
5
+ "VideoElement",
6
+ "VideoMarkdownNode",
7
+ ]