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,94 @@
1
+ from enum import Enum
2
+ from typing import Literal
3
+
4
+ from pydantic import BaseModel, ConfigDict, Field
5
+
6
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
7
+
8
+
9
+ class CodeLanguage(str, Enum):
10
+ ABAP = "abap"
11
+ ARDUINO = "arduino"
12
+ BASH = "bash"
13
+ BASIC = "basic"
14
+ C = "c"
15
+ CLOJURE = "clojure"
16
+ COFFEESCRIPT = "coffeescript"
17
+ CPP = "c++"
18
+ CSHARP = "c#"
19
+ CSS = "css"
20
+ DART = "dart"
21
+ DIFF = "diff"
22
+ DOCKER = "docker"
23
+ ELIXIR = "elixir"
24
+ ELM = "elm"
25
+ ERLANG = "erlang"
26
+ FLOW = "flow"
27
+ FORTRAN = "fortran"
28
+ FSHARP = "f#"
29
+ GHERKIN = "gherkin"
30
+ GLSL = "glsl"
31
+ GO = "go"
32
+ GRAPHQL = "graphql"
33
+ GROOVY = "groovy"
34
+ HASKELL = "haskell"
35
+ HTML = "html"
36
+ JAVA = "java"
37
+ JAVASCRIPT = "javascript"
38
+ JSON = "json"
39
+ JULIA = "julia"
40
+ KOTLIN = "kotlin"
41
+ LATEX = "latex"
42
+ LESS = "less"
43
+ LISP = "lisp"
44
+ LIVESCRIPT = "livescript"
45
+ LUA = "lua"
46
+ MAKEFILE = "makefile"
47
+ MARKDOWN = "markdown"
48
+ MARKUP = "markup"
49
+ MATLAB = "matlab"
50
+ MERMAID = "mermaid"
51
+ NIX = "nix"
52
+ OBJECTIVE_C = "objective-c"
53
+ OCAML = "ocaml"
54
+ PASCAL = "pascal"
55
+ PERL = "perl"
56
+ PHP = "php"
57
+ PLAIN_TEXT = "plain text"
58
+ POWERSHELL = "powershell"
59
+ PROLOG = "prolog"
60
+ PROTOBUF = "protobuf"
61
+ PYTHON = "python"
62
+ R = "r"
63
+ REASON = "reason"
64
+ RUBY = "ruby"
65
+ RUST = "rust"
66
+ SASS = "sass"
67
+ SCALA = "scala"
68
+ SCHEME = "scheme"
69
+ SCSS = "scss"
70
+ SHELL = "shell"
71
+ SQL = "sql"
72
+ SWIFT = "swift"
73
+ TYPESCRIPT = "typescript"
74
+ VB_NET = "vb.net"
75
+ VERILOG = "verilog"
76
+ VHDL = "vhdl"
77
+ VISUAL_BASIC = "visual basic"
78
+ WEBASSEMBLY = "webassembly"
79
+ XML = "xml"
80
+ YAML = "yaml"
81
+ JAVA_C_CPP_CSHARP = "java/c/c++/c#"
82
+
83
+
84
+ class CodeBlock(BaseModel):
85
+ caption: list[RichTextObject] = Field(default_factory=list)
86
+ rich_text: list[RichTextObject]
87
+ language: CodeLanguage = CodeLanguage.PLAIN_TEXT
88
+
89
+ model_config = ConfigDict(arbitrary_types_allowed=True)
90
+
91
+
92
+ class CreateCodeBlock(BaseModel):
93
+ type: Literal["code"] = "code"
94
+ code: CodeBlock
@@ -1,5 +1,29 @@
1
- from .column_element import ColumnElement
1
+ from notionary.blocks.column.column_element import ColumnElement
2
+ from notionary.blocks.column.column_list_element import ColumnListElement
3
+ from notionary.blocks.column.column_list_markdown_node import (
4
+ ColumnListMarkdownBlockParams,
5
+ ColumnListMarkdownNode,
6
+ )
7
+ from notionary.blocks.column.column_markdown_node import (
8
+ ColumnMarkdownBlockParams,
9
+ ColumnMarkdownNode,
10
+ )
11
+ from notionary.blocks.column.column_models import (
12
+ ColumnBlock,
13
+ ColumnListBlock,
14
+ CreateColumnBlock,
15
+ CreateColumnListBlock,
16
+ )
2
17
 
3
18
  __all__ = [
4
19
  "ColumnElement",
20
+ "ColumnListElement",
21
+ "ColumnBlock",
22
+ "CreateColumnBlock",
23
+ "ColumnListBlock",
24
+ "CreateColumnListBlock",
25
+ "ColumnMarkdownNode",
26
+ "ColumnMarkdownBlockParams",
27
+ "ColumnListMarkdownNode",
28
+ "ColumnListMarkdownBlockParams",
5
29
  ]
@@ -1,333 +1,65 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Optional, Callable
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.column.column_models import ColumnBlock, CreateColumnBlock
8
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
9
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
10
10
 
11
11
 
12
- class ColumnElement(NotionBlockElement):
12
+ class ColumnElement(BaseBlockElement):
13
13
  """
14
- Handles conversion between custom Markdown column syntax and Notion column blocks.
15
-
16
- Markdown column syntax:
17
- ::: columns
18
- ::: column
19
- Content for first column
20
- :::
21
- ::: column
22
- Content for second column
23
- :::
24
- :::
14
+ Handles individual `::: column` blocks with optional width ratio.
15
+ Content is automatically added by the stack processor.
25
16
 
26
- This creates a column layout in Notion with the specified content in each column.
17
+ Supported syntax:
18
+ - `::: column` (equal width)
19
+ - `::: column 0.5` (50% width)
20
+ - `::: column 0.25` (25% width)
27
21
  """
28
22
 
29
- COLUMNS_START = re.compile(r"^:::\s*columns\s*$")
30
- COLUMN_START = re.compile(r"^:::\s*column\s*$")
31
- BLOCK_END = re.compile(r"^:::\s*$")
32
-
33
- _converter_callback = None
23
+ COLUMN_START = re.compile(r"^:::\s*column(?:\s+(0?\.\d+|1\.0?))?\s*$")
34
24
 
35
25
  @classmethod
36
- def set_converter_callback(
37
- cls, callback: Callable[[str], list[dict[str, any]]]
38
- ) -> None:
39
- """
40
- Setze die Callback-Funktion, die zum Konvertieren von Markdown zu Notion-Blöcken verwendet wird.
41
-
42
- Args:
43
- callback: Funktion, die Markdown-Text annimmt und eine Liste von Notion-Blöcken zurückgibt
44
- """
45
- cls._converter_callback = callback
46
-
47
- @staticmethod
48
- def match_markdown(text: str) -> bool:
49
- """Check if text starts a columns block."""
50
- return bool(ColumnElement.COLUMNS_START.match(text.strip()))
51
-
52
- @staticmethod
53
- def match_notion(block: dict[str, any]) -> bool:
54
- """Check if block is a Notion column_list."""
55
- return block.get("type") == "column_list"
56
-
57
- @staticmethod
58
- def markdown_to_notion(text: str) -> NotionBlockResult:
59
- """
60
- Convert markdown column syntax to Notion column blocks.
61
-
62
- Note: This only processes the first line (columns start).
63
- The full column content needs to be processed separately.
64
- """
65
- if not ColumnElement.COLUMNS_START.match(text.strip()):
66
- return None
67
-
68
- # Create an empty column_list block
69
- # Child columns will be added by the column processor
70
- return [{"type": "column_list", "column_list": {"children": []}}]
71
-
72
- @staticmethod
73
- def notion_to_markdown(block: dict[str, any]) -> Optional[str]:
74
- """Convert Notion column_list block to markdown column syntax."""
75
- if block.get("type") != "column_list":
76
- return None
77
-
78
- column_children = block.get("column_list", {}).get("children", [])
79
-
80
- # Start the columns block
81
- result = ["::: columns"]
82
-
83
- # Process each column
84
- for column_block in column_children:
85
- if column_block.get("type") == "column":
86
- result.append("::: column")
87
-
88
- for _ in column_block.get("column", {}).get("children", []):
89
- result.append(" [Column content]") # Placeholder
90
-
91
- result.append(":::")
92
-
93
- # End the columns block
94
- result.append(":::")
95
-
96
- return "\n".join(result)
97
-
98
- @staticmethod
99
- def is_multiline() -> bool:
100
- """Column blocks span multiple lines."""
101
- return True
102
-
103
- @classmethod
104
- def find_matches(
105
- cls, text: str, converter_callback: Optional[Callable] = None
106
- ) -> list[tuple[int, int, dict[str, any]]]:
107
- """
108
- Find all column block matches in the text and return their positions and blocks.
109
-
110
- Args:
111
- text: The input markdown text
112
- converter_callback: Optional callback to convert nested content
113
-
114
- Returns:
115
- List of tuples (start_pos, end_pos, block)
116
- """
117
- # Wenn ein Callback übergeben wurde, nutze diesen, sonst die gespeicherte Referenz
118
- converter = converter_callback or cls._converter_callback
119
- if not converter:
120
- raise ValueError(
121
- "No converter callback provided for ColumnElement. Call set_converter_callback first or provide converter_callback parameter."
122
- )
123
-
124
- matches = []
125
- lines = text.split("\n")
126
- i = 0
127
-
128
- while i < len(lines):
129
- # Skip non-column lines
130
- if not ColumnElement.COLUMNS_START.match(lines[i].strip()):
131
- i += 1
132
- continue
133
-
134
- # Process a column block and add to matches
135
- column_block_info = cls._process_column_block(
136
- lines=lines, start_index=i, converter_callback=converter
137
- )
138
- matches.append(column_block_info)
139
-
140
- # Skip to the end of the processed column block
141
- i = column_block_info[3] # i is returned as the 4th element in the tuple
142
-
143
- return [(start, end, block) for start, end, block, _ in matches]
26
+ def match_notion(cls, block: Block) -> bool:
27
+ """Check if block is a Notion column."""
28
+ return block.type == BlockType.COLUMN and block.column
144
29
 
145
30
  @classmethod
146
- def _process_column_block(
147
- cls, lines: list[str], start_index: int, converter_callback: Callable
148
- ) -> tuple[int, int, dict[str, any], int]:
149
- """
150
- Process a complete column block structure from the given starting line.
151
-
152
- Args:
153
- lines: All lines of the text
154
- start_index: Index of the column block start line
155
- converter_callback: Callback function to convert markdown to notion blocks
156
-
157
- Returns:
158
- Tuple of (start_pos, end_pos, block, next_line_index)
159
- """
160
- columns_start = start_index
161
- columns_blocks = cls.markdown_to_notion(lines[start_index].strip())
162
- columns_block = columns_blocks[0] if columns_blocks else None
163
- columns_children = []
164
-
165
- next_index = cls._collect_columns(
166
- lines, start_index + 1, columns_children, converter_callback
167
- )
31
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
32
+ """Convert `::: column [ratio]` to Notion ColumnBlock."""
33
+ if not (match := cls.COLUMN_START.match(text.strip())):
34
+ return None
168
35
 
169
- # Add columns to the main block
170
- if columns_children and columns_block:
171
- columns_block["column_list"]["children"] = columns_children
36
+ ratio_str = match.group(1)
37
+ width_ratio = None
172
38
 
173
- # Calculate positions
174
- start_pos = sum(len(lines[j]) + 1 for j in range(columns_start))
175
- end_pos = sum(len(lines[j]) + 1 for j in range(next_index))
39
+ if ratio_str:
40
+ try:
41
+ width_ratio = float(ratio_str)
42
+ # Validate ratio is between 0 and 1
43
+ if not (0 < width_ratio <= 1.0):
44
+ width_ratio = None # Invalid ratio, use default
45
+ except ValueError:
46
+ width_ratio = None # Invalid format, use default
176
47
 
177
- return (start_pos, end_pos, columns_block, next_index)
48
+ column_content = ColumnBlock(width_ratio=width_ratio)
49
+ return CreateColumnBlock(column=column_content)
178
50
 
179
51
  @classmethod
180
- def _collect_columns(
181
- cls,
182
- lines: list[str],
183
- start_index: int,
184
- columns_children: list[dict[str, any]],
185
- converter_callback: Callable,
186
- ) -> int:
187
- """
188
- Collect all columns within a column block structure.
189
-
190
- Args:
191
- lines: All lines of the text
192
- start_index: Index to start collecting from
193
- columns_children: List to append collected columns to
194
- converter_callback: Callback function to convert column content
195
-
196
- Returns:
197
- Next line index after all columns have been processed
198
- """
199
- i = start_index
200
- in_column = False
201
- column_content = []
202
-
203
- while i < len(lines):
204
- current_line = lines[i].strip()
205
-
206
- if cls.COLUMNS_START.match(current_line):
207
- break
208
-
209
- if cls.COLUMN_START.match(current_line):
210
- cls._finalize_column(
211
- column_content, columns_children, in_column, converter_callback
212
- )
213
- column_content = []
214
- in_column = True
215
- i += 1
216
- continue
52
+ async def notion_to_markdown(cls, block: Block) -> str:
53
+ """Convert Notion column to markdown."""
54
+ if not cls.match_notion(block):
55
+ return ""
217
56
 
218
- if cls.BLOCK_END.match(current_line) and in_column:
219
- cls._finalize_column(
220
- column_content, columns_children, in_column, converter_callback
221
- )
222
- column_content = []
223
- in_column = False
224
- i += 1
225
- continue
226
-
227
- if cls.BLOCK_END.match(current_line) and not in_column:
228
- i += 1
229
- break
230
-
231
- if in_column:
232
- column_content.append(lines[i])
233
-
234
- i += 1
235
-
236
- cls._finalize_column(
237
- column_content, columns_children, in_column, converter_callback
238
- )
239
-
240
- return i
241
-
242
- @staticmethod
243
- def _finalize_column(
244
- column_content: list[str],
245
- columns_children: list[dict[str, any]],
246
- in_column: bool,
247
- converter_callback: Callable,
248
- ) -> None:
249
- """
250
- Finalize a column by processing its content and adding it to the columns_children list.
251
-
252
- Args:
253
- column_content: Content lines of the column
254
- columns_children: List to append the column block to
255
- in_column: Whether we're currently in a column (if False, does nothing)
256
- converter_callback: Callback function to convert column content
257
- """
258
- if not (in_column and column_content):
259
- return
260
-
261
- processed_content = ColumnElement._preprocess_column_content(column_content)
262
-
263
- column_blocks = converter_callback("\n".join(processed_content))
264
-
265
- # Create column block
266
- column_block = {"type": "column", "column": {"children": column_blocks}}
267
- columns_children.append(column_block)
268
-
269
- @classmethod
270
- def is_multiline(cls) -> bool:
271
- """Column blocks span multiple lines."""
272
- return True
57
+ if not block.column.width_ratio:
58
+ return "::: column"
273
59
 
274
- @staticmethod
275
- def _preprocess_column_content(lines: list[str]) -> list[str]:
276
- """Remove all spacer markers from column content."""
277
- return [line for line in lines if line.strip() != "---spacer---"]
60
+ return f"::: column {block.column.width_ratio}"
278
61
 
279
62
  @classmethod
280
- def get_llm_prompt_content(cls) -> ElementPromptContent:
281
- """
282
- Returns structured LLM prompt metadata for the column layout element.
283
- """
284
- return (
285
- ElementPromptBuilder()
286
- .with_description(
287
- "Creates a multi-column layout that displays content side by side."
288
- )
289
- .with_usage_guidelines(
290
- "Use columns sparingly, only for direct comparisons or when parallel presentation significantly improves readability. "
291
- "Best for pros/cons lists, feature comparisons, or pairing images with descriptions."
292
- )
293
- .with_avoidance_guidelines(
294
- "Avoid overusing as it can complicate document structure. Do not use for simple content that works better in linear format."
295
- )
296
- .with_syntax(
297
- "::: columns\n"
298
- "::: column\n"
299
- "Content for first column\n"
300
- ":::\n"
301
- "::: column\n"
302
- "Content for second column\n"
303
- ":::\n"
304
- ":::"
305
- )
306
- .with_examples(
307
- [
308
- "::: columns\n"
309
- "::: column\n"
310
- "## Features\n"
311
- "- Fast response time\n"
312
- "- Intuitive interface\n"
313
- "- Regular updates\n"
314
- ":::\n"
315
- "::: column\n"
316
- "## Benefits\n"
317
- "- Increased productivity\n"
318
- "- Better collaboration\n"
319
- "- Simplified workflows\n"
320
- ":::\n"
321
- ":::",
322
- "::: columns\n"
323
- "::: column\n"
324
- "![Image placeholder](/api/placeholder/400/320)\n"
325
- ":::\n"
326
- "::: column\n"
327
- "This text appears next to the image, creating a media-with-caption style layout.\n"
328
- ":::\n"
329
- ":::",
330
- ]
331
- )
332
- .build()
333
- )
63
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
64
+ """Column elements are documented via ColumnListElement - return None to avoid duplication."""
65
+ return None
@@ -0,0 +1,52 @@
1
+ from typing import Optional
2
+ import re
3
+
4
+ from notionary.blocks.base_block_element import BaseBlockElement
5
+ from notionary.blocks.column.column_models import ColumnListBlock, CreateColumnListBlock
6
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
7
+ from notionary.blocks.models import Block, BlockCreateResult
8
+ from notionary.blocks.types import BlockType
9
+
10
+
11
+ class ColumnListElement(BaseBlockElement):
12
+ """
13
+ Handles the `::: columns` container.
14
+ Individual columns are handled by ColumnElement.
15
+ """
16
+
17
+ COLUMNS_START = re.compile(r"^:::\s*columns\s*$")
18
+
19
+ @classmethod
20
+ def match_markdown(cls, text: str) -> bool:
21
+ """Check if text starts a columns container."""
22
+ return bool(cls.COLUMNS_START.match(text.strip()))
23
+
24
+ @classmethod
25
+ def match_notion(cls, block: Block) -> bool:
26
+ """Check if block is a Notion column_list."""
27
+ return block.type == BlockType.COLUMN_LIST and block.column_list
28
+
29
+ @classmethod
30
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
31
+ """Convert `::: columns` to Notion ColumnListBlock."""
32
+ if not cls.COLUMNS_START.match(text.strip()):
33
+ return None
34
+
35
+ # Empty ColumnListBlock - children (columns) added by stack processor
36
+ column_list_content = ColumnListBlock()
37
+ return CreateColumnListBlock(column_list=column_list_content)
38
+
39
+ @classmethod
40
+ @classmethod
41
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
42
+ """Get system prompt information for column list blocks."""
43
+ return BlockElementMarkdownInformation(
44
+ block_type=cls.__name__,
45
+ description="Column list containers organize multiple columns in side-by-side layouts",
46
+ syntax_examples=[
47
+ "::: columns\n::: column\nContent 1\n:::\n::: column\nContent 2\n:::\n:::",
48
+ "::: columns\n::: column 0.6\nMain content\n:::\n::: column 0.4\nSidebar\n:::\n:::",
49
+ "::: columns\n::: column 0.25\nLeft\n:::\n::: column 0.5\nCenter\n:::\n::: column 0.25\nRight\n:::\n:::",
50
+ ],
51
+ usage_guidelines="Use to create multi-column layouts with at least 2 columns. Column width ratios must add up to 1.0 when specified. Each column can contain any block content. Ends with :::.",
52
+ )
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.column.column_markdown_node import ColumnMarkdownNode
6
+ from notionary.markdown.markdown_document_model import MarkdownBlock
7
+ from notionary.markdown.markdown_node import MarkdownNode
8
+
9
+
10
+ class ColumnListMarkdownBlockParams(BaseModel):
11
+ columns: list[list[MarkdownBlock]]
12
+ model_config = {"arbitrary_types_allowed": True}
13
+
14
+
15
+ class ColumnListMarkdownNode(MarkdownNode):
16
+ """
17
+ Programmatic interface for creating a Markdown column list container.
18
+ This represents the `::: columns` container that holds multiple columns.
19
+
20
+ Example:
21
+ ::: columns
22
+ ::: column
23
+ Left content
24
+ with nested lines
25
+ :::
26
+
27
+ ::: column 0.3
28
+ Right content (30% width)
29
+ with nested lines
30
+ :::
31
+ :::
32
+ """
33
+
34
+ def __init__(self, columns: list[ColumnMarkdownNode]):
35
+ self.columns = columns
36
+
37
+ @classmethod
38
+ def from_params(
39
+ cls, params: ColumnListMarkdownBlockParams
40
+ ) -> ColumnListMarkdownNode:
41
+ return cls(columns=params.columns)
42
+
43
+ def to_markdown(self) -> str:
44
+ if not self.columns:
45
+ return "::: columns\n:::"
46
+
47
+ column_parts = [column.to_markdown() for column in self.columns]
48
+ columns_content = "\n\n".join(column_parts)
49
+
50
+ return f"::: columns\n{columns_content}\n:::"
@@ -0,0 +1,59 @@
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 ColumnMarkdownBlockParams(BaseModel):
11
+ children: list[MarkdownNode]
12
+ width_ratio: Optional[float] = None
13
+ model_config = {"arbitrary_types_allowed": True}
14
+
15
+
16
+ class ColumnMarkdownNode(MarkdownNode):
17
+ """
18
+ Programmatic interface for creating a single Markdown column block
19
+ with nested content and optional width ratio.
20
+
21
+ Example:
22
+ ::: column
23
+ # Column Title
24
+
25
+ Some content here
26
+ :::
27
+
28
+ ::: column 0.7
29
+ # Wide Column (70%)
30
+
31
+ This column takes 70% width
32
+ :::
33
+ """
34
+
35
+ def __init__(
36
+ self, children: list[MarkdownNode], width_ratio: Optional[float] = None
37
+ ):
38
+ self.children = children
39
+ self.width_ratio = width_ratio
40
+
41
+ @classmethod
42
+ def from_params(cls, params: ColumnMarkdownBlockParams) -> ColumnMarkdownNode:
43
+ return cls(children=params.children, width_ratio=params.width_ratio)
44
+
45
+ def to_markdown(self) -> str:
46
+ # Start tag with optional width ratio
47
+ if self.width_ratio is not None:
48
+ start_tag = f"::: column {self.width_ratio}"
49
+ else:
50
+ start_tag = "::: column"
51
+
52
+ if not self.children:
53
+ return f"{start_tag}\n:::"
54
+
55
+ # Convert children to markdown
56
+ content_parts = [child.to_markdown() for child in self.children]
57
+ content_text = "\n\n".join(content_parts)
58
+
59
+ return f"{start_tag}\n{content_text}\n:::"
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal, Optional
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from notionary.blocks.models import BlockCreateRequest
8
+
9
+
10
+ class ColumnBlock(BaseModel):
11
+ width_ratio: Optional[float] = None
12
+ children: list[BlockCreateRequest] = Field(default_factory=list)
13
+
14
+
15
+ class CreateColumnBlock(BaseModel):
16
+ type: Literal["column"] = "column"
17
+ column: ColumnBlock
18
+
19
+
20
+ class ColumnListBlock(BaseModel):
21
+ children: list[CreateColumnBlock] = Field(default_factory=list)
22
+
23
+
24
+ class CreateColumnListBlock(BaseModel):
25
+ type: Literal["column_list"] = "column_list"
26
+ column_list: ColumnListBlock