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
@@ -1,7 +1,14 @@
1
- from .divider_element import DividerElement
2
- from .divider_markdown_node import DividerMarkdownNode
1
+ from notionary.blocks.divider.divider_element import DividerElement
2
+ from notionary.blocks.divider.divider_markdown_node import (
3
+ DividerMarkdownBlockParams,
4
+ DividerMarkdownNode,
5
+ )
6
+ from notionary.blocks.divider.divider_models import CreateDividerBlock, DividerBlock
3
7
 
4
8
  __all__ = [
5
9
  "DividerElement",
10
+ "DividerBlock",
11
+ "CreateDividerBlock",
6
12
  "DividerMarkdownNode",
13
+ "DividerMarkdownBlockParams",
7
14
  ]
@@ -1,15 +1,15 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Dict, Any, Optional
4
+ from typing import Optional
3
5
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import (
6
- ElementPromptContent,
7
- ElementPromptBuilder,
8
- NotionBlockResult,
9
- )
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.divider.divider_models import CreateDividerBlock, DividerBlock
8
+ from notionary.blocks.models import Block, BlockCreateResult
9
+ from notionary.blocks.types import BlockType
10
10
 
11
11
 
12
- class DividerElement(NotionBlockElement):
12
+ class DividerElement(BaseBlockElement):
13
13
  """
14
14
  Handles conversion between Markdown horizontal dividers and Notion divider blocks.
15
15
 
@@ -20,53 +20,22 @@ class DividerElement(NotionBlockElement):
20
20
  PATTERN = re.compile(r"^\s*-{3,}\s*$")
21
21
 
22
22
  @classmethod
23
- def match_markdown(cls, text: str) -> bool:
24
- """Check if text is a markdown divider."""
25
- return bool(DividerElement.PATTERN.match(text))
26
-
27
- @classmethod
28
- def match_notion(cls, block: Dict[str, Any]) -> bool:
29
- """Check if block is a Notion divider."""
30
- return block.get("type") == "divider"
23
+ def match_notion(cls, block: Block) -> bool:
24
+ """Check if this element can handle the given Notion block."""
25
+ return block.type == BlockType.DIVIDER and block.divider
31
26
 
32
27
  @classmethod
33
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
34
- """Convert markdown divider to Notion divider block."""
35
- if not DividerElement.match_markdown(text):
28
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
29
+ """Convert markdown horizontal rule to Notion divider, with preceding empty paragraph."""
30
+ if not cls.PATTERN.match(text.strip()):
36
31
  return None
37
32
 
38
- empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
33
+ divider = DividerBlock()
39
34
 
40
- divider_block = {"type": "divider", "divider": {}}
41
-
42
- return [empty_paragraph, divider_block]
35
+ return CreateDividerBlock(divider=divider)
43
36
 
44
37
  @classmethod
45
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
46
- """Convert Notion divider block to markdown divider."""
47
- if block.get("type") != "divider":
38
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
39
+ if block.type != BlockType.DIVIDER or not block.divider:
48
40
  return None
49
-
50
41
  return "---"
51
-
52
- @classmethod
53
- def is_multiline(cls) -> bool:
54
- return False
55
-
56
- @classmethod
57
- def get_llm_prompt_content(cls) -> ElementPromptContent:
58
- """Returns structured LLM prompt metadata for the divider element."""
59
- return (
60
- ElementPromptBuilder()
61
- .with_description(
62
- "Creates a horizontal divider line to visually separate sections of content."
63
- )
64
- .with_usage_guidelines(
65
- "Use dividers only sparingly and only when the user explicitly asks for them. Dividers create strong visual breaks between content sections, so they should not be used unless specifically requested by the user."
66
- )
67
- .with_syntax("---")
68
- .with_examples(
69
- ["## Section 1\nContent\n\n---\n\n## Section 2\nMore content"]
70
- )
71
- .build()
72
- )
@@ -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 DividerMarkdownBlockParams(BaseModel):
@@ -0,0 +1,12 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class DividerBlock(BaseModel):
7
+ pass
8
+
9
+
10
+ class CreateDividerBlock(BaseModel):
11
+ type: Literal["divider"] = "divider"
12
+ divider: DividerBlock
@@ -1,7 +1,14 @@
1
- from .embed_element import EmbedElement
2
- from .embed_markdown_node import EmbedMarkdownNode
1
+ from notionary.blocks.embed.embed_element import EmbedElement
2
+ from notionary.blocks.embed.embed_markdown_node import (
3
+ EmbedMarkdownBlockParams,
4
+ EmbedMarkdownNode,
5
+ )
6
+ from notionary.blocks.embed.embed_models import CreateEmbedBlock, EmbedBlock
3
7
 
4
8
  __all__ = [
5
9
  "EmbedElement",
10
+ "EmbedBlock",
11
+ "CreateEmbedBlock",
6
12
  "EmbedMarkdownNode",
13
+ "EmbedMarkdownBlockParams",
7
14
  ]
@@ -1,144 +1,98 @@
1
- import re
2
- from typing import Dict, Any, Optional, List
1
+ from __future__ import annotations
3
2
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import (
6
- ElementPromptContent,
7
- ElementPromptBuilder,
8
- NotionBlockResult,
3
+ import re
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.embed.embed_models import CreateEmbedBlock, EmbedBlock
8
+ from notionary.blocks.file.file_element_models import (
9
+ ExternalFile,
10
+ FileUploadFile,
11
+ NotionHostedFile,
9
12
  )
13
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
14
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
15
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
16
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
10
17
 
11
18
 
12
- class EmbedElement(NotionBlockElement):
19
+ class EmbedElement(BaseBlockElement):
13
20
  """
14
21
  Handles conversion between Markdown embeds and Notion embed blocks.
15
22
 
16
23
  Markdown embed syntax:
17
- - [embed](https://example.com) - Simple embed with URL only
18
- - [embed](https://example.com "Caption") - Embed with URL and caption
19
-
20
- Where:
21
- - URL is the required embed URL
22
- - Caption is an optional descriptive text (enclosed in quotes)
23
-
24
- Supports various URL types including websites, PDFs, Google Maps, Google Drive,
25
- Twitter/X posts, and other sources that Notion can embed.
24
+ - [embed](https://example.com) - URL only
25
+ - [embed](https://example.com "Caption") - URL + caption
26
26
  """
27
27
 
28
- # Regex pattern for embed syntax with optional caption
29
28
  PATTERN = re.compile(
30
- r"^\[embed\]\(" # [embed]( prefix
31
- + r'(https?://[^\s"]+)' # URL (required)
32
- + r'(?:\s+"([^"]+)")?' # Optional caption in quotes
33
- + r"\)$" # closing parenthesis
29
+ r"^\[embed\]\(" # prefix
30
+ r"(https?://[^\s\"]+)" # URL
31
+ r"(?:\s+\"([^\"]+)\")?" # optional caption
32
+ r"\)$"
34
33
  )
35
34
 
36
35
  @classmethod
37
- def match_markdown(cls, text: str) -> bool:
38
- """Check if text is a markdown embed."""
39
- return text.strip().startswith("[embed]") and bool(
40
- EmbedElement.PATTERN.match(text.strip())
41
- )
36
+ def match_notion(cls, block: Block) -> bool:
37
+ return block.type == BlockType.EMBED and block.embed
42
38
 
43
39
  @classmethod
44
- def match_notion(cls, block: Dict[str, Any]) -> bool:
45
- """Check if block is a Notion embed."""
46
- return block.get("type") == "embed"
47
-
48
- @classmethod
49
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
50
- """Convert markdown embed to Notion embed block."""
51
- embed_match = EmbedElement.PATTERN.match(text.strip())
52
- if not embed_match:
40
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
41
+ """Convert markdown embed syntax to Notion EmbedBlock."""
42
+ match = cls.PATTERN.match(text.strip())
43
+ if not match:
53
44
  return None
54
45
 
55
- url = embed_match.group(1)
56
- caption = embed_match.group(2)
57
-
58
- if not url:
59
- return None
46
+ url, rich_text = match.group(1), match.group(2) or ""
60
47
 
61
- embed_data = {"url": url}
62
-
63
- # Add caption if provided
64
- if caption:
65
- embed_data["caption"] = [{"type": "text", "text": {"content": caption}}]
66
- else:
67
- embed_data["caption"] = []
48
+ # Build EmbedBlock
49
+ embed_block = EmbedBlock(url=url, caption=[])
50
+ if rich_text.strip():
51
+ rich_text_obj = RichTextObject.from_plain_text(rich_text.strip())
52
+ embed_block.caption = [rich_text_obj]
68
53
 
69
- # Prepare the embed block
70
- embed_block = {"type": "embed", "embed": embed_data}
71
-
72
- # Add empty paragraph after embed
73
- empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
74
-
75
- return [embed_block, empty_paragraph]
54
+ return CreateEmbedBlock(embed=embed_block)
76
55
 
77
56
  @classmethod
78
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
79
- """Convert Notion embed block to markdown embed."""
80
- if block.get("type") != "embed":
57
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
58
+ if block.type != BlockType.EMBED or not block.embed:
81
59
  return None
82
60
 
83
- embed_data = block.get("embed", {})
84
- url = embed_data.get("url", "")
61
+ fo = block.embed
85
62
 
86
- if not url:
63
+ if isinstance(fo, (ExternalFile, NotionHostedFile)):
64
+ url = fo.url
65
+ elif isinstance(fo, FileUploadFile):
66
+ return None
67
+ else:
87
68
  return None
88
69
 
89
- caption_rich_text = embed_data.get("caption", [])
90
-
91
- if not caption_rich_text:
92
- # Simple embed with URL only
70
+ if not fo.caption:
93
71
  return f"[embed]({url})"
94
72
 
95
- # Extract caption text
96
- caption = EmbedElement._extract_text_content(caption_rich_text)
73
+ text_parts = []
74
+ for rt in fo.caption:
75
+ if rt.plain_text:
76
+ text_parts.append(rt.plain_text)
77
+ else:
78
+ formatted_text = await TextInlineFormatter.extract_text_with_formatting(
79
+ [rt]
80
+ )
81
+ text_parts.append(formatted_text)
82
+ text = "".join(text_parts)
97
83
 
98
- if caption:
99
- return f'[embed]({url} "{caption}")'
100
-
101
- return f"[embed]({url})"
102
-
103
- @classmethod
104
- def is_multiline(cls) -> bool:
105
- """Embeds are single-line elements."""
106
- return False
107
-
108
- @classmethod
109
- def _extract_text_content(cls, rich_text: List[Dict[str, Any]]) -> str:
110
- """Extract plain text content from Notion rich_text elements."""
111
- result = ""
112
- for text_obj in rich_text:
113
- if text_obj.get("type") == "text":
114
- result += text_obj.get("text", {}).get("content", "")
115
- elif "plain_text" in text_obj:
116
- result += text_obj.get("plain_text", "")
117
- return result
84
+ return f'[embed]({url} "{text}")'
118
85
 
119
86
  @classmethod
120
- def get_llm_prompt_content(cls) -> ElementPromptContent:
121
- """
122
- Returns structured LLM prompt metadata for the embed element.
123
- """
124
- return (
125
- ElementPromptBuilder()
126
- .with_description(
127
- "Embeds external content from websites, PDFs, Google Maps, and other sources directly in your document."
128
- )
129
- .with_usage_guidelines(
130
- "Use embeds when you want to include external content that isn't just a video or image. "
131
- "Embeds are great for interactive content, reference materials, or live data sources."
132
- )
133
- .with_syntax('[embed](https://example.com "Optional caption")')
134
- .with_examples(
135
- [
136
- "[embed](https://drive.google.com/file/d/123456/view)",
137
- '[embed](https://www.google.com/maps?q=San+Francisco "Our office location")',
138
- '[embed](https://twitter.com/NotionHQ/status/1234567890 "Latest announcement")',
139
- '[embed](https://github.com/username/repo "Project documentation")',
140
- '[embed](https://example.com/important-reference.pdf "Course materials")',
141
- ]
142
- )
143
- .build()
87
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
88
+ """Get system prompt information for embed blocks."""
89
+ return BlockElementMarkdownInformation(
90
+ block_type=cls.__name__,
91
+ description="Embed blocks display interactive content from external URLs like videos, maps, or widgets",
92
+ syntax_examples=[
93
+ "[embed](https://youtube.com/watch?v=123)",
94
+ '[embed](https://maps.google.com/location "Map Location")',
95
+ '[embed](https://codepen.io/pen/123 "Interactive Demo")',
96
+ ],
97
+ usage_guidelines="Use for embedding interactive content that supports iframe embedding. URL must be from a supported platform. Caption describes the embedded content.",
144
98
  )
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import Optional
4
+
4
5
  from pydantic import BaseModel
5
- from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+ from notionary.markdown.markdown_node import MarkdownNode
6
8
 
7
9
 
8
10
  class EmbedMarkdownBlockParams(BaseModel):
@@ -0,0 +1,14 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing_extensions import Literal
3
+
4
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
5
+
6
+
7
+ class EmbedBlock(BaseModel):
8
+ url: str
9
+ caption: list[RichTextObject] = Field(default_factory=list)
10
+
11
+
12
+ class CreateEmbedBlock(BaseModel):
13
+ type: Literal["embed"] = "embed"
14
+ embed: EmbedBlock
@@ -0,0 +1,14 @@
1
+ from notionary.blocks.equation.equation_element import EquationElement
2
+ from notionary.blocks.equation.equation_element_markdown_node import (
3
+ EquationMarkdownBlockParams,
4
+ EquationMarkdownNode,
5
+ )
6
+ from notionary.blocks.equation.equation_models import CreateEquationBlock, EquationBlock
7
+
8
+ __all__ = [
9
+ "EquationElement",
10
+ "EquationBlock",
11
+ "CreateEquationBlock",
12
+ "EquationMarkdownNode",
13
+ "EquationMarkdownBlockParams",
14
+ ]
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import textwrap
5
+ from typing import Optional
6
+
7
+ from notionary.blocks.base_block_element import BaseBlockElement
8
+ from notionary.blocks.equation.equation_models import CreateEquationBlock, EquationBlock
9
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
10
+ from notionary.blocks.models import Block, BlockCreateResult
11
+ from notionary.blocks.types import BlockType
12
+
13
+
14
+ class EquationElement(BaseBlockElement):
15
+ """
16
+ Supports standard Markdown equation syntax:
17
+
18
+ - $$E = mc^2$$ # simple equations
19
+ - $$E = mc^2 + \\frac{a}{b}$$ # complex equations with LaTeX
20
+
21
+ Uses $$...$$ parsing for block equations.
22
+ """
23
+
24
+ _EQUATION_PATTERN = re.compile(
25
+ r"^\$\$\s*(?P<expression>.*?)\s*\$\$$",
26
+ re.DOTALL,
27
+ )
28
+
29
+ @classmethod
30
+ def match_notion(cls, block: Block) -> bool:
31
+ return block.type == BlockType.EQUATION and block.equation
32
+
33
+ @classmethod
34
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
35
+ input_text = text.strip()
36
+
37
+ equation_match = cls._EQUATION_PATTERN.match(input_text)
38
+ if not equation_match:
39
+ return None
40
+
41
+ expression = equation_match.group("expression").strip()
42
+ if not expression:
43
+ return None
44
+
45
+ return CreateEquationBlock(equation=EquationBlock(expression=expression))
46
+
47
+ @classmethod
48
+ def create_from_markdown_block(
49
+ cls, opening_line: str, equation_lines: list[str]
50
+ ) -> BlockCreateResult:
51
+ """
52
+ Create a complete equation block from markdown components.
53
+ Handles multiline equations like:
54
+ $$
55
+ some
56
+ inline formula here
57
+ $$
58
+
59
+ Automatically handles:
60
+ - Indentation removal from multiline strings
61
+ - Single backslash conversion to double backslash for LaTeX line breaks
62
+ """
63
+ # Check if opening line is just $$
64
+ if opening_line.strip() != "$$":
65
+ return None
66
+
67
+ # Process equation lines if any exist
68
+ if equation_lines:
69
+ # Remove common indentation from all lines
70
+ raw_content = "\n".join(equation_lines)
71
+ dedented_content = textwrap.dedent(raw_content)
72
+
73
+ # Fix single backslashes at line ends for LaTeX line breaks
74
+ fixed_lines = cls._fix_latex_line_breaks(dedented_content.splitlines())
75
+ expression = "\n".join(fixed_lines).strip()
76
+
77
+ if expression:
78
+ return CreateEquationBlock(
79
+ equation=EquationBlock(expression=expression)
80
+ )
81
+
82
+ return None
83
+
84
+ @classmethod
85
+ def _fix_latex_line_breaks(cls, lines: list[str]) -> list[str]:
86
+ """
87
+ Fix lines that end with single backslashes by converting them to double backslashes.
88
+ This makes LaTeX line breaks work correctly when users write single backslashes.
89
+
90
+ Examples:
91
+ - "a = b + c \" -> "a = b + c \\"
92
+ - "a = b + c \\\\" -> "a = b + c \\\\" (unchanged)
93
+ """
94
+ fixed_lines = []
95
+
96
+ for line in lines:
97
+ # Check if line ends with backslashes
98
+ backslash_match = re.search(r"(\\+)$", line)
99
+ if backslash_match:
100
+ backslashes = backslash_match.group(1)
101
+ # If odd number of backslashes, the last one needs to be doubled
102
+ if len(backslashes) % 2 == 1:
103
+ line = line + "\\"
104
+
105
+ fixed_lines.append(line)
106
+
107
+ return fixed_lines
108
+
109
+ @classmethod
110
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
111
+ if block.type != BlockType.EQUATION or not block.equation:
112
+ return None
113
+
114
+ expression = (block.equation.expression or "").strip()
115
+ if not expression:
116
+ return None
117
+
118
+ return f"$${expression}$$"
119
+
120
+ @classmethod
121
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
122
+ """Get system prompt information for equation blocks."""
123
+ return BlockElementMarkdownInformation(
124
+ block_type=cls.__name__,
125
+ description="Mathematical equations using standard Markdown LaTeX syntax",
126
+ syntax_examples=[
127
+ "$$E = mc^2$$",
128
+ "$$\\frac{a}{b} + \\sqrt{c}$$",
129
+ "$$\\int_0^\\infty e^{-x} dx = 1$$",
130
+ "$$\\sum_{i=1}^n i = \\frac{n(n+1)}{2}$$",
131
+ ],
132
+ usage_guidelines="Use for mathematical expressions and formulas. Supports LaTeX syntax. Wrap equations in double dollar signs ($$).",
133
+ )
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.markdown.markdown_node import MarkdownNode
6
+
7
+
8
+ class EquationMarkdownBlockParams(BaseModel):
9
+ expression: str
10
+
11
+
12
+ class EquationMarkdownNode(MarkdownNode):
13
+ """
14
+ Programmatic interface for creating Markdown equation blocks.
15
+ Uses standard Markdown equation syntax with double dollar signs.
16
+
17
+ Examples:
18
+ $$E = mc^2$$
19
+ $$\\frac{a}{b} + \\sqrt{c}$$
20
+ $$\\int_0^\\infty e^{-x} dx = 1$$
21
+ """
22
+
23
+ def __init__(self, expression: str):
24
+ self.expression = expression
25
+
26
+ @classmethod
27
+ def from_params(cls, params: EquationMarkdownBlockParams) -> EquationMarkdownNode:
28
+ return cls(expression=params.expression)
29
+
30
+ def to_markdown(self) -> str:
31
+ expr = self.expression.strip()
32
+ if not expr:
33
+ return "$$$$"
34
+
35
+ return f"$${expr}$$"
@@ -0,0 +1,11 @@
1
+ from pydantic import BaseModel
2
+ from typing_extensions import Literal
3
+
4
+
5
+ class EquationBlock(BaseModel):
6
+ expression: str
7
+
8
+
9
+ class CreateEquationBlock(BaseModel):
10
+ type: Literal["equation"] = "equation"
11
+ equation: EquationBlock
@@ -0,0 +1,25 @@
1
+ from notionary.blocks.file.file_element import FileElement
2
+ from notionary.blocks.file.file_element_markdown_node import (
3
+ FileMarkdownNode,
4
+ FileMarkdownNodeParams,
5
+ )
6
+ from notionary.blocks.file.file_element_models import (
7
+ CreateFileBlock,
8
+ ExternalFile,
9
+ FileBlock,
10
+ FileType,
11
+ FileUploadFile,
12
+ NotionHostedFile,
13
+ )
14
+
15
+ __all__ = [
16
+ "FileElement",
17
+ "FileType",
18
+ "ExternalFile",
19
+ "NotionHostedFile",
20
+ "FileUploadFile",
21
+ "FileBlock",
22
+ "CreateFileBlock",
23
+ "FileMarkdownNode",
24
+ "FileMarkdownNodeParams",
25
+ ]