notionary 0.2.19__py3-none-any.whl → 0.2.21__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 (205) 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 +263 -0
  5. notionary/blocks/audio/__init__.py +8 -2
  6. notionary/blocks/audio/audio_element.py +42 -104
  7. notionary/blocks/audio/audio_markdown_node.py +3 -1
  8. notionary/blocks/audio/audio_models.py +6 -55
  9. notionary/blocks/base_block_element.py +30 -0
  10. notionary/blocks/bookmark/__init__.py +9 -2
  11. notionary/blocks/bookmark/bookmark_element.py +46 -139
  12. notionary/blocks/bookmark/bookmark_markdown_node.py +3 -1
  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 +40 -55
  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 +40 -89
  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 +7 -0
  27. notionary/blocks/child_database/child_database_models.py +19 -0
  28. notionary/blocks/child_page/__init__.py +9 -0
  29. notionary/blocks/child_page/child_page_models.py +12 -0
  30. notionary/blocks/{shared/block_client.py → client.py} +55 -54
  31. notionary/blocks/code/__init__.py +6 -2
  32. notionary/blocks/code/code_element.py +53 -187
  33. notionary/blocks/code/code_markdown_node.py +13 -13
  34. notionary/blocks/code/code_models.py +94 -0
  35. notionary/blocks/column/__init__.py +25 -1
  36. notionary/blocks/column/column_element.py +40 -314
  37. notionary/blocks/column/column_list_element.py +37 -0
  38. notionary/blocks/column/column_list_markdown_node.py +50 -0
  39. notionary/blocks/column/column_markdown_node.py +59 -0
  40. notionary/blocks/column/column_models.py +26 -0
  41. notionary/blocks/divider/__init__.py +9 -2
  42. notionary/blocks/divider/divider_element.py +26 -49
  43. notionary/blocks/divider/divider_markdown_node.py +2 -1
  44. notionary/blocks/divider/divider_models.py +12 -0
  45. notionary/blocks/embed/__init__.py +9 -2
  46. notionary/blocks/embed/embed_element.py +47 -114
  47. notionary/blocks/embed/embed_markdown_node.py +3 -1
  48. notionary/blocks/embed/embed_models.py +14 -0
  49. notionary/blocks/equation/__init__.py +14 -0
  50. notionary/blocks/equation/equation_element.py +80 -0
  51. notionary/blocks/equation/equation_element_markdown_node.py +36 -0
  52. notionary/blocks/equation/equation_models.py +11 -0
  53. notionary/blocks/file/__init__.py +25 -0
  54. notionary/blocks/file/file_element.py +93 -0
  55. notionary/blocks/file/file_element_markdown_node.py +35 -0
  56. notionary/blocks/file/file_element_models.py +39 -0
  57. notionary/blocks/heading/__init__.py +16 -2
  58. notionary/blocks/heading/heading_element.py +67 -72
  59. notionary/blocks/heading/heading_markdown_node.py +2 -1
  60. notionary/blocks/heading/heading_models.py +29 -0
  61. notionary/blocks/image_block/__init__.py +13 -0
  62. notionary/blocks/image_block/image_element.py +84 -0
  63. notionary/blocks/{image → image_block}/image_markdown_node.py +3 -1
  64. notionary/blocks/image_block/image_models.py +10 -0
  65. notionary/blocks/models.py +172 -0
  66. notionary/blocks/numbered_list/__init__.py +12 -2
  67. notionary/blocks/numbered_list/numbered_list_element.py +33 -58
  68. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
  69. notionary/blocks/numbered_list/numbered_list_models.py +17 -0
  70. notionary/blocks/paragraph/__init__.py +12 -2
  71. notionary/blocks/paragraph/paragraph_element.py +27 -69
  72. notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
  73. notionary/blocks/paragraph/paragraph_models.py +16 -0
  74. notionary/blocks/pdf/__init__.py +13 -0
  75. notionary/blocks/pdf/pdf_element.py +91 -0
  76. notionary/blocks/pdf/pdf_markdown_node.py +35 -0
  77. notionary/blocks/pdf/pdf_models.py +11 -0
  78. notionary/blocks/quote/__init__.py +11 -2
  79. notionary/blocks/quote/quote_element.py +31 -65
  80. notionary/blocks/quote/quote_markdown_node.py +4 -1
  81. notionary/blocks/quote/quote_models.py +18 -0
  82. notionary/blocks/registry/__init__.py +4 -0
  83. notionary/blocks/registry/block_registry.py +75 -91
  84. notionary/blocks/registry/block_registry_builder.py +107 -59
  85. notionary/blocks/rich_text/__init__.py +33 -0
  86. notionary/blocks/rich_text/rich_text_models.py +188 -0
  87. notionary/blocks/rich_text/text_inline_formatter.py +125 -0
  88. notionary/blocks/table/__init__.py +16 -2
  89. notionary/blocks/table/table_element.py +48 -241
  90. notionary/blocks/table/table_markdown_node.py +2 -1
  91. notionary/blocks/table/table_models.py +28 -0
  92. notionary/blocks/table_of_contents/__init__.py +19 -0
  93. notionary/blocks/table_of_contents/table_of_contents_element.py +51 -0
  94. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
  95. notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
  96. notionary/blocks/todo/__init__.py +9 -2
  97. notionary/blocks/todo/todo_element.py +38 -95
  98. notionary/blocks/todo/todo_markdown_node.py +2 -1
  99. notionary/blocks/todo/todo_models.py +19 -0
  100. notionary/blocks/toggle/__init__.py +13 -3
  101. notionary/blocks/toggle/toggle_element.py +57 -264
  102. notionary/blocks/toggle/toggle_markdown_node.py +24 -14
  103. notionary/blocks/toggle/toggle_models.py +17 -0
  104. notionary/blocks/toggleable_heading/__init__.py +6 -2
  105. notionary/blocks/toggleable_heading/toggleable_heading_element.py +74 -244
  106. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
  107. notionary/blocks/types.py +61 -0
  108. notionary/blocks/video/__init__.py +8 -2
  109. notionary/blocks/video/video_element.py +67 -143
  110. notionary/blocks/video/video_element_models.py +10 -0
  111. notionary/blocks/video/video_markdown_node.py +3 -1
  112. notionary/database/client.py +3 -8
  113. notionary/database/database.py +13 -14
  114. notionary/database/database_filter_builder.py +2 -2
  115. notionary/database/database_provider.py +5 -4
  116. notionary/database/models.py +337 -0
  117. notionary/database/notion_database.py +6 -7
  118. notionary/file_upload/client.py +5 -7
  119. notionary/file_upload/models.py +2 -1
  120. notionary/file_upload/notion_file_upload.py +2 -3
  121. notionary/markdown/markdown_builder.py +722 -0
  122. notionary/markdown/markdown_document_model.py +228 -0
  123. notionary/{blocks → markdown}/markdown_node.py +1 -0
  124. notionary/models/notion_database_response.py +0 -338
  125. notionary/page/client.py +9 -10
  126. notionary/page/models.py +327 -0
  127. notionary/page/notion_page.py +99 -52
  128. notionary/page/notion_text_length_utils.py +119 -0
  129. notionary/page/{content/page_content_writer.py → page_content_writer.py} +88 -38
  130. notionary/page/reader/handler/__init__.py +17 -0
  131. notionary/page/reader/handler/base_block_renderer.py +44 -0
  132. notionary/page/reader/handler/block_processing_context.py +35 -0
  133. notionary/page/reader/handler/block_rendering_context.py +43 -0
  134. notionary/page/reader/handler/column_list_renderer.py +51 -0
  135. notionary/page/reader/handler/column_renderer.py +60 -0
  136. notionary/page/reader/handler/line_renderer.py +60 -0
  137. notionary/page/reader/handler/toggle_renderer.py +69 -0
  138. notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
  139. notionary/page/reader/page_content_retriever.py +69 -0
  140. notionary/page/search_filter_builder.py +2 -1
  141. notionary/page/writer/handler/__init__.py +22 -0
  142. notionary/page/writer/handler/code_handler.py +100 -0
  143. notionary/page/writer/handler/column_handler.py +141 -0
  144. notionary/page/writer/handler/column_list_handler.py +139 -0
  145. notionary/page/writer/handler/line_handler.py +35 -0
  146. notionary/page/writer/handler/line_processing_context.py +54 -0
  147. notionary/page/writer/handler/regular_line_handler.py +92 -0
  148. notionary/page/writer/handler/table_handler.py +130 -0
  149. notionary/page/writer/handler/toggle_handler.py +153 -0
  150. notionary/page/writer/handler/toggleable_heading_handler.py +167 -0
  151. notionary/page/writer/markdown_to_notion_converter.py +76 -0
  152. notionary/telemetry/__init__.py +2 -2
  153. notionary/telemetry/service.py +4 -3
  154. notionary/user/__init__.py +2 -2
  155. notionary/user/base_notion_user.py +2 -1
  156. notionary/user/client.py +2 -3
  157. notionary/user/models.py +1 -0
  158. notionary/user/notion_bot_user.py +4 -5
  159. notionary/user/notion_user.py +3 -4
  160. notionary/user/notion_user_manager.py +3 -2
  161. notionary/user/notion_user_provider.py +1 -1
  162. notionary/util/__init__.py +3 -2
  163. notionary/util/fuzzy.py +2 -1
  164. notionary/util/logging_mixin.py +2 -2
  165. notionary/util/singleton_metaclass.py +1 -1
  166. notionary/workspace.py +3 -2
  167. {notionary-0.2.19.dist-info → notionary-0.2.21.dist-info}/METADATA +12 -8
  168. notionary-0.2.21.dist-info/RECORD +185 -0
  169. notionary/blocks/document/__init__.py +0 -7
  170. notionary/blocks/document/document_element.py +0 -102
  171. notionary/blocks/document/document_markdown_node.py +0 -31
  172. notionary/blocks/image/__init__.py +0 -7
  173. notionary/blocks/image/image_element.py +0 -151
  174. notionary/blocks/markdown_builder.py +0 -356
  175. notionary/blocks/mention/__init__.py +0 -7
  176. notionary/blocks/mention/mention_element.py +0 -229
  177. notionary/blocks/mention/mention_markdown_node.py +0 -38
  178. notionary/blocks/prompts/element_prompt_builder.py +0 -83
  179. notionary/blocks/prompts/element_prompt_content.py +0 -41
  180. notionary/blocks/shared/__init__.py +0 -0
  181. notionary/blocks/shared/models.py +0 -713
  182. notionary/blocks/shared/notion_block_element.py +0 -37
  183. notionary/blocks/shared/text_inline_formatter.py +0 -262
  184. notionary/blocks/shared/text_inline_formatter_new.py +0 -139
  185. notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  186. notionary/database/models/page_result.py +0 -10
  187. notionary/elements/__init__.py +0 -0
  188. notionary/models/notion_block_response.py +0 -264
  189. notionary/models/notion_page_response.py +0 -78
  190. notionary/models/search_response.py +0 -0
  191. notionary/page/__init__.py +0 -0
  192. notionary/page/content/notion_text_length_utils.py +0 -87
  193. notionary/page/content/page_content_retriever.py +0 -60
  194. notionary/page/formatting/line_processor.py +0 -153
  195. notionary/page/formatting/markdown_to_notion_converter.py +0 -153
  196. notionary/page/markdown_syntax_prompt_generator.py +0 -114
  197. notionary/page/notion_to_markdown_converter.py +0 -179
  198. notionary/page/properites/property_value_extractor.py +0 -0
  199. notionary-0.2.19.dist-info/RECORD +0 -150
  200. /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
  201. /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
  202. /notionary/page/{content/markdown_whitespace_processor.py → markdown_whitespace_processor.py} +0 -0
  203. /notionary/{blocks/mention/mention_models.py → page/reader/handler/context.py} +0 -0
  204. {notionary-0.2.19.dist-info → notionary-0.2.21.dist-info}/LICENSE +0 -0
  205. {notionary-0.2.19.dist-info → notionary-0.2.21.dist-info}/WHEEL +0 -0
@@ -1,7 +1,17 @@
1
- from .paragraph_element import ParagraphElement
2
- from .paragraph_markdown_node import ParagraphMarkdownNode
1
+ from notionary.blocks.paragraph.paragraph_element import ParagraphElement
2
+ from notionary.blocks.paragraph.paragraph_markdown_node import (
3
+ ParagraphMarkdownBlockParams,
4
+ ParagraphMarkdownNode,
5
+ )
6
+ from notionary.blocks.paragraph.paragraph_models import (
7
+ CreateParagraphBlock,
8
+ ParagraphBlock,
9
+ )
3
10
 
4
11
  __all__ = [
5
12
  "ParagraphElement",
13
+ "ParagraphBlock",
14
+ "CreateParagraphBlock",
6
15
  "ParagraphMarkdownNode",
16
+ "ParagraphMarkdownBlockParams",
7
17
  ]
@@ -1,84 +1,42 @@
1
- from typing import Dict, Any, Optional
1
+ from __future__ import annotations
2
2
 
3
- from notionary.blocks import NotionBlockElement
4
- from notionary.blocks import (
5
- ElementPromptContent,
6
- ElementPromptBuilder,
7
- NotionBlockResult,
8
- )
9
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
3
+ from typing import Optional
10
4
 
5
+ from notionary.blocks.base_block_element import BaseBlockElement
6
+ from notionary.blocks.models import Block, BlockCreateResult
7
+ from notionary.blocks.paragraph.paragraph_models import (
8
+ CreateParagraphBlock,
9
+ ParagraphBlock,
10
+ )
11
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
12
+ from notionary.blocks.types import BlockColor, BlockType
11
13
 
12
- class ParagraphElement(NotionBlockElement):
13
- """Handles conversion between Markdown paragraphs and Notion paragraph blocks."""
14
14
 
15
- @classmethod
16
- def match_markdown(cls, text: str) -> bool:
17
- """
18
- Check if text is a markdown paragraph.
19
- Paragraphs are essentially any text that isn't matched by other block elements.
20
- Since paragraphs are the fallback element, this always returns True.
21
- """
22
- return True
15
+ class ParagraphElement(BaseBlockElement):
16
+ """
17
+ Handles conversion between Markdown paragraphs and Notion paragraph blocks.
18
+ """
23
19
 
24
20
  @classmethod
25
- def match_notion(cls, block: Dict[str, Any]) -> bool:
26
- """Check if block is a Notion paragraph."""
27
- return block.get("type") == "paragraph"
21
+ def match_notion(cls, block: Block) -> bool:
22
+ return block.type == "paragraph" and block.paragraph
28
23
 
29
24
  @classmethod
30
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
31
- """Convert markdown paragraph to Notion paragraph block."""
25
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
26
+ """Convert markdown text to a Notion ParagraphBlock."""
32
27
  if not text.strip():
33
28
  return None
34
29
 
35
- return {
36
- "type": "paragraph",
37
- "paragraph": {
38
- "rich_text": TextInlineFormatter.parse_inline_formatting(text)
39
- },
40
- }
30
+ rich = TextInlineFormatter.parse_inline_formatting(text)
41
31
 
42
- @classmethod
43
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
44
- """Convert Notion paragraph block to markdown paragraph."""
45
- if block.get("type") != "paragraph":
46
- return None
47
-
48
- paragraph_data = block.get("paragraph", {})
49
- rich_text = paragraph_data.get("rich_text", [])
50
-
51
- text = TextInlineFormatter.extract_text_with_formatting(rich_text)
52
- return text if text else None
32
+ paragraph_content = ParagraphBlock(rich_text=rich, color=BlockColor.DEFAULT)
33
+ return CreateParagraphBlock(paragraph=paragraph_content)
53
34
 
54
35
  @classmethod
55
- def is_multiline(cls) -> bool:
56
- return False
36
+ def notion_to_markdown(cls, block: Block) -> Optional[str]:
37
+ if block.type != "paragraph" or not block.paragraph:
38
+ return None
57
39
 
58
- @classmethod
59
- def get_llm_prompt_content(cls) -> ElementPromptContent:
60
- """
61
- Returns structured LLM prompt metadata for the paragraph element,
62
- including information about supported inline formatting.
63
- """
64
- return (
65
- ElementPromptBuilder()
66
- .with_description(
67
- "Creates standard paragraph blocks for regular text content with support for inline formatting: "
68
- "**bold**, *italic*, `code`, ~~strikethrough~~, __underline__, and [links](url)."
69
- )
70
- .with_usage_guidelines(
71
- "Use for normal text content. Paragraphs are the default block type when no specific formatting is applied. "
72
- "Apply inline formatting to highlight key points or provide links to resources."
73
- )
74
- .with_syntax("Just write text normally without any special prefix")
75
- .with_examples(
76
- [
77
- "This is a simple paragraph with plain text.",
78
- "This paragraph has **bold** and *italic* formatting.",
79
- "You can include [links](https://example.com) or `inline code`.",
80
- "Advanced formatting: ~~strikethrough~~ and __underlined text__.",
81
- ]
82
- )
83
- .build()
84
- )
40
+ rich_list = block.paragraph.rich_text
41
+ markdown = TextInlineFormatter.extract_text_with_formatting(rich_list)
42
+ return markdown or None
@@ -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 ParagraphMarkdownBlockParams(BaseModel):
@@ -0,0 +1,16 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
6
+ from notionary.blocks.types import BlockColor
7
+
8
+
9
+ class ParagraphBlock(BaseModel):
10
+ rich_text: list[RichTextObject]
11
+ color: BlockColor = BlockColor.DEFAULT.value
12
+
13
+
14
+ class CreateParagraphBlock(BaseModel):
15
+ type: Literal["paragraph"] = "paragraph"
16
+ paragraph: ParagraphBlock
@@ -0,0 +1,13 @@
1
+ from notionary.blocks.pdf.pdf_element import PdfElement
2
+ from notionary.blocks.pdf.pdf_markdown_node import (
3
+ PdfMarkdownNode,
4
+ PdfMarkdownNodeParams,
5
+ )
6
+ from notionary.blocks.pdf.pdf_models import CreatePdfBlock
7
+
8
+ __all__ = [
9
+ "PdfElement",
10
+ "CreatePdfBlock",
11
+ "PdfMarkdownNode",
12
+ "PdfMarkdownNodeParams",
13
+ ]
@@ -0,0 +1,91 @@
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.models import Block, BlockCreateResult, BlockType
9
+ from notionary.blocks.paragraph.paragraph_models import (
10
+ CreateParagraphBlock,
11
+ ParagraphBlock,
12
+ )
13
+ from notionary.blocks.pdf.pdf_models import CreatePdfBlock
14
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
15
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
16
+
17
+
18
+ class PdfElement(BaseBlockElement):
19
+ """
20
+ Handles conversion between Markdown PDF embeds and Notion PDF blocks.
21
+
22
+ Markdown PDF syntax:
23
+ - [pdf](https://example.com/document.pdf "Caption") # External URL
24
+ - [pdf](notion://file_id_here "Caption") # Notion hosted file
25
+ - [pdf](upload://upload_id_here "Caption") # File upload
26
+ - [pdf](https://example.com/document.pdf) # Without caption
27
+
28
+ Supports all three PDF types: external, notion-hosted, and file uploads.
29
+ """
30
+
31
+ PATTERN = re.compile(
32
+ r"^\[pdf\]\(" # prefix
33
+ r'((?:https?://|notion://|upload://)[^\s\)"]+)' # URL with protocol
34
+ r'(?:\s+"([^"]*)")?' # optional caption
35
+ r"\)$"
36
+ )
37
+
38
+ @classmethod
39
+ def match_notion(cls, block: Block) -> bool:
40
+ # Notion PDF block covers PDFs
41
+ return block.type == BlockType.PDF and block.pdf
42
+
43
+ @classmethod
44
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
45
+ """Convert markdown PDF link to Notion FileBlock (used for PDF) followed by an empty paragraph."""
46
+ match = cls.PATTERN.match(text.strip())
47
+ if not match:
48
+ return None
49
+
50
+ url, caption_text = match.group(1), match.group(2) or ""
51
+
52
+ # Build FileBlock using FileType enum (reused for PDF)
53
+ pdf_block = FileBlock(
54
+ type=FileType.EXTERNAL, external=ExternalFile(url=url), caption=[]
55
+ )
56
+ if caption_text.strip():
57
+ rt = RichTextObject.from_plain_text(caption_text)
58
+ pdf_block.caption = [rt]
59
+
60
+ empty_para = ParagraphBlock(rich_text=[])
61
+
62
+ return [
63
+ CreatePdfBlock(pdf=pdf_block),
64
+ CreateParagraphBlock(paragraph=empty_para),
65
+ ]
66
+
67
+ @classmethod
68
+ def notion_to_markdown(cls, block: Block) -> Optional[str]:
69
+ if block.type != BlockType.PDF or not block.pdf:
70
+ return None
71
+
72
+ pb: FileBlock = block.pdf
73
+
74
+ # Determine URL (only external and file types are valid for Markdown)
75
+ if pb.type == FileType.EXTERNAL and pb.external:
76
+ url = pb.external.url
77
+ elif pb.type == FileType.FILE and pb.file:
78
+ url = pb.file.url
79
+ elif pb.type == FileType.FILE_UPLOAD:
80
+ # Uploaded PDF has no stable URL for Markdown
81
+ return None
82
+ else:
83
+ return None
84
+
85
+ if not pb.caption:
86
+ return f"[pdf]({url})"
87
+
88
+ caption_md = TextInlineFormatter.extract_text_with_formatting(pb.caption)
89
+ if caption_md:
90
+ return f'[pdf]({url} "{caption_md}")'
91
+ return f"[pdf]({url})"
@@ -0,0 +1,35 @@
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
+
9
+
10
+ class PdfMarkdownNodeParams(BaseModel):
11
+ url: str
12
+ caption: Optional[str] = None
13
+
14
+
15
+ class PdfMarkdownNode(MarkdownNode):
16
+ """
17
+ Programmatic interface for creating Notion-style Markdown PDF embeds.
18
+ Example: [pdf](https://example.com/document.pdf "My Caption")
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
+ """
31
+ Convert to markdown as [pdf](url "caption") or [pdf](url) if caption is empty.
32
+ """
33
+ if self.caption:
34
+ return f'[pdf]({self.url} "{self.caption}")'
35
+ return f"[pdf]({self.url})"
@@ -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,34 @@
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.models import Block, BlockCreateResult, BlockType
8
+ from notionary.blocks.quote.quote_models import CreateQuoteBlock, QuoteBlock
9
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
10
+ from notionary.blocks.types import BlockColor
3
11
 
4
- from notionary.blocks import NotionBlockElement, NotionBlockResult
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
6
12
 
7
- class QuoteElement(NotionBlockElement):
13
+ class QuoteElement(BaseBlockElement):
8
14
  """
9
15
  Handles conversion between Markdown quotes and Notion quote blocks.
16
+
10
17
  Markdown quote syntax:
11
18
  - [quote](Simple quote text)
19
+
20
+ Only single-line quotes without author metadata.
12
21
  """
13
22
 
14
- # Einzeilig, kein Author, kein Anführungszeichen-Kram mehr!
15
- PATTERN = re.compile(r'^\[quote\]\(([^\n\r]+)\)$')
23
+ PATTERN = re.compile(r"^\[quote\]\(([^\n\r]+)\)$")
16
24
 
17
25
  @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())
26
+ def match_notion(cls, block: Block) -> bool:
27
+ return block.type == BlockType.QUOTE and block.quote
22
28
 
23
29
  @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
-
30
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
+ """Convert markdown quote to Notion QuoteBlock."""
32
32
  match = cls.PATTERN.match(text.strip())
33
33
  if not match:
34
34
  return None
@@ -37,56 +37,22 @@ class QuoteElement(NotionBlockElement):
37
37
  if not content:
38
38
  return None
39
39
 
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":
46
- return None
47
-
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
40
+ # Parse inline formatting into rich text objects
41
+ rich_text = TextInlineFormatter.parse_inline_formatting(content)
52
42
 
53
- return f"[quote]({content.strip()})"
43
+ # Return a typed QuoteBlock
44
+ quote_content = QuoteBlock(rich_text=rich_text, color=BlockColor.DEFAULT)
45
+ return CreateQuoteBlock(quote=quote_content)
54
46
 
55
47
  @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
48
+ def notion_to_markdown(cls, block: Block) -> Optional[str]:
49
+ if block.type != BlockType.QUOTE or not block.quote:
50
+ return None
64
51
 
65
- @classmethod
66
- def is_multiline(cls) -> bool:
67
- return False
52
+ rich = block.quote.rich_text
53
+ text = TextInlineFormatter.extract_text_with_formatting(rich)
68
54
 
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
- )
55
+ if not text.strip():
56
+ return None
76
57
 
77
- @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()
92
- )
58
+ return f"[quote]({text.strip()})"
@@ -1,11 +1,14 @@
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.
@@ -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"]