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,97 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.file.file_element_models import ExternalFile, FileBlock, FileType
8
+ from notionary.blocks.mixins.captions import CaptionMixin
9
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
10
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
11
+ from notionary.blocks.pdf.pdf_models import CreatePdfBlock
12
+
13
+
14
+ class PdfElement(BaseBlockElement, CaptionMixin):
15
+ """
16
+ Handles conversion between Markdown PDF embeds and Notion PDF blocks.
17
+
18
+ Markdown PDF syntax:
19
+ - [pdf](https://example.com/document.pdf) - External URL
20
+ - [pdf](https://example.com/document.pdf)(caption:Annual Report 2024) - URL with caption
21
+ - (caption:User Manual)[pdf](https://example.com/manual.pdf) - caption before URL
22
+ - [pdf](notion://file_id_here)(caption:Notion hosted file) - Notion hosted file
23
+ - [pdf](upload://upload_id_here)(caption:File upload) - File upload
24
+
25
+ Supports all three PDF types: external, notion-hosted, and file uploads.
26
+ """
27
+
28
+ # Flexible pattern that can handle caption in any position
29
+ PDF_PATTERN = re.compile(r"\[pdf\]\(((?:https?://|notion://|upload://)[^\s\"]+)\)")
30
+
31
+ @classmethod
32
+ def match_notion(cls, block: Block) -> bool:
33
+ # Notion PDF block covers PDFs
34
+ return block.type == BlockType.PDF and block.pdf
35
+
36
+ @classmethod
37
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
38
+ """Convert markdown PDF link to Notion FileBlock (used for PDF)."""
39
+ # Use our own regex to find the PDF URL
40
+ pdf_match = cls.PDF_PATTERN.search(text.strip())
41
+ if not pdf_match:
42
+ return None
43
+
44
+ url = pdf_match.group(1)
45
+
46
+ # Use mixin to extract caption (if present anywhere in text)
47
+ caption_text = cls.extract_caption(text.strip())
48
+ caption_rich_text = cls.build_caption_rich_text(caption_text or "")
49
+
50
+ # Build FileBlock using FileType enum (reused for PDF)
51
+ pdf_block = FileBlock(
52
+ type=FileType.EXTERNAL,
53
+ external=ExternalFile(url=url),
54
+ caption=caption_rich_text,
55
+ )
56
+
57
+ return CreatePdfBlock(pdf=pdf_block)
58
+
59
+ @classmethod
60
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
61
+ if block.type != BlockType.PDF or not block.pdf:
62
+ return None
63
+
64
+ pb: FileBlock = block.pdf
65
+
66
+ if pb.type == FileType.EXTERNAL and pb.external:
67
+ url = pb.external.url
68
+ elif pb.type == FileType.FILE and pb.file:
69
+ url = pb.file.url
70
+ elif pb.type == FileType.FILE_UPLOAD:
71
+ return None
72
+ else:
73
+ return None
74
+
75
+ result = f"[pdf]({url})"
76
+
77
+ # Add caption if present
78
+ caption_markdown = await cls.format_caption_for_markdown(pb.caption or [])
79
+ if caption_markdown:
80
+ result += caption_markdown
81
+
82
+ return result
83
+
84
+ @classmethod
85
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
86
+ """Get system prompt information for PDF blocks."""
87
+ return BlockElementMarkdownInformation(
88
+ block_type=cls.__name__,
89
+ description="PDF blocks embed and display PDF documents from external URLs with optional captions",
90
+ syntax_examples=[
91
+ "[pdf](https://example.com/document.pdf)",
92
+ "[pdf](https://example.com/report.pdf)(caption:Annual Report 2024)",
93
+ "(caption:User Manual)[pdf](https://example.com/manual.pdf)",
94
+ "[pdf](https://example.com/guide.pdf)(caption:**Important** documentation)",
95
+ ],
96
+ usage_guidelines="Use for embedding PDF documents that can be viewed inline. Supports external URLs and Notion-hosted files. Caption supports rich text formatting and should describe the PDF content.",
97
+ )
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from notionary.markdown.markdown_node import MarkdownNode
8
+ from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
9
+
10
+
11
+ class PdfMarkdownNodeParams(BaseModel):
12
+ url: str
13
+ caption: Optional[str] = None
14
+
15
+
16
+ class PdfMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
17
+ """
18
+ Programmatic interface for creating Notion-style Markdown PDF embeds.
19
+ """
20
+
21
+ def __init__(self, url: str, caption: Optional[str] = None):
22
+ self.url = url
23
+ self.caption = caption or ""
24
+
25
+ @classmethod
26
+ def from_params(cls, params: PdfMarkdownNodeParams) -> PdfMarkdownNode:
27
+ return cls(url=params.url, caption=params.caption)
28
+
29
+ def to_markdown(self) -> str:
30
+ """Return the Markdown representation.
31
+
32
+ Examples:
33
+ - [pdf](https://example.com/document.pdf)
34
+ - [pdf](https://example.com/document.pdf)(caption:Critical safety information)
35
+ """
36
+ base_markdown = f"[pdf]({self.url})"
37
+ return self.append_caption_to_markdown(base_markdown, self.caption)
@@ -0,0 +1,11 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.file.file_element_models import FileBlock
6
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
7
+
8
+
9
+ class CreatePdfBlock(BaseModel):
10
+ type: Literal["pdf"] = "pdf"
11
+ pdf: FileBlock
@@ -1,7 +1,16 @@
1
- from .quote_element import QuoteElement
2
- from .quote_markdown_node import QuoteMarkdownNode
1
+ """Quote block handling for Notionary."""
2
+
3
+ from notionary.blocks.quote.quote_element import QuoteElement
4
+ from notionary.blocks.quote.quote_markdown_node import (
5
+ QuoteMarkdownBlockParams,
6
+ QuoteMarkdownNode,
7
+ )
8
+ from notionary.blocks.quote.quote_models import CreateQuoteBlock, QuoteBlock
3
9
 
4
10
  __all__ = [
5
11
  "QuoteElement",
12
+ "QuoteBlock",
13
+ "CreateQuoteBlock",
6
14
  "QuoteMarkdownNode",
15
+ "QuoteMarkdownBlockParams",
7
16
  ]
@@ -1,34 +1,35 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Dict, Any, Optional, List, Tuple
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, BlockType
9
+ from notionary.blocks.quote.quote_models import CreateQuoteBlock, QuoteBlock
10
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
11
+ from notionary.blocks.types import BlockColor
3
12
 
4
- from notionary.blocks import NotionBlockElement, NotionBlockResult
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
6
13
 
7
- class QuoteElement(NotionBlockElement):
14
+ class QuoteElement(BaseBlockElement):
8
15
  """
9
16
  Handles conversion between Markdown quotes and Notion quote blocks.
17
+
10
18
  Markdown quote syntax:
11
- - [quote](Simple quote text)
19
+ - > Simple quote text
20
+
21
+ Only single-line quotes without author metadata.
12
22
  """
13
23
 
14
- # Einzeilig, kein Author, kein Anführungszeichen-Kram mehr!
15
- PATTERN = re.compile(r'^\[quote\]\(([^\n\r]+)\)$')
24
+ PATTERN = re.compile(r"^>\s*(.+)$")
16
25
 
17
26
  @classmethod
18
- def match_markdown(cls, text: str) -> bool:
19
- m = cls.PATTERN.match(text.strip())
20
- # Nur gültig, wenn etwas nicht-leeres drinsteht
21
- return bool(m and m.group(1).strip())
27
+ def match_notion(cls, block: Block) -> bool:
28
+ return block.type == BlockType.QUOTE and block.quote
22
29
 
23
30
  @classmethod
24
- def match_notion(cls, block: Dict[str, Any]) -> bool:
25
- return block.get("type") == "quote"
26
-
27
- @classmethod
28
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
29
- if not text:
30
- return None
31
-
31
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
32
+ """Convert markdown quote to Notion QuoteBlock."""
32
33
  match = cls.PATTERN.match(text.strip())
33
34
  if not match:
34
35
  return None
@@ -37,56 +38,38 @@ class QuoteElement(NotionBlockElement):
37
38
  if not content:
38
39
  return None
39
40
 
40
- rich_text = [{"type": "text", "text": {"content": content}}]
41
- return {"type": "quote", "quote": {"rich_text": rich_text, "color": "default"}}
42
-
43
- @classmethod
44
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
45
- if block.get("type") != "quote":
41
+ # Reject multiline quotes
42
+ if "\n" in content or "\r" in content:
46
43
  return None
47
44
 
48
- rich_text = block.get("quote", {}).get("rich_text", [])
49
- content = cls._extract_text_content(rich_text)
50
- if not content.strip():
51
- return None
45
+ rich_text = await TextInlineFormatter.parse_inline_formatting(content)
52
46
 
53
- return f"[quote]({content.strip()})"
47
+ quote_content = QuoteBlock(rich_text=rich_text, color=BlockColor.DEFAULT)
48
+ return CreateQuoteBlock(quote=quote_content)
54
49
 
55
50
  @classmethod
56
- def find_matches(cls, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
57
- matches = []
58
- for match in re.finditer(r"^\[quote\]\([^\n\r]+\)$", text, re.MULTILINE):
59
- candidate = match.group(0)
60
- block = cls.markdown_to_notion(candidate)
61
- if block:
62
- matches.append((match.start(), match.end(), block))
63
- return matches
51
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
52
+ if block.type != BlockType.QUOTE or not block.quote:
53
+ return None
64
54
 
65
- @classmethod
66
- def is_multiline(cls) -> bool:
67
- return False
55
+ rich = block.quote.rich_text
56
+ text = await TextInlineFormatter.extract_text_with_formatting(rich)
68
57
 
69
- @classmethod
70
- def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
71
- return "".join(
72
- t.get("text", {}).get("content", "")
73
- for t in rich_text
74
- if t.get("type") == "text"
75
- )
58
+ if not text.strip():
59
+ return None
60
+
61
+ return f"> {text.strip()}"
76
62
 
77
63
  @classmethod
78
- def get_llm_prompt_content(cls) -> ElementPromptContent:
79
- return (
80
- ElementPromptBuilder()
81
- .with_description("Creates blockquotes that visually distinguish quoted text.")
82
- .with_usage_guidelines(
83
- "Use quotes for quoting external sources, highlighting important statements, "
84
- "or creating visual emphasis for key information."
85
- )
86
- .with_syntax('[quote](Quote text)')
87
- .with_examples([
88
- "[quote](This is a simple blockquote)",
89
- "[quote](Knowledge is power)",
90
- ])
91
- .build()
64
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
65
+ """Get system prompt information for quote blocks."""
66
+ return BlockElementMarkdownInformation(
67
+ block_type=cls.__name__,
68
+ description="Quote blocks display highlighted quotations or emphasized text",
69
+ syntax_examples=[
70
+ "> This is an important quote",
71
+ "> The only way to do great work is to love what you do",
72
+ "> Innovation distinguishes between a leader and a follower",
73
+ ],
74
+ usage_guidelines="Use for quotations, important statements, or text that should be visually emphasized. Content should be meaningful and stand out from regular paragraphs.",
92
75
  )
@@ -1,15 +1,18 @@
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
6
+
5
7
 
6
8
  class QuoteMarkdownBlockParams(BaseModel):
7
9
  text: str
8
10
 
11
+
9
12
  class QuoteMarkdownNode(MarkdownNode):
10
13
  """
11
14
  Programmatic interface for creating Notion-style quote blocks.
12
- Example: [quote](This is a quote)
15
+ Example: > This is a quote
13
16
  """
14
17
 
15
18
  def __init__(self, text: str):
@@ -20,4 +23,4 @@ class QuoteMarkdownNode(MarkdownNode):
20
23
  return cls(text=params.text)
21
24
 
22
25
  def to_markdown(self) -> str:
23
- return f"[quote]({self.text})"
26
+ return f"> {self.text}"
@@ -0,0 +1,18 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from notionary.blocks.models import Block
6
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
7
+ from notionary.blocks.types import BlockColor
8
+
9
+
10
+ class QuoteBlock(BaseModel):
11
+ rich_text: list[RichTextObject]
12
+ color: BlockColor = BlockColor.DEFAULT
13
+ children: list[Block] = Field(default_factory=list)
14
+
15
+
16
+ class CreateQuoteBlock(BaseModel):
17
+ type: Literal["quote"] = "quote"
18
+ quote: QuoteBlock
@@ -0,0 +1,4 @@
1
+ from .block_registry import BlockRegistry
2
+ from .block_registry_builder import BlockRegistryBuilder
3
+
4
+ __all__ = ["BlockRegistryBuilder", "BlockRegistry"]
@@ -1,156 +1,95 @@
1
1
  from __future__ import annotations
2
- from typing import Dict, Any, Optional, List, Set, Type
3
2
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.page.markdown_syntax_prompt_generator import (
6
- MarkdownSyntaxPromptGenerator,
7
- )
8
- from notionary.blocks import TextInlineFormatter
3
+ from typing import Optional, Type
9
4
 
10
- from notionary.blocks import NotionBlockElement
5
+ from notionary.blocks.base_block_element import BaseBlockElement
6
+ from notionary.blocks.registry.block_registry_builder import BlockRegistryBuilder
11
7
  from notionary.telemetry import (
12
8
  ProductTelemetry,
13
- NotionMarkdownSyntaxPromptEvent,
14
- MarkdownToNotionConversionEvent,
15
- NotionToMarkdownConversionEvent,
16
9
  )
17
10
 
18
11
 
19
12
  class BlockRegistry:
20
13
  """Registry of elements that can convert between Markdown and Notion."""
21
14
 
22
- def __init__(self, elements=None):
15
+ def __init__(self, builder: Optional[BlockRegistryBuilder] = None):
23
16
  """
24
17
  Initialize a new registry instance.
25
18
 
26
19
  Args:
27
- elements: Initial elements to register
20
+ builder: BlockRegistryBuilder instance to delegate operations to
28
21
  """
29
- self._elements: List[NotionBlockElement] = []
30
- self._element_types: Set[Type[NotionBlockElement]] = set()
31
-
32
- if elements:
33
- for element in elements:
34
- self.register(element)
22
+ # Import here to avoid circular imports
23
+ from notionary.blocks.registry.block_registry_builder import (
24
+ BlockRegistryBuilder,
25
+ )
35
26
 
27
+ self._builder: BlockRegistryBuilder = builder or BlockRegistryBuilder()
36
28
  self.telemetry = ProductTelemetry()
37
29
 
38
30
  @classmethod
39
31
  def create_registry(cls) -> BlockRegistry:
40
32
  """
41
33
  Create a registry with all standard elements in recommended order.
42
-
43
- This uses the BlockRegistryBuilder internally to construct a complete
44
- registry with all available block types.
45
-
46
- Returns:
47
- BlockRegistry: A fully configured registry with all standard elements
48
34
  """
49
- from notionary.blocks import BlockRegistryBuilder
50
-
51
- return BlockRegistryBuilder.create_registry()
52
-
53
- def register(self, element_class: Type[NotionBlockElement]) -> bool:
35
+ from notionary.blocks.registry.block_registry_builder import (
36
+ BlockRegistryBuilder,
37
+ )
38
+
39
+ builder = BlockRegistryBuilder()
40
+ builder = (
41
+ builder.with_headings()
42
+ .with_callouts()
43
+ .with_code()
44
+ .with_dividers()
45
+ .with_tables()
46
+ .with_bulleted_list()
47
+ .with_numbered_list()
48
+ .with_toggles()
49
+ .with_toggleable_heading_element()
50
+ .with_quotes()
51
+ .with_todos()
52
+ .with_bookmarks()
53
+ .with_images()
54
+ .with_videos()
55
+ .with_embeds()
56
+ .with_audio()
57
+ .with_columns()
58
+ .with_equation()
59
+ .with_table_of_contents()
60
+ .with_breadcrumbs()
61
+ .with_child_database()
62
+ .with_paragraphs() # position here is important - its a fallback!
63
+ )
64
+
65
+ return cls(builder=builder)
66
+
67
+ @property
68
+ def builder(self) -> BlockRegistryBuilder:
69
+ return self._builder
70
+
71
+ def register(self, element_class: Type[BaseBlockElement]) -> bool:
54
72
  """
55
- Register an element class.
56
-
57
- Args:
58
- element_class: The element class to register
59
-
60
- Returns:
61
- bool: True if element was added, False if it already existed
73
+ Register an element class via builder.
62
74
  """
63
- if element_class in self._element_types:
64
- return False
75
+ initial_count = len(self._builder._elements)
76
+ self._builder._add_element(element_class)
77
+ return len(self._builder._elements) > initial_count
65
78
 
66
- self._elements.append(element_class)
67
- self._element_types.add(element_class)
68
- return True
69
-
70
- def deregister(self, element_class: Type[NotionBlockElement]) -> bool:
79
+ def deregister(self, element_class: Type[BaseBlockElement]) -> bool:
71
80
  """
72
- Deregister an element class.
81
+ Deregister an element class via builder.
73
82
  """
74
- if element_class in self._element_types:
75
- self._elements.remove(element_class)
76
- self._element_types.remove(element_class)
77
- return True
78
- return False
83
+ initial_count = len(self._builder._elements)
84
+ self._builder.remove_element(element_class)
85
+ return len(self._builder._elements) < initial_count
79
86
 
80
- def contains(self, element_class: Type[NotionBlockElement]) -> bool:
87
+ def contains(self, element_class: Type[BaseBlockElement]) -> bool:
81
88
  """
82
89
  Checks if a specific element is contained in the registry.
83
-
84
- Args:
85
- element_class: The element class to check.
86
-
87
- Returns:
88
- bool: True if the element is contained, otherwise False.
89
90
  """
90
- return element_class in self._elements
91
-
92
- def find_markdown_handler(self, text: str) -> Optional[Type[NotionBlockElement]]:
93
- """Find an element that can handle the given markdown text."""
94
- for element in self._elements:
95
- if element.match_markdown(text):
96
- return element
97
- return None
91
+ return element_class.__name__ in self._builder._elements
98
92
 
99
- def markdown_to_notion(self, text: str) -> Optional[Dict[str, Any]]:
100
- """Convert markdown to Notion block using registered elements."""
101
- handler = self.find_markdown_handler(text)
102
-
103
- if handler:
104
- self.telemetry.capture(
105
- MarkdownToNotionConversionEvent(
106
- handler_element_name=handler.__name__,
107
- )
108
- )
109
-
110
- return handler.markdown_to_notion(text)
111
- return None
112
-
113
- def notion_to_markdown(self, block: Dict[str, Any]) -> Optional[str]:
114
- """Convert Notion block to markdown using registered elements."""
115
- handler = self._find_notion_handler(block)
116
-
117
- if handler:
118
- self.telemetry.capture(
119
- NotionToMarkdownConversionEvent(
120
- handler_element_name=handler.__name__,
121
- )
122
- )
123
-
124
- return handler.notion_to_markdown(block)
125
- return None
126
-
127
- def get_multiline_elements(self) -> List[Type[NotionBlockElement]]:
128
- """Get all registered multiline elements."""
129
- return [element for element in self._elements if element.is_multiline()]
130
-
131
- def get_elements(self) -> List[Type[NotionBlockElement]]:
93
+ def get_elements(self) -> list[Type[BaseBlockElement]]:
132
94
  """Get all registered elements."""
133
- return self._elements.copy()
134
-
135
- def get_notion_markdown_syntax_prompt(self) -> str:
136
- """
137
- Generates an LLM system prompt that describes the Markdown syntax of all registered elements.
138
- """
139
- element_classes = self._elements.copy()
140
-
141
- formatter_names = [e.__name__ for e in element_classes]
142
- if "TextInlineFormatter" not in formatter_names:
143
- element_classes = element_classes + [TextInlineFormatter]
144
-
145
- self.telemetry.capture(NotionMarkdownSyntaxPromptEvent())
146
-
147
- return MarkdownSyntaxPromptGenerator.generate_system_prompt(element_classes)
148
-
149
- def _find_notion_handler(
150
- self, block: Dict[str, Any]
151
- ) -> Optional[Type[NotionBlockElement]]:
152
- """Find an element that can handle the given Notion block."""
153
- for element in self._elements:
154
- if element.match_notion(block):
155
- return element
156
- return None
95
+ return list(self._builder._elements.values())