notionary 0.2.21__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 (96) hide show
  1. notionary/blocks/_bootstrap.py +9 -1
  2. notionary/blocks/audio/audio_element.py +53 -28
  3. notionary/blocks/audio/audio_markdown_node.py +10 -4
  4. notionary/blocks/base_block_element.py +15 -3
  5. notionary/blocks/bookmark/bookmark_element.py +39 -36
  6. notionary/blocks/bookmark/bookmark_markdown_node.py +16 -17
  7. notionary/blocks/breadcrumbs/breadcrumb_element.py +2 -2
  8. notionary/blocks/bulleted_list/bulleted_list_element.py +21 -4
  9. notionary/blocks/callout/callout_element.py +20 -4
  10. notionary/blocks/child_database/__init__.py +11 -4
  11. notionary/blocks/child_database/child_database_element.py +61 -0
  12. notionary/blocks/child_database/child_database_models.py +7 -14
  13. notionary/blocks/child_page/child_page_element.py +94 -0
  14. notionary/blocks/client.py +0 -1
  15. notionary/blocks/code/code_element.py +51 -2
  16. notionary/blocks/code/code_markdown_node.py +52 -1
  17. notionary/blocks/column/column_element.py +9 -3
  18. notionary/blocks/column/column_list_element.py +18 -3
  19. notionary/blocks/divider/divider_element.py +3 -11
  20. notionary/blocks/embed/embed_element.py +27 -6
  21. notionary/blocks/equation/equation_element.py +94 -41
  22. notionary/blocks/equation/equation_element_markdown_node.py +8 -9
  23. notionary/blocks/file/file_element.py +56 -37
  24. notionary/blocks/file/file_element_markdown_node.py +9 -7
  25. notionary/blocks/guards.py +22 -0
  26. notionary/blocks/heading/heading_element.py +23 -4
  27. notionary/blocks/image_block/image_element.py +43 -38
  28. notionary/blocks/image_block/image_markdown_node.py +10 -5
  29. notionary/blocks/mixins/captions/__init__.py +4 -0
  30. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  31. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  32. notionary/blocks/models.py +3 -1
  33. notionary/blocks/numbered_list/numbered_list_element.py +21 -4
  34. notionary/blocks/paragraph/paragraph_element.py +21 -5
  35. notionary/blocks/pdf/pdf_element.py +47 -41
  36. notionary/blocks/pdf/pdf_markdown_node.py +9 -7
  37. notionary/blocks/quote/quote_element.py +26 -9
  38. notionary/blocks/quote/quote_markdown_node.py +2 -2
  39. notionary/blocks/registry/block_registry.py +1 -46
  40. notionary/blocks/registry/block_registry_builder.py +8 -0
  41. notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
  42. notionary/blocks/rich_text/rich_text_models.py +62 -29
  43. notionary/blocks/rich_text/text_inline_formatter.py +432 -101
  44. notionary/blocks/syntax_prompt_builder.py +137 -0
  45. notionary/blocks/table/table_element.py +110 -9
  46. notionary/blocks/table_of_contents/table_of_contents_element.py +19 -2
  47. notionary/blocks/todo/todo_element.py +21 -4
  48. notionary/blocks/toggle/toggle_element.py +19 -3
  49. notionary/blocks/toggle/toggle_markdown_node.py +1 -1
  50. notionary/blocks/toggleable_heading/toggleable_heading_element.py +19 -4
  51. notionary/blocks/types.py +69 -0
  52. notionary/blocks/video/video_element.py +44 -39
  53. notionary/blocks/video/video_markdown_node.py +10 -5
  54. notionary/database/client.py +23 -0
  55. notionary/file_upload/models.py +2 -2
  56. notionary/markdown/markdown_builder.py +34 -27
  57. notionary/page/client.py +26 -6
  58. notionary/page/notion_page.py +37 -6
  59. notionary/page/page_content_deleting_service.py +117 -0
  60. notionary/page/page_content_writer.py +89 -113
  61. notionary/page/page_context.py +65 -0
  62. notionary/page/reader/handler/__init__.py +2 -0
  63. notionary/page/reader/handler/base_block_renderer.py +4 -4
  64. notionary/page/reader/handler/block_rendering_context.py +5 -0
  65. notionary/page/reader/handler/line_renderer.py +16 -3
  66. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  67. notionary/page/reader/page_content_retriever.py +17 -5
  68. notionary/page/writer/handler/__init__.py +2 -0
  69. notionary/page/writer/handler/code_handler.py +12 -40
  70. notionary/page/writer/handler/column_handler.py +12 -12
  71. notionary/page/writer/handler/column_list_handler.py +13 -13
  72. notionary/page/writer/handler/equation_handler.py +74 -0
  73. notionary/page/writer/handler/line_handler.py +4 -4
  74. notionary/page/writer/handler/regular_line_handler.py +31 -37
  75. notionary/page/writer/handler/table_handler.py +8 -72
  76. notionary/page/writer/handler/toggle_handler.py +14 -12
  77. notionary/page/writer/handler/toggleable_heading_handler.py +22 -16
  78. notionary/page/writer/markdown_to_notion_converter.py +28 -9
  79. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  80. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  81. notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
  82. notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
  83. notionary/page/writer/notion_text_length_processor.py +150 -0
  84. notionary/telemetry/service.py +0 -1
  85. notionary/user/notion_user_manager.py +22 -95
  86. notionary/util/concurrency_limiter.py +0 -0
  87. notionary/workspace.py +4 -4
  88. notionary-0.2.22.dist-info/METADATA +237 -0
  89. {notionary-0.2.21.dist-info → notionary-0.2.22.dist-info}/RECORD +92 -77
  90. notionary/page/markdown_whitespace_processor.py +0 -80
  91. notionary/page/notion_text_length_utils.py +0 -119
  92. notionary/user/notion_user_provider.py +0 -1
  93. notionary-0.2.21.dist-info/METADATA +0 -229
  94. /notionary/page/reader/handler/{context.py → equation_renderer.py} +0 -0
  95. {notionary-0.2.21.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
  96. {notionary-0.2.21.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
@@ -4,9 +4,16 @@ import re
4
4
  from typing import Optional
5
5
 
6
6
  from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
7
8
  from notionary.blocks.models import Block, BlockCreateResult
9
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
8
10
  from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
9
- from notionary.blocks.table.table_models import CreateTableBlock, TableBlock
11
+ from notionary.blocks.table.table_models import (
12
+ CreateTableBlock,
13
+ TableBlock,
14
+ CreateTableRowBlock,
15
+ TableRowBlock,
16
+ )
10
17
  from notionary.blocks.types import BlockType
11
18
 
12
19
 
@@ -17,12 +24,11 @@ class TableElement(BaseBlockElement):
17
24
 
18
25
  Markdown table syntax:
19
26
  | Header 1 | Header 2 | Header 3 |
20
- [table rows as child lines]
27
+ | -------- | -------- | -------- |
28
+ | Cell 1 | Cell 2 | Cell 3 |
21
29
  """
22
30
 
23
- # Pattern für Table-Zeilen (jede Zeile die mit | startet und endet)
24
31
  ROW_PATTERN = re.compile(r"^\s*\|(.+)\|\s*$")
25
- # Pattern für Separator-Zeilen
26
32
  SEPARATOR_PATTERN = re.compile(r"^\s*\|([\s\-:|]+)\|\s*$")
27
33
 
28
34
  @classmethod
@@ -31,7 +37,7 @@ class TableElement(BaseBlockElement):
31
37
  return block.type == BlockType.TABLE and block.table
32
38
 
33
39
  @classmethod
34
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
40
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
35
41
  """Convert opening table row to Notion table block."""
36
42
  if not cls.ROW_PATTERN.match(text.strip()):
37
43
  return None
@@ -40,7 +46,6 @@ class TableElement(BaseBlockElement):
40
46
  header_cells = cls._parse_table_row(text)
41
47
  col_count = len(header_cells)
42
48
 
43
- # Create empty TableBlock - content will be added by stack processor
44
49
  table_block = TableBlock(
45
50
  table_width=col_count,
46
51
  has_column_header=True,
@@ -51,7 +56,91 @@ class TableElement(BaseBlockElement):
51
56
  return CreateTableBlock(table=table_block)
52
57
 
53
58
  @classmethod
54
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
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:
66
+ return None
67
+
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
74
+
75
+ if not first_row:
76
+ return None
77
+
78
+ # Parse header row to determine column count
79
+ header_cells = cls._parse_table_row(first_row)
80
+ col_count = len(header_cells)
81
+
82
+ # Process all table lines
83
+ table_rows, separator_found = await cls._process_table_lines(table_lines)
84
+
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
+ )
92
+
93
+ return CreateTableBlock(table=table_block)
94
+
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
102
+
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
141
+
142
+ @classmethod
143
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
55
144
  """Convert Notion table block to markdown table."""
56
145
  if block.type != BlockType.TABLE:
57
146
  return None
@@ -80,7 +169,7 @@ class TableElement(BaseBlockElement):
80
169
  header_processed = False
81
170
 
82
171
  for child in children:
83
- if child.type != "table_row":
172
+ if child.type != BlockType.TABLE_ROW:
84
173
  continue
85
174
 
86
175
  if not child.table_row:
@@ -91,7 +180,7 @@ class TableElement(BaseBlockElement):
91
180
 
92
181
  row_cells = []
93
182
  for cell in cells:
94
- cell_text = TextInlineFormatter.extract_text_with_formatting(cell)
183
+ cell_text = await TextInlineFormatter.extract_text_with_formatting(cell)
95
184
  row_cells.append(cell_text or "")
96
185
 
97
186
  row = "| " + " | ".join(row_cells) + " |"
@@ -122,3 +211,15 @@ class TableElement(BaseBlockElement):
122
211
  def is_table_row(cls, line: str) -> bool:
123
212
  """Check if a line is a valid table row."""
124
213
  return bool(cls.ROW_PATTERN.match(line.strip()))
214
+
215
+ @classmethod
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.",
225
+ )
@@ -4,6 +4,7 @@ import re
4
4
  from typing import Optional
5
5
 
6
6
  from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
7
8
  from notionary.blocks.models import Block, BlockCreateResult
8
9
  from notionary.blocks.table_of_contents.table_of_contents_models import (
9
10
  CreateTableOfContentsBlock,
@@ -29,7 +30,7 @@ class TableOfContentsElement(BaseBlockElement):
29
30
  return block.type == BlockType.TABLE_OF_CONTENTS and block.table_of_contents
30
31
 
31
32
  @classmethod
32
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
33
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
33
34
  if not (input_match := cls.PATTERN.match(text.strip())):
34
35
  return None
35
36
 
@@ -39,7 +40,7 @@ class TableOfContentsElement(BaseBlockElement):
39
40
  )
40
41
 
41
42
  @classmethod
42
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
43
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
43
44
  # Fix: Use 'or' instead of 'and'
44
45
  if block.type != BlockType.TABLE_OF_CONTENTS or not block.table_of_contents:
45
46
  return None
@@ -49,3 +50,19 @@ class TableOfContentsElement(BaseBlockElement):
49
50
  if color == "default":
50
51
  return "[toc]"
51
52
  return f"[toc]({color})"
53
+
54
+ @classmethod
55
+ @classmethod
56
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
57
+ """Get system prompt information for table of contents blocks."""
58
+ return BlockElementMarkdownInformation(
59
+ block_type=cls.__name__,
60
+ description="Table of contents blocks automatically generate navigation for page headings",
61
+ syntax_examples=[
62
+ "[toc]",
63
+ "[toc](blue)",
64
+ "[toc](blue_background)",
65
+ "[toc](gray_background)",
66
+ ],
67
+ usage_guidelines="Use to automatically generate a clickable table of contents from page headings. Optional color parameter changes the appearance. Default color is gray.",
68
+ )
@@ -4,6 +4,7 @@ import re
4
4
  from typing import TYPE_CHECKING, Optional
5
5
 
6
6
  from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
7
8
  from notionary.blocks.models import Block, BlockCreateResult, BlockType
8
9
  from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
9
10
  from notionary.blocks.todo.todo_models import CreateToDoBlock, ToDoBlock
@@ -28,7 +29,7 @@ class TodoElement(BaseBlockElement):
28
29
  return block.type == BlockType.TO_DO and block.to_do
29
30
 
30
31
  @classmethod
31
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
32
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
32
33
  """Convert markdown todo or done item to Notion to_do block."""
33
34
  m_done = cls.DONE_PATTERN.match(text)
34
35
  m_todo = None if m_done else cls.PATTERN.match(text)
@@ -43,7 +44,7 @@ class TodoElement(BaseBlockElement):
43
44
  return None
44
45
 
45
46
  # build rich text
46
- rich = TextInlineFormatter.parse_inline_formatting(content)
47
+ rich = await TextInlineFormatter.parse_inline_formatting(content)
47
48
 
48
49
  todo_content = ToDoBlock(
49
50
  rich_text=rich,
@@ -53,12 +54,28 @@ class TodoElement(BaseBlockElement):
53
54
  return CreateToDoBlock(to_do=todo_content)
54
55
 
55
56
  @classmethod
56
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
57
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
57
58
  """Convert Notion to_do block to markdown todo item."""
58
59
  if block.type != BlockType.TO_DO or not block.to_do:
59
60
  return None
60
61
 
61
62
  td = block.to_do
62
- content = TextInlineFormatter.extract_text_with_formatting(td.rich_text)
63
+ content = await TextInlineFormatter.extract_text_with_formatting(td.rich_text)
63
64
  checkbox = "[x]" if td.checked else "[ ]"
64
65
  return f"- {checkbox} {content}"
66
+
67
+ @classmethod
68
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
69
+ """Get system prompt information for todo blocks."""
70
+ return BlockElementMarkdownInformation(
71
+ block_type=cls.__name__,
72
+ description="Todo blocks create interactive checkboxes for task management",
73
+ syntax_examples=[
74
+ "- [ ] Unchecked todo item",
75
+ "- [x] Checked todo item",
76
+ "* [ ] Todo with asterisk",
77
+ "+ [ ] Todo with plus sign",
78
+ "- [x] Completed task",
79
+ ],
80
+ usage_guidelines="Use for task lists and checkboxes. [ ] for unchecked, [x] for checked items. Supports -, *, or + as bullet markers. Interactive in Notion interface.",
81
+ )
@@ -4,6 +4,7 @@ import re
4
4
  from typing import Optional
5
5
 
6
6
  from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
7
8
  from notionary.blocks.models import Block, BlockCreateResult, BlockType
8
9
  from notionary.blocks.rich_text.rich_text_models import RichTextObject
9
10
  from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
@@ -27,7 +28,7 @@ class ToggleElement(BaseBlockElement):
27
28
  return block.type == BlockType.TOGGLE
28
29
 
29
30
  @classmethod
30
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
32
  """
32
33
  Convert markdown toggle line to Notion ToggleBlock.
33
34
  Children are automatically handled by the StackBasedMarkdownConverter.
@@ -36,7 +37,7 @@ class ToggleElement(BaseBlockElement):
36
37
  return None
37
38
 
38
39
  title = match.group(1).strip()
39
- rich_text = TextInlineFormatter.parse_inline_formatting(title)
40
+ rich_text = await TextInlineFormatter.parse_inline_formatting(title)
40
41
 
41
42
  # Create toggle block with empty children - they will be populated automatically
42
43
  toggle_content = ToggleBlock(
@@ -46,7 +47,7 @@ class ToggleElement(BaseBlockElement):
46
47
  return CreateToggleBlock(toggle=toggle_content)
47
48
 
48
49
  @classmethod
49
- def notion_to_markdown(cls, block: Block) -> Optional[str]:
50
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
50
51
  """
51
52
  Converts a Notion toggle block into markdown using the ultra-simplified +++ syntax.
52
53
  """
@@ -94,3 +95,18 @@ class ToggleElement(BaseBlockElement):
94
95
  elif "plain_text" in text_obj:
95
96
  result += text_obj.get("plain_text", "")
96
97
  return result
98
+
99
+ @classmethod
100
+ @classmethod
101
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
102
+ """Get system prompt information for toggle blocks."""
103
+ return BlockElementMarkdownInformation(
104
+ block_type=cls.__name__,
105
+ description="Toggle blocks create collapsible sections with expandable content",
106
+ syntax_examples=[
107
+ "+++Title\nContent goes here\n+++",
108
+ "+++Details\nMore information\nAdditional content\n+++",
109
+ "+++FAQ\nFrequently asked questions\n+++",
110
+ ],
111
+ usage_guidelines="Use for collapsible content sections. Start with +++Title, add content, end with +++. Great for FAQs, details, or organizing long content.",
112
+ )
@@ -32,7 +32,7 @@ class ToggleMarkdownNode(MarkdownNode):
32
32
  return cls(title=params.title, children=params.children)
33
33
 
34
34
  def to_markdown(self) -> str:
35
- result = f"+++ {self.title}"
35
+ result = f"+++{self.title}"
36
36
 
37
37
  if not self.children:
38
38
  result += "\n+++"
@@ -10,6 +10,7 @@ from notionary.blocks.heading.heading_models import (
10
10
  CreateHeading3Block,
11
11
  HeadingBlock,
12
12
  )
13
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
13
14
  from notionary.blocks.models import Block, BlockCreateResult, BlockType
14
15
  from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
15
16
 
@@ -42,7 +43,7 @@ class ToggleableHeadingElement(BaseBlockElement):
42
43
  return True
43
44
 
44
45
  @classmethod
45
- def markdown_to_notion(cls, text: str) -> BlockCreateResult:
46
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
46
47
  """
47
48
  Convert markdown collapsible heading to a toggleable Notion HeadingBlock.
48
49
  Children are automatically handled by the StackBasedMarkdownConverter.
@@ -56,7 +57,7 @@ class ToggleableHeadingElement(BaseBlockElement):
56
57
  if level < 1 or level > 3 or not content:
57
58
  return None
58
59
 
59
- rich_text = TextInlineFormatter.parse_inline_formatting(content)
60
+ rich_text = await TextInlineFormatter.parse_inline_formatting(content)
60
61
 
61
62
  heading_content = HeadingBlock(
62
63
  rich_text=rich_text, color="default", is_toggleable=True, children=[]
@@ -70,7 +71,7 @@ class ToggleableHeadingElement(BaseBlockElement):
70
71
  return CreateHeading3Block(heading_3=heading_content)
71
72
 
72
73
  @staticmethod
73
- def notion_to_markdown(block: Block) -> Optional[str]:
74
+ async def notion_to_markdown(block: Block) -> Optional[str]:
74
75
  """Convert Notion toggleable heading block to markdown collapsible heading."""
75
76
  # Only handle heading blocks via BlockType enum
76
77
  if block.type not in (
@@ -92,9 +93,23 @@ class ToggleableHeadingElement(BaseBlockElement):
92
93
  if not isinstance(heading_content, HeadingBlock):
93
94
  return None
94
95
 
95
- text = TextInlineFormatter.extract_text_with_formatting(
96
+ text = await TextInlineFormatter.extract_text_with_formatting(
96
97
  heading_content.rich_text
97
98
  )
98
99
  prefix = "#" * level
99
100
 
100
101
  return f'+++{prefix} {text or ""}'
102
+
103
+ @classmethod
104
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
105
+ """Get system prompt information for toggleable heading blocks."""
106
+ return BlockElementMarkdownInformation(
107
+ block_type=cls.__name__,
108
+ description="Toggleable heading blocks create collapsible sections with heading-style titles",
109
+ syntax_examples=[
110
+ "+++# Main Section\nContent goes here\n+++",
111
+ "+++## Subsection\nSubsection content\n+++",
112
+ "+++### Details\nDetailed information\n+++",
113
+ ],
114
+ usage_guidelines="Use for collapsible sections with heading structure. Combines heading levels (1-3) with toggle functionality. Great for organizing hierarchical expandable content.",
115
+ )
notionary/blocks/types.py CHANGED
@@ -2,6 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  from enum import Enum
4
4
 
5
+ from typing import Protocol, TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from notionary.blocks.models import BlockCreateRequest
9
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
10
+
5
11
 
6
12
  class BlockColor(str, Enum):
7
13
  BLUE = "blue"
@@ -59,3 +65,66 @@ class BlockType(str, Enum):
59
65
  UNSUPPORTED = "unsupported"
60
66
  VIDEO = "video"
61
67
  AUDIO = "audio"
68
+
69
+
70
+ class MarkdownBlockType(str, Enum):
71
+ """
72
+ Extended block types for the MarkdownBuilder.
73
+ Includes all BlockType values and adds user-friendly aliases
74
+ for blocks with no direct Notion API counterpart.
75
+ """
76
+
77
+ # All BlockType values
78
+ BOOKMARK = "bookmark"
79
+ BREADCRUMB = "breadcrumb"
80
+ BULLETED_LIST_ITEM = "bulleted_list_item"
81
+ CALLOUT = "callout"
82
+ CHILD_DATABASE = "child_database"
83
+ CHILD_PAGE = "child_page"
84
+ COLUMN = "column"
85
+ COLUMN_LIST = "column_list"
86
+ CODE = "code"
87
+ DIVIDER = "divider"
88
+ EMBED = "embed"
89
+ EQUATION = "equation"
90
+ FILE = "file"
91
+ HEADING_1 = "heading_1"
92
+ HEADING_2 = "heading_2"
93
+ HEADING_3 = "heading_3"
94
+ IMAGE = "image"
95
+ LINK_PREVIEW = "link_preview"
96
+ LINK_TO_PAGE = "link_to_page"
97
+ NUMBERED_LIST_ITEM = "numbered_list_item"
98
+ PARAGRAPH = "paragraph"
99
+ PDF = "pdf"
100
+ QUOTE = "quote"
101
+ SYNCED_BLOCK = "synced_block"
102
+ TABLE = "table"
103
+ TABLE_OF_CONTENTS = "table_of_contents"
104
+ TABLE_ROW = "table_row"
105
+ TO_DO = "to_do"
106
+ TOGGLE = "toggle"
107
+ UNSUPPORTED = "unsupported"
108
+ VIDEO = "video"
109
+ AUDIO = "audio"
110
+
111
+ # Markdown-specific aliases
112
+ HEADING = "heading"
113
+ BULLETED_LIST = "bulleted_list"
114
+ NUMBERED_LIST = "numbered_list"
115
+ TODO = "todo"
116
+ TOGGLEABLE_HEADING = "toggleable_heading"
117
+ COLUMNS = "columns"
118
+ SPACE = "space"
119
+
120
+
121
+ class HasRichText(Protocol):
122
+ """Protocol for objects that have a rich_text attribute."""
123
+
124
+ rich_text: list[RichTextObject]
125
+
126
+
127
+ class HasChildren(Protocol):
128
+ """Protocol for objects that have children blocks."""
129
+
130
+ children: list[BlockCreateRequest]