notionary 0.2.19__py3-none-any.whl → 0.2.22__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 (220) hide show
  1. notionary/__init__.py +8 -4
  2. notionary/base_notion_client.py +3 -1
  3. notionary/blocks/__init__.py +2 -91
  4. notionary/blocks/_bootstrap.py +271 -0
  5. notionary/blocks/audio/__init__.py +8 -2
  6. notionary/blocks/audio/audio_element.py +69 -106
  7. notionary/blocks/audio/audio_markdown_node.py +13 -5
  8. notionary/blocks/audio/audio_models.py +6 -55
  9. notionary/blocks/base_block_element.py +42 -0
  10. notionary/blocks/bookmark/__init__.py +9 -2
  11. notionary/blocks/bookmark/bookmark_element.py +49 -139
  12. notionary/blocks/bookmark/bookmark_markdown_node.py +19 -18
  13. notionary/blocks/bookmark/bookmark_models.py +15 -0
  14. notionary/blocks/breadcrumbs/__init__.py +17 -0
  15. notionary/blocks/breadcrumbs/breadcrumb_element.py +39 -0
  16. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +32 -0
  17. notionary/blocks/breadcrumbs/breadcrumb_models.py +12 -0
  18. notionary/blocks/bulleted_list/__init__.py +12 -2
  19. notionary/blocks/bulleted_list/bulleted_list_element.py +55 -53
  20. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +2 -1
  21. notionary/blocks/bulleted_list/bulleted_list_models.py +18 -0
  22. notionary/blocks/callout/__init__.py +9 -2
  23. notionary/blocks/callout/callout_element.py +53 -86
  24. notionary/blocks/callout/callout_markdown_node.py +3 -1
  25. notionary/blocks/callout/callout_models.py +33 -0
  26. notionary/blocks/child_database/__init__.py +14 -0
  27. notionary/blocks/child_database/child_database_element.py +61 -0
  28. notionary/blocks/child_database/child_database_models.py +12 -0
  29. notionary/blocks/child_page/__init__.py +9 -0
  30. notionary/blocks/child_page/child_page_element.py +94 -0
  31. notionary/blocks/child_page/child_page_models.py +12 -0
  32. notionary/blocks/{shared/block_client.py → client.py} +54 -54
  33. notionary/blocks/code/__init__.py +6 -2
  34. notionary/blocks/code/code_element.py +96 -181
  35. notionary/blocks/code/code_markdown_node.py +64 -13
  36. notionary/blocks/code/code_models.py +94 -0
  37. notionary/blocks/column/__init__.py +25 -1
  38. notionary/blocks/column/column_element.py +44 -312
  39. notionary/blocks/column/column_list_element.py +52 -0
  40. notionary/blocks/column/column_list_markdown_node.py +50 -0
  41. notionary/blocks/column/column_markdown_node.py +59 -0
  42. notionary/blocks/column/column_models.py +26 -0
  43. notionary/blocks/divider/__init__.py +9 -2
  44. notionary/blocks/divider/divider_element.py +18 -49
  45. notionary/blocks/divider/divider_markdown_node.py +2 -1
  46. notionary/blocks/divider/divider_models.py +12 -0
  47. notionary/blocks/embed/__init__.py +9 -2
  48. notionary/blocks/embed/embed_element.py +65 -111
  49. notionary/blocks/embed/embed_markdown_node.py +3 -1
  50. notionary/blocks/embed/embed_models.py +14 -0
  51. notionary/blocks/equation/__init__.py +14 -0
  52. notionary/blocks/equation/equation_element.py +133 -0
  53. notionary/blocks/equation/equation_element_markdown_node.py +35 -0
  54. notionary/blocks/equation/equation_models.py +11 -0
  55. notionary/blocks/file/__init__.py +25 -0
  56. notionary/blocks/file/file_element.py +112 -0
  57. notionary/blocks/file/file_element_markdown_node.py +37 -0
  58. notionary/blocks/file/file_element_models.py +39 -0
  59. notionary/blocks/guards.py +22 -0
  60. notionary/blocks/heading/__init__.py +16 -2
  61. notionary/blocks/heading/heading_element.py +83 -69
  62. notionary/blocks/heading/heading_markdown_node.py +2 -1
  63. notionary/blocks/heading/heading_models.py +29 -0
  64. notionary/blocks/image_block/__init__.py +13 -0
  65. notionary/blocks/image_block/image_element.py +89 -0
  66. notionary/blocks/{image → image_block}/image_markdown_node.py +13 -6
  67. notionary/blocks/image_block/image_models.py +10 -0
  68. notionary/blocks/mixins/captions/__init__.py +4 -0
  69. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  70. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  71. notionary/blocks/models.py +174 -0
  72. notionary/blocks/numbered_list/__init__.py +12 -2
  73. notionary/blocks/numbered_list/numbered_list_element.py +48 -56
  74. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
  75. notionary/blocks/numbered_list/numbered_list_models.py +17 -0
  76. notionary/blocks/paragraph/__init__.py +12 -2
  77. notionary/blocks/paragraph/paragraph_element.py +40 -66
  78. notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
  79. notionary/blocks/paragraph/paragraph_models.py +16 -0
  80. notionary/blocks/pdf/__init__.py +13 -0
  81. notionary/blocks/pdf/pdf_element.py +97 -0
  82. notionary/blocks/pdf/pdf_markdown_node.py +37 -0
  83. notionary/blocks/pdf/pdf_models.py +11 -0
  84. notionary/blocks/quote/__init__.py +11 -2
  85. notionary/blocks/quote/quote_element.py +45 -62
  86. notionary/blocks/quote/quote_markdown_node.py +6 -3
  87. notionary/blocks/quote/quote_models.py +18 -0
  88. notionary/blocks/registry/__init__.py +4 -0
  89. notionary/blocks/registry/block_registry.py +60 -121
  90. notionary/blocks/registry/block_registry_builder.py +115 -59
  91. notionary/blocks/rich_text/__init__.py +33 -0
  92. notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
  93. notionary/blocks/rich_text/rich_text_models.py +221 -0
  94. notionary/blocks/rich_text/text_inline_formatter.py +456 -0
  95. notionary/blocks/syntax_prompt_builder.py +137 -0
  96. notionary/blocks/table/__init__.py +16 -2
  97. notionary/blocks/table/table_element.py +136 -228
  98. notionary/blocks/table/table_markdown_node.py +2 -1
  99. notionary/blocks/table/table_models.py +28 -0
  100. notionary/blocks/table_of_contents/__init__.py +19 -0
  101. notionary/blocks/table_of_contents/table_of_contents_element.py +68 -0
  102. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
  103. notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
  104. notionary/blocks/todo/__init__.py +9 -2
  105. notionary/blocks/todo/todo_element.py +52 -92
  106. notionary/blocks/todo/todo_markdown_node.py +2 -1
  107. notionary/blocks/todo/todo_models.py +19 -0
  108. notionary/blocks/toggle/__init__.py +13 -3
  109. notionary/blocks/toggle/toggle_element.py +69 -260
  110. notionary/blocks/toggle/toggle_markdown_node.py +25 -15
  111. notionary/blocks/toggle/toggle_models.py +17 -0
  112. notionary/blocks/toggleable_heading/__init__.py +6 -2
  113. notionary/blocks/toggleable_heading/toggleable_heading_element.py +86 -241
  114. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
  115. notionary/blocks/types.py +130 -0
  116. notionary/blocks/video/__init__.py +8 -2
  117. notionary/blocks/video/video_element.py +70 -141
  118. notionary/blocks/video/video_element_models.py +10 -0
  119. notionary/blocks/video/video_markdown_node.py +13 -6
  120. notionary/database/client.py +26 -8
  121. notionary/database/database.py +13 -14
  122. notionary/database/database_filter_builder.py +2 -2
  123. notionary/database/database_provider.py +5 -4
  124. notionary/database/models.py +337 -0
  125. notionary/database/notion_database.py +6 -7
  126. notionary/file_upload/client.py +5 -7
  127. notionary/file_upload/models.py +3 -2
  128. notionary/file_upload/notion_file_upload.py +2 -3
  129. notionary/markdown/markdown_builder.py +729 -0
  130. notionary/markdown/markdown_document_model.py +228 -0
  131. notionary/{blocks → markdown}/markdown_node.py +1 -0
  132. notionary/models/notion_database_response.py +0 -338
  133. notionary/page/client.py +34 -15
  134. notionary/page/models.py +327 -0
  135. notionary/page/notion_page.py +136 -58
  136. notionary/page/{content/page_content_writer.py → page_content_deleting_service.py} +25 -59
  137. notionary/page/page_content_writer.py +177 -0
  138. notionary/page/page_context.py +65 -0
  139. notionary/page/reader/handler/__init__.py +19 -0
  140. notionary/page/reader/handler/base_block_renderer.py +44 -0
  141. notionary/page/reader/handler/block_processing_context.py +35 -0
  142. notionary/page/reader/handler/block_rendering_context.py +48 -0
  143. notionary/page/reader/handler/column_list_renderer.py +51 -0
  144. notionary/page/reader/handler/column_renderer.py +60 -0
  145. notionary/page/reader/handler/line_renderer.py +73 -0
  146. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  147. notionary/page/reader/handler/toggle_renderer.py +69 -0
  148. notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
  149. notionary/page/reader/page_content_retriever.py +81 -0
  150. notionary/page/search_filter_builder.py +2 -1
  151. notionary/page/writer/handler/__init__.py +24 -0
  152. notionary/page/writer/handler/code_handler.py +72 -0
  153. notionary/page/writer/handler/column_handler.py +141 -0
  154. notionary/page/writer/handler/column_list_handler.py +139 -0
  155. notionary/page/writer/handler/equation_handler.py +74 -0
  156. notionary/page/writer/handler/line_handler.py +35 -0
  157. notionary/page/writer/handler/line_processing_context.py +54 -0
  158. notionary/page/writer/handler/regular_line_handler.py +86 -0
  159. notionary/page/writer/handler/table_handler.py +66 -0
  160. notionary/page/writer/handler/toggle_handler.py +155 -0
  161. notionary/page/writer/handler/toggleable_heading_handler.py +173 -0
  162. notionary/page/writer/markdown_to_notion_converter.py +95 -0
  163. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  164. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  165. notionary/page/writer/notion_text_length_processor.py +150 -0
  166. notionary/telemetry/__init__.py +2 -2
  167. notionary/telemetry/service.py +3 -3
  168. notionary/user/__init__.py +2 -2
  169. notionary/user/base_notion_user.py +2 -1
  170. notionary/user/client.py +2 -3
  171. notionary/user/models.py +1 -0
  172. notionary/user/notion_bot_user.py +4 -5
  173. notionary/user/notion_user.py +3 -4
  174. notionary/user/notion_user_manager.py +23 -95
  175. notionary/util/__init__.py +3 -2
  176. notionary/util/fuzzy.py +2 -1
  177. notionary/util/logging_mixin.py +2 -2
  178. notionary/util/singleton_metaclass.py +1 -1
  179. notionary/workspace.py +6 -5
  180. notionary-0.2.22.dist-info/METADATA +237 -0
  181. notionary-0.2.22.dist-info/RECORD +200 -0
  182. notionary/blocks/document/__init__.py +0 -7
  183. notionary/blocks/document/document_element.py +0 -102
  184. notionary/blocks/document/document_markdown_node.py +0 -31
  185. notionary/blocks/image/__init__.py +0 -7
  186. notionary/blocks/image/image_element.py +0 -151
  187. notionary/blocks/markdown_builder.py +0 -356
  188. notionary/blocks/mention/__init__.py +0 -7
  189. notionary/blocks/mention/mention_element.py +0 -229
  190. notionary/blocks/mention/mention_markdown_node.py +0 -38
  191. notionary/blocks/prompts/element_prompt_builder.py +0 -83
  192. notionary/blocks/prompts/element_prompt_content.py +0 -41
  193. notionary/blocks/shared/models.py +0 -713
  194. notionary/blocks/shared/notion_block_element.py +0 -37
  195. notionary/blocks/shared/text_inline_formatter.py +0 -262
  196. notionary/blocks/shared/text_inline_formatter_new.py +0 -139
  197. notionary/database/models/page_result.py +0 -10
  198. notionary/models/notion_block_response.py +0 -264
  199. notionary/models/notion_page_response.py +0 -78
  200. notionary/models/search_response.py +0 -0
  201. notionary/page/__init__.py +0 -0
  202. notionary/page/content/markdown_whitespace_processor.py +0 -80
  203. notionary/page/content/notion_text_length_utils.py +0 -87
  204. notionary/page/content/page_content_retriever.py +0 -60
  205. notionary/page/formatting/line_processor.py +0 -153
  206. notionary/page/formatting/markdown_to_notion_converter.py +0 -153
  207. notionary/page/markdown_syntax_prompt_generator.py +0 -114
  208. notionary/page/notion_to_markdown_converter.py +0 -179
  209. notionary/page/properites/property_value_extractor.py +0 -0
  210. notionary/user/notion_user_provider.py +0 -1
  211. notionary-0.2.19.dist-info/METADATA +0 -225
  212. notionary-0.2.19.dist-info/RECORD +0 -150
  213. /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
  214. /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
  215. /notionary/{blocks/mention/mention_models.py → page/reader/handler/equation_renderer.py} +0 -0
  216. /notionary/{blocks/shared/__init__.py → page/writer/markdown_to_notion_post_processor.py} +0 -0
  217. /notionary/{blocks/toggleable_heading/toggleable_heading_models.py → page/writer/markdown_to_notion_text_length_post_processor.py} +0 -0
  218. /notionary/{elements/__init__.py → util/concurrency_limiter.py} +0 -0
  219. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
  220. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/WHEEL +0 -0
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from textwrap import dedent
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+
8
+ if TYPE_CHECKING:
9
+ from notionary.blocks.registry.block_registry import BlockRegistry
10
+
11
+
12
+ @dataclass
13
+ class BlockElementMarkdownInformation:
14
+ """Metadata describing how a Notion block maps to Markdown syntax."""
15
+
16
+ block_type: str
17
+ description: str
18
+ syntax_examples: list[str]
19
+ usage_guidelines: str
20
+
21
+
22
+ class SyntaxPromptBuilder:
23
+ """
24
+ Builds a comprehensive markdown syntax reference from a block registry.
25
+ Iterates over all registered elements and collects their system prompt information.
26
+ """
27
+
28
+ def __init__(self, block_registry: BlockRegistry):
29
+ self.block_registry = block_registry
30
+
31
+ def build_markdown_reference(self) -> str:
32
+ """
33
+ Build a complete markdown syntax reference string.
34
+ """
35
+ sections = [
36
+ self._build_header(),
37
+ *self._build_element_sections(),
38
+ ]
39
+
40
+ return "\n\n".join(sections)
41
+
42
+ def build_concise_reference(self) -> str:
43
+ """
44
+ Build a more concise reference suitable for system prompts.
45
+ """
46
+ lines = ["# Notionary Markdown Syntax"]
47
+
48
+ for element_class in self.block_registry.get_elements():
49
+ info: Optional[BlockElementMarkdownInformation] = (
50
+ element_class.get_system_prompt_information()
51
+ )
52
+ if info and info.syntax_examples:
53
+ # Just show the first example for conciseness
54
+ example = info.syntax_examples[0]
55
+ lines.append(f"- {info.block_type}: `{example}`")
56
+
57
+ return "\n".join(lines)
58
+
59
+ def get_blocks_with_information(self) -> list[str]:
60
+ """Get list of block names that provide system prompt information."""
61
+ blocks = []
62
+
63
+ for element_class in self.block_registry.get_elements():
64
+ info: Optional[BlockElementMarkdownInformation] = (
65
+ element_class.get_system_prompt_information()
66
+ )
67
+ if info:
68
+ blocks.append(info.block_type)
69
+
70
+ return blocks
71
+
72
+ def _build_header(self) -> str:
73
+ """Build the header section of the reference."""
74
+ return dedent(
75
+ """
76
+ # Notionary Markdown Syntax Reference
77
+
78
+ This comprehensive reference documents all supported markdown syntax for converting between Markdown and Notion blocks.
79
+
80
+ Each block type includes:
81
+ - **Description:** What the block does
82
+ - **When to use:** Guidelines for appropriate usage
83
+ - **Syntax:** Complete syntax examples with variations
84
+ """
85
+ ).strip()
86
+
87
+ def _build_element_sections(self) -> list[str]:
88
+ """Build sections for all registered elements."""
89
+ sections = []
90
+
91
+ for element_class in self.block_registry.get_elements():
92
+ info = element_class.get_system_prompt_information()
93
+ if info:
94
+ sections.append(self._build_element_section(info))
95
+
96
+ return sections
97
+
98
+ def _build_element_section(self, info: BlockElementMarkdownInformation) -> str:
99
+ """Build a well-structured section for a single block element."""
100
+ section_parts = [
101
+ f"## {info.block_type}",
102
+ "",
103
+ f"**Description:** {info.description}",
104
+ "",
105
+ ]
106
+
107
+ if info.usage_guidelines:
108
+ section_parts.extend(["**When to use:**", info.usage_guidelines, ""])
109
+
110
+ if info.syntax_examples:
111
+ section_parts.extend(
112
+ [
113
+ "**Syntax:**",
114
+ "",
115
+ *self._format_syntax_examples(info.syntax_examples),
116
+ "",
117
+ ]
118
+ )
119
+
120
+ return "\n".join(section_parts).rstrip()
121
+
122
+ def _format_syntax_examples(self, examples: list[str]) -> list[str]:
123
+ """Format syntax examples with proper markdown and clear structure."""
124
+ formatted = []
125
+
126
+ for i, example in enumerate(examples, 1):
127
+ if len(examples) > 1:
128
+ formatted.append(f"**Example {i}:**")
129
+
130
+ if "\n" in example:
131
+ # Multi-line example - use code block
132
+ formatted.extend(["```", example, "```", ""])
133
+ else:
134
+ # Single line - use inline code with description
135
+ formatted.extend([f"`{example}`", ""])
136
+
137
+ return formatted
@@ -1,7 +1,21 @@
1
- from .table_element import TableElement
2
- from .table_markdown_node import TableMarkdownNode
1
+ from notionary.blocks.table.table_element import TableElement
2
+ from notionary.blocks.table.table_markdown_node import (
3
+ TableMarkdownBlockParams,
4
+ TableMarkdownNode,
5
+ )
6
+ from notionary.blocks.table.table_models import (
7
+ CreateTableBlock,
8
+ CreateTableRowBlock,
9
+ TableBlock,
10
+ TableRowBlock,
11
+ )
3
12
 
4
13
  __all__ = [
5
14
  "TableElement",
15
+ "TableBlock",
16
+ "TableRowBlock",
17
+ "CreateTableRowBlock",
18
+ "CreateTableBlock",
6
19
  "TableMarkdownNode",
20
+ "TableMarkdownBlockParams",
7
21
  ]
@@ -1,106 +1,158 @@
1
- import re
2
- from typing import Dict, Any, Optional, List, Tuple
1
+ from __future__ import annotations
3
2
 
4
- from notionary.blocks import (
5
- NotionBlockElement,
6
- NotionBlockResult,
7
- ElementPromptContent,
8
- ElementPromptBuilder,
3
+ import re
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
8
+ from notionary.blocks.models import Block, BlockCreateResult
9
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
10
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
11
+ from notionary.blocks.table.table_models import (
12
+ CreateTableBlock,
13
+ TableBlock,
14
+ CreateTableRowBlock,
15
+ TableRowBlock,
9
16
  )
10
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
17
+ from notionary.blocks.types import BlockType
11
18
 
12
19
 
13
- class TableElement(NotionBlockElement):
20
+ class TableElement(BaseBlockElement):
14
21
  """
15
22
  Handles conversion between Markdown tables and Notion table blocks.
23
+ Now integrated into the LineProcessor stack system.
16
24
 
17
25
  Markdown table syntax:
18
26
  | Header 1 | Header 2 | Header 3 |
19
27
  | -------- | -------- | -------- |
20
28
  | Cell 1 | Cell 2 | Cell 3 |
21
- | Cell 4 | Cell 5 | Cell 6 |
22
-
23
- The second line with dashes and optional colons defines column alignment.
24
29
  """
25
30
 
26
31
  ROW_PATTERN = re.compile(r"^\s*\|(.+)\|\s*$")
27
32
  SEPARATOR_PATTERN = re.compile(r"^\s*\|([\s\-:|]+)\|\s*$")
28
33
 
29
34
  @classmethod
30
- def match_markdown(cls, text: str) -> bool:
31
- """
32
- Check if text contains a markdown table.
33
- Accepts tables with only header + separator, as well as header + separator + data rows.
34
- """
35
- lines = text.split("\n")
35
+ def match_notion(cls, block: Block) -> bool:
36
+ """Check if block is a Notion table."""
37
+ return block.type == BlockType.TABLE and block.table
36
38
 
37
- if len(lines) < 2:
38
- return False
39
+ @classmethod
40
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
41
+ """Convert opening table row to Notion table block."""
42
+ if not cls.ROW_PATTERN.match(text.strip()):
43
+ return None
39
44
 
40
- # Akzeptiere Header + Separator auch ohne Datenzeile
41
- for i, line in enumerate(lines[:-1]):
42
- if (
43
- cls.ROW_PATTERN.match(line)
44
- and cls.SEPARATOR_PATTERN.match(lines[i + 1])
45
- ):
46
- return True
45
+ # Parse the header row to determine column count
46
+ header_cells = cls._parse_table_row(text)
47
+ col_count = len(header_cells)
47
48
 
48
- return False
49
+ table_block = TableBlock(
50
+ table_width=col_count,
51
+ has_column_header=True,
52
+ has_row_header=False,
53
+ children=[], # Will be populated by stack processor
54
+ )
49
55
 
50
- @classmethod
51
- def match_notion(cls, block: Dict[str, Any]) -> bool:
52
- """Check if block is a Notion table."""
53
- return block.get("type") == "table"
56
+ return CreateTableBlock(table=table_block)
54
57
 
55
58
  @classmethod
56
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
57
- """Convert markdown table to Notion table block."""
58
- if not TableElement.match_markdown(text):
59
+ async def create_from_markdown_table(
60
+ cls, table_lines: list[str]
61
+ ) -> BlockCreateResult:
62
+ """
63
+ Create a complete table block from markdown table lines.
64
+ """
65
+ if not table_lines:
59
66
  return None
60
67
 
61
- lines = text.split("\n")
68
+ first_row = None
69
+ for line in table_lines:
70
+ line = line.strip()
71
+ if line and cls.ROW_PATTERN.match(line):
72
+ first_row = line
73
+ break
62
74
 
63
- table_start = TableElement._find_table_start(lines)
64
- if table_start is None:
75
+ if not first_row:
65
76
  return None
66
77
 
67
- table_end = TableElement._find_table_end(lines, table_start)
68
- table_lines = lines[table_start:table_end]
78
+ # Parse header row to determine column count
79
+ header_cells = cls._parse_table_row(first_row)
80
+ col_count = len(header_cells)
69
81
 
70
- rows = TableElement._extract_table_rows(table_lines)
71
- if not rows:
72
- return None
82
+ # Process all table lines
83
+ table_rows, separator_found = await cls._process_table_lines(table_lines)
73
84
 
74
- column_count = len(rows[0])
75
- TableElement._normalize_row_lengths(rows, column_count)
85
+ # Create complete TableBlock
86
+ table_block = TableBlock(
87
+ table_width=col_count,
88
+ has_column_header=separator_found,
89
+ has_row_header=False,
90
+ children=table_rows,
91
+ )
76
92
 
77
- table_block = {
78
- "type": "table",
79
- "table": {
80
- "table_width": column_count,
81
- "has_column_header": True,
82
- "has_row_header": False,
83
- "children": TableElement._create_table_rows(rows),
84
- },
85
- }
93
+ return CreateTableBlock(table=table_block)
86
94
 
87
- # Leerer Paragraph nach der Tabelle
88
- empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
95
+ @classmethod
96
+ async def _process_table_lines(
97
+ cls, table_lines: list[str]
98
+ ) -> tuple[list[CreateTableRowBlock], bool]:
99
+ """Process all table lines and return rows and separator status."""
100
+ table_rows = []
101
+ separator_found = False
89
102
 
90
- return [table_block, empty_paragraph]
103
+ for line in table_lines:
104
+ line = line.strip()
105
+ if not line:
106
+ continue
107
+
108
+ if cls._is_separator_line(line):
109
+ separator_found = True
110
+ continue
111
+
112
+ if cls.ROW_PATTERN.match(line):
113
+ table_row = await cls._create_table_row_from_line(line)
114
+ table_rows.append(table_row)
115
+
116
+ return table_rows, separator_found
117
+
118
+ @classmethod
119
+ def _is_separator_line(cls, line: str) -> bool:
120
+ """Check if line is a table separator (|---|---|)."""
121
+ return cls.SEPARATOR_PATTERN.match(line) is not None
122
+
123
+ @classmethod
124
+ async def _create_table_row_from_line(cls, line: str) -> CreateTableRowBlock:
125
+ """Create a table row block from a markdown line."""
126
+ cells = cls._parse_table_row(line)
127
+ rich_text_cells = []
128
+ for cell in cells:
129
+ rich_text_cell = await cls._convert_cell_to_rich_text(cell)
130
+ rich_text_cells.append(rich_text_cell)
131
+ table_row = TableRowBlock(cells=rich_text_cells)
132
+ return CreateTableRowBlock(table_row=table_row)
133
+
134
+ @classmethod
135
+ async def _convert_cell_to_rich_text(cls, cell: str) -> list[RichTextObject]:
136
+ """Convert cell text to rich text objects."""
137
+ rich_text = await TextInlineFormatter.parse_inline_formatting(cell)
138
+ if not rich_text:
139
+ rich_text = [RichTextObject.from_plain_text(cell)]
140
+ return rich_text
91
141
 
92
142
  @classmethod
93
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
143
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
94
144
  """Convert Notion table block to markdown table."""
95
- if block.get("type") != "table":
145
+ if block.type != BlockType.TABLE:
96
146
  return None
97
147
 
98
- table_data = block.get("table", {})
99
- children = block.get("children", [])
148
+ if not block.table:
149
+ return None
100
150
 
101
- if not children:
102
- table_width = table_data.get("table_width", 3)
151
+ table_data = block.table
152
+ children = block.children or []
103
153
 
154
+ if not children:
155
+ table_width = table_data.table_width or 3
104
156
  header = (
105
157
  "| " + " | ".join([f"Column {i+1}" for i in range(table_width)]) + " |"
106
158
  )
@@ -110,7 +162,6 @@ class TableElement(NotionBlockElement):
110
162
  data_row = (
111
163
  "| " + " | ".join([" " for _ in range(table_width)]) + " |"
112
164
  )
113
-
114
165
  table_rows = [header, separator, data_row]
115
166
  return "\n".join(table_rows)
116
167
 
@@ -118,86 +169,34 @@ class TableElement(NotionBlockElement):
118
169
  header_processed = False
119
170
 
120
171
  for child in children:
121
- if child.get("type") != "table_row":
172
+ if child.type != BlockType.TABLE_ROW:
122
173
  continue
123
174
 
124
- row_data = child.get("table_row", {})
125
- cells = row_data.get("cells", [])
175
+ if not child.table_row:
176
+ continue
177
+
178
+ row_data = child.table_row
179
+ cells = row_data.cells or []
126
180
 
127
181
  row_cells = []
128
182
  for cell in cells:
129
- cell_text = TextInlineFormatter.extract_text_with_formatting(cell)
183
+ cell_text = await TextInlineFormatter.extract_text_with_formatting(cell)
130
184
  row_cells.append(cell_text or "")
131
185
 
132
186
  row = "| " + " | ".join(row_cells) + " |"
133
187
  table_rows.append(row)
134
188
 
135
- if not header_processed and table_data.get("has_column_header", True):
189
+ if not header_processed and table_data.has_column_header:
136
190
  header_processed = True
137
191
  separator = (
138
192
  "| " + " | ".join(["--------" for _ in range(len(cells))]) + " |"
139
193
  )
140
194
  table_rows.append(separator)
141
195
 
142
- if not table_rows:
143
- return None
144
-
145
- if len(table_rows) == 1 and table_data.get("has_column_header", True):
146
- cells_count = len(children[0].get("table_row", {}).get("cells", []))
147
- separator = (
148
- "| " + " | ".join(["--------" for _ in range(cells_count)]) + " |"
149
- )
150
- table_rows.insert(1, separator)
151
-
152
196
  return "\n".join(table_rows)
153
197
 
154
198
  @classmethod
155
- def is_multiline(cls) -> bool:
156
- """Indicates if this element handles content that spans multiple lines."""
157
- return True
158
-
159
- @classmethod
160
- def _find_table_start(cls, lines: List[str]) -> Optional[int]:
161
- """Find the start index of a table in the lines."""
162
- for i in range(len(lines) - 2):
163
- if (
164
- TableElement.ROW_PATTERN.match(lines[i])
165
- and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
166
- and TableElement.ROW_PATTERN.match(lines[i + 2])
167
- ):
168
- return i
169
- return None
170
-
171
- @classmethod
172
- def _find_table_end(cls, lines: List[str], start_idx: int) -> int:
173
- """Find the end index of a table, starting from start_idx."""
174
- end_idx = start_idx + 3 # Minimum: Header, Separator, one data row
175
- while end_idx < len(lines) and TableElement.ROW_PATTERN.match(lines[end_idx]):
176
- end_idx += 1
177
- return end_idx
178
-
179
- @classmethod
180
- def _extract_table_rows(cls, table_lines: List[str]) -> List[List[str]]:
181
- """Extract row contents from table lines, excluding separator line."""
182
- rows = []
183
- for i, line in enumerate(table_lines):
184
- if i != 1 and TableElement.ROW_PATTERN.match(line): # Skip separator line
185
- cells = TableElement._parse_table_row(line)
186
- if cells:
187
- rows.append(cells)
188
- return rows
189
-
190
- @classmethod
191
- def _normalize_row_lengths(cls, rows: List[List[str]], column_count: int) -> None:
192
- """Normalize row lengths to the specified column count."""
193
- for row in rows:
194
- if len(row) < column_count:
195
- row.extend([""] * (column_count - len(row)))
196
- elif len(row) > column_count:
197
- del row[column_count:]
198
-
199
- @classmethod
200
- def _parse_table_row(cls, row_text: str) -> List[str]:
199
+ def _parse_table_row(cls, row_text: str) -> list[str]:
201
200
  """Convert table row text to cell contents."""
202
201
  row_content = row_text.strip()
203
202
 
@@ -209,109 +208,18 @@ class TableElement(NotionBlockElement):
209
208
  return [cell.strip() for cell in row_content.split("|")]
210
209
 
211
210
  @classmethod
212
- def _create_table_rows(cls, rows: List[List[str]]) -> List[Dict[str, Any]]:
213
- """Create Notion table rows from cell contents."""
214
- table_rows = []
215
-
216
- for row in rows:
217
- cells_data = []
218
-
219
- for cell_content in row:
220
- rich_text = TextInlineFormatter.parse_inline_formatting(cell_content)
221
-
222
- if not rich_text:
223
- rich_text = [
224
- {
225
- "type": "text",
226
- "text": {"content": ""},
227
- "annotations": {
228
- "bold": False,
229
- "italic": False,
230
- "strikethrough": False,
231
- "underline": False,
232
- "code": False,
233
- "color": "default",
234
- },
235
- "plain_text": "",
236
- "href": None,
237
- }
238
- ]
239
-
240
- cells_data.append(rich_text)
241
-
242
- table_rows.append({"type": "table_row", "table_row": {"cells": cells_data}})
243
-
244
- return table_rows
245
-
246
- @classmethod
247
- def find_matches(cls, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
248
- """
249
- Find all tables in the text and return their positions.
250
-
251
- Args:
252
- text: The text to search in
253
-
254
- Returns:
255
- List of tuples with (start_pos, end_pos, block)
256
- """
257
- matches = []
258
- lines = text.split("\n")
259
-
260
- i = 0
261
- while i < len(lines) - 2:
262
- if (
263
- TableElement.ROW_PATTERN.match(lines[i])
264
- and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
265
- and TableElement.ROW_PATTERN.match(lines[i + 2])
266
- ):
267
-
268
- start_line = i
269
- end_line = TableElement._find_table_end(lines, start_line)
270
-
271
- start_pos = TableElement._calculate_position(lines, 0, start_line)
272
- end_pos = start_pos + TableElement._calculate_position(
273
- lines, start_line, end_line
274
- )
275
-
276
- table_text = "\n".join(lines[start_line:end_line])
277
- table_block = TableElement.markdown_to_notion(table_text)
278
-
279
- if table_block:
280
- matches.append((start_pos, end_pos, table_block))
281
-
282
- i = end_line
283
- else:
284
- i += 1
285
-
286
- return matches
287
-
288
- @classmethod
289
- def _calculate_position(cls, lines: List[str], start: int, end: int) -> int:
290
- """Calculate the text position in characters from line start to end."""
291
- position = 0
292
- for i in range(start, end):
293
- position += len(lines[i]) + 1 # +1 for newline
294
- return position
211
+ def is_table_row(cls, line: str) -> bool:
212
+ """Check if a line is a valid table row."""
213
+ return bool(cls.ROW_PATTERN.match(line.strip()))
295
214
 
296
215
  @classmethod
297
- def get_llm_prompt_content(cls) -> ElementPromptContent:
298
- """Returns information for LLM prompts about this element."""
299
- return (
300
- ElementPromptBuilder()
301
- .with_description(
302
- "Creates formatted tables with rows and columns for structured data."
303
- )
304
- .with_usage_guidelines(
305
- "Use tables to organize and present structured data in a grid format, making information easier to compare and analyze. Tables are ideal for data sets, comparison charts, pricing information, or any content that benefits from columnar organization."
306
- )
307
- .with_syntax(
308
- "| Header 1 | Header 2 | Header 3 |\n| -------- | -------- | -------- |\n| Cell 1 | Cell 2 | Cell 3 |"
309
- )
310
- .with_examples(
311
- [
312
- "| Product | Price | Stock |\n| ------- | ----- | ----- |\n| Widget A | $10.99 | 42 |\n| Widget B | $14.99 | 27 |",
313
- "| Name | Role | Department |\n| ---- | ---- | ---------- |\n| John Smith | Manager | Marketing |\n| Jane Doe | Director | Sales |",
314
- ]
315
- )
316
- .build()
216
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
217
+ """Get system prompt information for table blocks."""
218
+ return BlockElementMarkdownInformation(
219
+ block_type=cls.__name__,
220
+ description="Table blocks create structured data in rows and columns with headers",
221
+ syntax_examples=[
222
+ "| Name | Age | City |\n| -------- | -------- | -------- |\n| Alice | 25 | Berlin |\n| Bob | 30 | Munich |"
223
+ ],
224
+ usage_guidelines="Use for structured data presentation. First row is header, second row is separator with dashes, following rows are data. Cells are separated by | characters.",
317
225
  )
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pydantic import BaseModel
4
- from notionary.blocks.markdown_node import MarkdownNode
4
+
5
+ from notionary.markdown.markdown_node import MarkdownNode
5
6
 
6
7
 
7
8
  class TableMarkdownBlockParams(BaseModel):
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
8
+
9
+
10
+ class TableBlock(BaseModel):
11
+ table_width: int
12
+ has_column_header: bool = False
13
+ has_row_header: bool = False
14
+ children: list[CreateTableRowBlock] = []
15
+
16
+
17
+ class TableRowBlock(BaseModel):
18
+ cells: list[list[RichTextObject]]
19
+
20
+
21
+ class CreateTableRowBlock(BaseModel):
22
+ type: Literal["table_row"] = "table_row"
23
+ table_row: TableRowBlock
24
+
25
+
26
+ class CreateTableBlock(BaseModel):
27
+ type: Literal["table"] = "table"
28
+ table: TableBlock
@@ -0,0 +1,19 @@
1
+ from notionary.blocks.table_of_contents.table_of_contents_element import (
2
+ TableOfContentsElement,
3
+ )
4
+ from notionary.blocks.table_of_contents.table_of_contents_markdown_node import (
5
+ TableOfContentsMarkdownBlockParams,
6
+ TableOfContentsMarkdownNode,
7
+ )
8
+ from notionary.blocks.table_of_contents.table_of_contents_models import (
9
+ CreateTableOfContentsBlock,
10
+ TableOfContentsBlock,
11
+ )
12
+
13
+ __all__ = [
14
+ "TableOfContentsElement",
15
+ "TableOfContentsBlock",
16
+ "CreateTableOfContentsBlock",
17
+ "TableOfContentsMarkdownNode",
18
+ "TableOfContentsMarkdownBlockParams",
19
+ ]