notionary 0.2.17__py3-none-any.whl → 0.2.19__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.
- notionary/__init__.py +3 -2
- notionary/blocks/__init__.py +54 -25
- notionary/blocks/audio/__init__.py +7 -0
- notionary/blocks/audio/audio_element.py +152 -0
- notionary/blocks/audio/audio_markdown_node.py +29 -0
- notionary/blocks/audio/audio_models.py +59 -0
- notionary/blocks/bookmark/__init__.py +7 -0
- notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
- notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
- notionary/blocks/bookmark/bookmark_models.py +0 -0
- notionary/blocks/bulleted_list/__init__.py +7 -0
- notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
- notionary/blocks/callout/__init__.py +7 -0
- notionary/blocks/callout/callout_element.py +132 -0
- notionary/blocks/callout/callout_markdown_node.py +31 -0
- notionary/blocks/callout/callout_models.py +0 -0
- notionary/blocks/code/__init__.py +7 -0
- notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
- notionary/blocks/code/code_markdown_node.py +43 -0
- notionary/blocks/code/code_models.py +0 -0
- notionary/blocks/column/__init__.py +5 -0
- notionary/blocks/{column_element.py → column/column_element.py} +24 -55
- notionary/blocks/column/column_models.py +0 -0
- notionary/blocks/divider/__init__.py +7 -0
- notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
- notionary/blocks/divider/divider_markdown_node.py +24 -0
- notionary/blocks/divider/divider_models.py +0 -0
- notionary/blocks/document/__init__.py +7 -0
- notionary/blocks/document/document_element.py +102 -0
- notionary/blocks/document/document_markdown_node.py +31 -0
- notionary/blocks/document/document_models.py +0 -0
- notionary/blocks/embed/__init__.py +7 -0
- notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
- notionary/blocks/embed/embed_markdown_node.py +30 -0
- notionary/blocks/embed/embed_models.py +0 -0
- notionary/blocks/heading/__init__.py +7 -0
- notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
- notionary/blocks/heading/heading_markdown_node.py +29 -0
- notionary/blocks/heading/heading_models.py +0 -0
- notionary/blocks/image/__init__.py +7 -0
- notionary/blocks/{image_element.py → image/image_element.py} +62 -42
- notionary/blocks/image/image_markdown_node.py +33 -0
- notionary/blocks/image/image_models.py +0 -0
- notionary/blocks/markdown_builder.py +356 -0
- notionary/blocks/markdown_node.py +29 -0
- notionary/blocks/mention/__init__.py +7 -0
- notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
- notionary/blocks/mention/mention_markdown_node.py +38 -0
- notionary/blocks/mention/mention_models.py +0 -0
- notionary/blocks/numbered_list/__init__.py +7 -0
- notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
- notionary/blocks/numbered_list/numbered_list_models.py +0 -0
- notionary/blocks/paragraph/__init__.py +7 -0
- notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
- notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
- notionary/blocks/paragraph/paragraph_models.py +0 -0
- notionary/blocks/quote/__init__.py +7 -0
- notionary/blocks/quote/quote_element.py +92 -0
- notionary/blocks/quote/quote_markdown_node.py +23 -0
- notionary/blocks/quote/quote_models.py +0 -0
- notionary/blocks/registry/block_registry.py +17 -3
- notionary/blocks/registry/block_registry_builder.py +90 -178
- notionary/blocks/shared/__init__.py +0 -0
- notionary/blocks/shared/block_client.py +256 -0
- notionary/blocks/shared/models.py +713 -0
- notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
- notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
- notionary/blocks/shared/text_inline_formatter_new.py +139 -0
- notionary/blocks/table/__init__.py +7 -0
- notionary/blocks/{table_element.py → table/table_element.py} +23 -11
- notionary/blocks/table/table_markdown_node.py +40 -0
- notionary/blocks/table/table_models.py +0 -0
- notionary/blocks/todo/__init__.py +7 -0
- notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
- notionary/blocks/todo/todo_markdown_node.py +31 -0
- notionary/blocks/todo/todo_models.py +0 -0
- notionary/blocks/toggle/__init__.py +4 -0
- notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
- notionary/blocks/toggle/toggle_markdown_node.py +35 -0
- notionary/blocks/toggle/toggle_models.py +0 -0
- notionary/blocks/toggleable_heading/__init__.py +9 -0
- notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
- notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
- notionary/blocks/video/__init__.py +7 -0
- notionary/blocks/{video_element.py → video/video_element.py} +82 -57
- notionary/blocks/video/video_markdown_node.py +30 -0
- notionary/file_upload/notion_file_upload.py +1 -1
- notionary/page/content/markdown_whitespace_processor.py +80 -0
- notionary/page/content/notion_text_length_utils.py +87 -0
- notionary/page/content/page_content_retriever.py +18 -10
- notionary/page/content/page_content_writer.py +97 -148
- notionary/page/formatting/line_processor.py +153 -0
- notionary/page/formatting/markdown_to_notion_converter.py +104 -425
- notionary/page/notion_page.py +9 -11
- notionary/page/notion_to_markdown_converter.py +9 -13
- notionary/util/factory_decorator.py +0 -0
- notionary/workspace.py +0 -1
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/METADATA +1 -1
- notionary-0.2.19.dist-info/RECORD +150 -0
- notionary/blocks/audio_element.py +0 -144
- notionary/blocks/callout_element.py +0 -122
- notionary/blocks/document_element.py +0 -194
- notionary/blocks/notion_block_client.py +0 -26
- notionary/blocks/qoute_element.py +0 -169
- notionary/page/content/notion_page_content_chunker.py +0 -84
- notionary/page/formatting/spacer_rules.py +0 -483
- notionary-0.2.17.dist-info/RECORD +0 -85
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/LICENSE +0 -0
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/WHEEL +0 -0
@@ -1,18 +1,21 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Optional, Any, TypeAlias, Union
|
2
2
|
from abc import ABC
|
3
3
|
|
4
4
|
from notionary.blocks.prompts.element_prompt_content import ElementPromptContent
|
5
5
|
|
6
|
+
NotionBlock: TypeAlias = dict[str, Any]
|
7
|
+
NotionBlockResult: TypeAlias = Optional[Union[list[dict[str, Any]], dict[str, Any]]]
|
8
|
+
|
6
9
|
|
7
10
|
class NotionBlockElement(ABC):
|
8
11
|
"""Base class for elements that can be converted between Markdown and Notion."""
|
9
12
|
|
10
13
|
@classmethod
|
11
|
-
def markdown_to_notion(cls, text: str) ->
|
12
|
-
"""Convert markdown to Notion block."""
|
14
|
+
def markdown_to_notion(cls, text: str) -> NotionBlockResult:
|
15
|
+
"""Convert markdown to Notion blocks (can return multiple blocks or single block)."""
|
13
16
|
|
14
17
|
@classmethod
|
15
|
-
def notion_to_markdown(cls, block:
|
18
|
+
def notion_to_markdown(cls, block: dict[str, any]) -> Optional[str]:
|
16
19
|
"""Convert Notion block to markdown."""
|
17
20
|
|
18
21
|
@classmethod
|
@@ -21,7 +24,7 @@ class NotionBlockElement(ABC):
|
|
21
24
|
return bool(cls.markdown_to_notion(text)) # Now calls the class's version
|
22
25
|
|
23
26
|
@classmethod
|
24
|
-
def match_notion(cls, block:
|
27
|
+
def match_notion(cls, block: dict[str, any]) -> bool:
|
25
28
|
"""Check if this element can handle the given Notion block."""
|
26
29
|
return bool(cls.notion_to_markdown(block)) # Now calls the class's version
|
27
30
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Any
|
2
2
|
import re
|
3
3
|
|
4
4
|
from notionary.blocks import ElementPromptBuilder, ElementPromptContent
|
@@ -30,7 +30,7 @@ class TextInlineFormatter:
|
|
30
30
|
]
|
31
31
|
|
32
32
|
@classmethod
|
33
|
-
def parse_inline_formatting(cls, text: str) ->
|
33
|
+
def parse_inline_formatting(cls, text: str) -> list[dict[str, Any]]:
|
34
34
|
"""
|
35
35
|
Parse inline text formatting into Notion rich_text format.
|
36
36
|
|
@@ -38,7 +38,7 @@ class TextInlineFormatter:
|
|
38
38
|
text: Markdown text with inline formatting
|
39
39
|
|
40
40
|
Returns:
|
41
|
-
|
41
|
+
list of Notion rich_text objects
|
42
42
|
"""
|
43
43
|
if not text:
|
44
44
|
return []
|
@@ -47,17 +47,17 @@ class TextInlineFormatter:
|
|
47
47
|
|
48
48
|
@classmethod
|
49
49
|
def _split_text_into_segments(
|
50
|
-
cls, text: str, format_patterns:
|
51
|
-
) ->
|
50
|
+
cls, text: str, format_patterns: list[tuple]
|
51
|
+
) -> list[dict[str, Any]]:
|
52
52
|
"""
|
53
53
|
Split text into segments by formatting markers and convert to Notion rich_text format.
|
54
54
|
|
55
55
|
Args:
|
56
56
|
text: Text to split
|
57
|
-
format_patterns:
|
57
|
+
format_patterns: list of (regex pattern, formatting dict) tuples
|
58
58
|
|
59
59
|
Returns:
|
60
|
-
|
60
|
+
list of Notion rich_text objects
|
61
61
|
"""
|
62
62
|
segments = []
|
63
63
|
remaining_text = text
|
@@ -107,8 +107,8 @@ class TextInlineFormatter:
|
|
107
107
|
|
108
108
|
@classmethod
|
109
109
|
def _create_text_element(
|
110
|
-
cls, text: str, formatting:
|
111
|
-
) ->
|
110
|
+
cls, text: str, formatting: dict[str, Any]
|
111
|
+
) -> dict[str, Any]:
|
112
112
|
"""
|
113
113
|
Create a Notion text element with formatting.
|
114
114
|
|
@@ -136,7 +136,7 @@ class TextInlineFormatter:
|
|
136
136
|
}
|
137
137
|
|
138
138
|
@classmethod
|
139
|
-
def _create_link_element(cls, text: str, url: str) ->
|
139
|
+
def _create_link_element(cls, text: str, url: str) -> dict[str, Any]:
|
140
140
|
"""
|
141
141
|
Create a Notion link element.
|
142
142
|
|
@@ -155,7 +155,7 @@ class TextInlineFormatter:
|
|
155
155
|
}
|
156
156
|
|
157
157
|
@classmethod
|
158
|
-
def _create_mention_element(cls, id: str) ->
|
158
|
+
def _create_mention_element(cls, id: str) -> dict[str, Any]:
|
159
159
|
"""
|
160
160
|
Create a Notion mention element.
|
161
161
|
|
@@ -172,12 +172,12 @@ class TextInlineFormatter:
|
|
172
172
|
}
|
173
173
|
|
174
174
|
@classmethod
|
175
|
-
def extract_text_with_formatting(cls, rich_text:
|
175
|
+
def extract_text_with_formatting(cls, rich_text: list[dict[str, Any]]) -> str:
|
176
176
|
"""
|
177
177
|
Convert Notion rich_text elements back to Markdown formatted text.
|
178
178
|
|
179
179
|
Args:
|
180
|
-
rich_text:
|
180
|
+
rich_text: list of Notion rich_text elements
|
181
181
|
|
182
182
|
Returns:
|
183
183
|
Markdown formatted text
|
@@ -214,7 +214,7 @@ class TextInlineFormatter:
|
|
214
214
|
return "".join(formatted_parts)
|
215
215
|
|
216
216
|
@classmethod
|
217
|
-
def _default_annotations(cls) ->
|
217
|
+
def _default_annotations(cls) -> dict[str, bool]:
|
218
218
|
"""
|
219
219
|
Create default annotations object.
|
220
220
|
|
@@ -0,0 +1,139 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
import re
|
3
|
+
|
4
|
+
# TODO: Use this inline formatting here
|
5
|
+
from notionary.blocks.shared.models import (
|
6
|
+
MentionRichText,
|
7
|
+
RichTextObject,
|
8
|
+
TextAnnotations,
|
9
|
+
TextContent,
|
10
|
+
)
|
11
|
+
|
12
|
+
FORMAT_PATTERNS = [
|
13
|
+
(r"\*\*(.+?)\*\*", {"bold": True}),
|
14
|
+
(r"\*(.+?)\*", {"italic": True}),
|
15
|
+
(r"_(.+?)_", {"italic": True}),
|
16
|
+
(r"__(.+?)__", {"underline": True}),
|
17
|
+
(r"~~(.+?)~~", {"strikethrough": True}),
|
18
|
+
(r"`(.+?)`", {"code": True}),
|
19
|
+
(r"\[(.+?)\]\((.+?)\)", {"link": True}),
|
20
|
+
(r"@\[([0-9a-f-]+)\]", {"mention": True}),
|
21
|
+
]
|
22
|
+
|
23
|
+
|
24
|
+
def parse_inline_formatting(text: str) -> list[dict[str, any]]:
|
25
|
+
"""Parse inline text formatting into Notion rich_text format."""
|
26
|
+
if not text:
|
27
|
+
return []
|
28
|
+
|
29
|
+
return _split_text_into_segments(text)
|
30
|
+
|
31
|
+
|
32
|
+
def _split_text_into_segments(text: str) -> list[dict[str, any]]:
|
33
|
+
"""Split text into segments by formatting markers."""
|
34
|
+
segments = []
|
35
|
+
remaining_text = text
|
36
|
+
|
37
|
+
while remaining_text:
|
38
|
+
match_info = _find_earliest_match(remaining_text)
|
39
|
+
|
40
|
+
# No more formatting found - add remaining text and exit
|
41
|
+
if not match_info:
|
42
|
+
segments.append(_create_plain_text(remaining_text))
|
43
|
+
break
|
44
|
+
|
45
|
+
match, formatting, pos = match_info
|
46
|
+
|
47
|
+
# Add text before match if exists
|
48
|
+
if pos > 0:
|
49
|
+
segments.append(_create_plain_text(remaining_text[:pos]))
|
50
|
+
|
51
|
+
# Add formatted segment
|
52
|
+
segments.append(_create_formatted_segment(match, formatting))
|
53
|
+
|
54
|
+
# Update remaining text
|
55
|
+
remaining_text = remaining_text[pos + len(match.group(0)) :]
|
56
|
+
|
57
|
+
return segments
|
58
|
+
|
59
|
+
|
60
|
+
def _find_earliest_match(text: str) -> Optional[tuple]:
|
61
|
+
"""Find the earliest formatting match in text."""
|
62
|
+
earliest_match = None
|
63
|
+
earliest_format = None
|
64
|
+
earliest_pos = len(text)
|
65
|
+
|
66
|
+
for pattern, formatting in FORMAT_PATTERNS:
|
67
|
+
match = re.search(pattern, text)
|
68
|
+
if match and match.start() < earliest_pos:
|
69
|
+
earliest_match = match
|
70
|
+
earliest_format = formatting
|
71
|
+
earliest_pos = match.start()
|
72
|
+
|
73
|
+
return (earliest_match, earliest_format, earliest_pos) if earliest_match else None
|
74
|
+
|
75
|
+
|
76
|
+
def _create_formatted_segment(match: re.Match, formatting: dict) -> dict[str, any]:
|
77
|
+
"""Create a formatted segment based on match and formatting."""
|
78
|
+
if "link" in formatting:
|
79
|
+
return _create_link_text(match.group(1), match.group(2))
|
80
|
+
elif "mention" in formatting:
|
81
|
+
return _create_mention_text(match.group(1))
|
82
|
+
else:
|
83
|
+
return _create_formatted_text(match.group(1), **formatting)
|
84
|
+
|
85
|
+
|
86
|
+
def _create_plain_text(content: str) -> dict[str, any]:
|
87
|
+
"""Create plain text rich text object."""
|
88
|
+
return RichTextObject.from_plain_text(content).model_dump()
|
89
|
+
|
90
|
+
|
91
|
+
def _create_formatted_text(content: str, **formatting) -> dict[str, any]:
|
92
|
+
"""Create formatted text rich text object."""
|
93
|
+
return RichTextObject.from_plain_text(content, **formatting).model_dump()
|
94
|
+
|
95
|
+
|
96
|
+
def _create_link_text(content: str, url: str) -> dict[str, any]:
|
97
|
+
"""Create link text rich text object."""
|
98
|
+
text_content = TextContent(content=content, link=url)
|
99
|
+
annotations = TextAnnotations()
|
100
|
+
|
101
|
+
rich_text = RichTextObject(
|
102
|
+
text=text_content, annotations=annotations, plain_text=content, href=url
|
103
|
+
)
|
104
|
+
return rich_text.model_dump()
|
105
|
+
|
106
|
+
|
107
|
+
def _create_mention_text(page_id: str) -> dict[str, any]:
|
108
|
+
"""Create mention rich text object."""
|
109
|
+
return MentionRichText.from_page_id(page_id).model_dump()
|
110
|
+
|
111
|
+
|
112
|
+
def extract_text_with_formatting(rich_text: list[dict[str, any]]) -> str:
|
113
|
+
"""Convert Notion rich_text elements back to Markdown."""
|
114
|
+
return "".join(_rich_text_to_markdown(item) for item in rich_text)
|
115
|
+
|
116
|
+
|
117
|
+
def _rich_text_to_markdown(text_obj: dict[str, any]) -> str:
|
118
|
+
"""Convert single rich text object to markdown."""
|
119
|
+
content = text_obj.get("plain_text", text_obj.get("text", {}).get("content", ""))
|
120
|
+
annotations = text_obj.get("annotations", {})
|
121
|
+
|
122
|
+
# Apply formatting in reverse order
|
123
|
+
if annotations.get("code", False):
|
124
|
+
content = f"`{content}`"
|
125
|
+
if annotations.get("strikethrough", False):
|
126
|
+
content = f"~~{content}~~"
|
127
|
+
if annotations.get("underline", False):
|
128
|
+
content = f"__{content}__"
|
129
|
+
if annotations.get("italic", False):
|
130
|
+
content = f"*{content}*"
|
131
|
+
if annotations.get("bold", False):
|
132
|
+
content = f"**{content}**"
|
133
|
+
|
134
|
+
# Handle links
|
135
|
+
link_data = text_obj.get("text", {}).get("link")
|
136
|
+
if link_data and link_data.get("url"):
|
137
|
+
content = f"[{content}]({link_data['url']})"
|
138
|
+
|
139
|
+
return content
|
@@ -1,9 +1,13 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional, List, Tuple
|
3
3
|
|
4
|
-
from notionary.blocks import
|
5
|
-
|
6
|
-
|
4
|
+
from notionary.blocks import (
|
5
|
+
NotionBlockElement,
|
6
|
+
NotionBlockResult,
|
7
|
+
ElementPromptContent,
|
8
|
+
ElementPromptBuilder,
|
9
|
+
)
|
10
|
+
from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
|
7
11
|
|
8
12
|
|
9
13
|
class TableElement(NotionBlockElement):
|
@@ -24,17 +28,20 @@ class TableElement(NotionBlockElement):
|
|
24
28
|
|
25
29
|
@classmethod
|
26
30
|
def match_markdown(cls, text: str) -> bool:
|
27
|
-
"""
|
31
|
+
"""
|
32
|
+
Check if text contains a markdown table.
|
33
|
+
Accepts tables with only header + separator, as well as header + separator + data rows.
|
34
|
+
"""
|
28
35
|
lines = text.split("\n")
|
29
36
|
|
30
|
-
if len(lines) <
|
37
|
+
if len(lines) < 2:
|
31
38
|
return False
|
32
39
|
|
33
|
-
|
40
|
+
# Akzeptiere Header + Separator auch ohne Datenzeile
|
41
|
+
for i, line in enumerate(lines[:-1]):
|
34
42
|
if (
|
35
|
-
|
36
|
-
and
|
37
|
-
and TableElement.ROW_PATTERN.match(lines[i + 2])
|
43
|
+
cls.ROW_PATTERN.match(line)
|
44
|
+
and cls.SEPARATOR_PATTERN.match(lines[i + 1])
|
38
45
|
):
|
39
46
|
return True
|
40
47
|
|
@@ -46,7 +53,7 @@ class TableElement(NotionBlockElement):
|
|
46
53
|
return block.get("type") == "table"
|
47
54
|
|
48
55
|
@classmethod
|
49
|
-
def markdown_to_notion(cls, text: str) ->
|
56
|
+
def markdown_to_notion(cls, text: str) -> NotionBlockResult:
|
50
57
|
"""Convert markdown table to Notion table block."""
|
51
58
|
if not TableElement.match_markdown(text):
|
52
59
|
return None
|
@@ -67,7 +74,7 @@ class TableElement(NotionBlockElement):
|
|
67
74
|
column_count = len(rows[0])
|
68
75
|
TableElement._normalize_row_lengths(rows, column_count)
|
69
76
|
|
70
|
-
|
77
|
+
table_block = {
|
71
78
|
"type": "table",
|
72
79
|
"table": {
|
73
80
|
"table_width": column_count,
|
@@ -77,6 +84,11 @@ class TableElement(NotionBlockElement):
|
|
77
84
|
},
|
78
85
|
}
|
79
86
|
|
87
|
+
# Leerer Paragraph nach der Tabelle
|
88
|
+
empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
|
89
|
+
|
90
|
+
return [table_block, empty_paragraph]
|
91
|
+
|
80
92
|
@classmethod
|
81
93
|
def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
|
82
94
|
"""Convert Notion table block to markdown table."""
|
@@ -0,0 +1,40 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
from notionary.blocks.markdown_node import MarkdownNode
|
5
|
+
|
6
|
+
|
7
|
+
class TableMarkdownBlockParams(BaseModel):
|
8
|
+
headers: list[str]
|
9
|
+
rows: list[list[str]]
|
10
|
+
|
11
|
+
|
12
|
+
class TableMarkdownNode(MarkdownNode):
|
13
|
+
"""
|
14
|
+
Programmatic interface for creating Markdown tables.
|
15
|
+
Example:
|
16
|
+
| Header 1 | Header 2 | Header 3 |
|
17
|
+
| -------- | -------- | -------- |
|
18
|
+
| Cell 1 | Cell 2 | Cell 3 |
|
19
|
+
| Cell 4 | Cell 5 | Cell 6 |
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, headers: list[str], rows: list[list[str]]):
|
23
|
+
if not headers or not all(isinstance(row, list) for row in rows):
|
24
|
+
raise ValueError("headers must be a list and rows must be a list of lists")
|
25
|
+
self.headers = headers
|
26
|
+
self.rows = rows
|
27
|
+
|
28
|
+
@classmethod
|
29
|
+
def from_params(cls, params: TableMarkdownBlockParams) -> TableMarkdownNode:
|
30
|
+
return cls(headers=params.headers, rows=params.rows)
|
31
|
+
|
32
|
+
def to_markdown(self) -> str:
|
33
|
+
col_count = len(self.headers)
|
34
|
+
# Header row
|
35
|
+
header = "| " + " | ".join(self.headers) + " |"
|
36
|
+
# Separator row
|
37
|
+
separator = "| " + " | ".join(["--------"] * col_count) + " |"
|
38
|
+
# Data rows
|
39
|
+
data_rows = ["| " + " | ".join(row) + " |" for row in self.rows]
|
40
|
+
return "\n".join([header, separator] + data_rows)
|
File without changes
|
@@ -1,9 +1,13 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional
|
3
3
|
|
4
|
-
from notionary.blocks import
|
5
|
-
|
6
|
-
|
4
|
+
from notionary.blocks import (
|
5
|
+
ElementPromptContent,
|
6
|
+
ElementPromptBuilder,
|
7
|
+
NotionBlockResult,
|
8
|
+
NotionBlockElement,
|
9
|
+
)
|
10
|
+
from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
|
7
11
|
|
8
12
|
|
9
13
|
class TodoElement(NotionBlockElement):
|
@@ -34,7 +38,7 @@ class TodoElement(NotionBlockElement):
|
|
34
38
|
return block.get("type") == "to_do"
|
35
39
|
|
36
40
|
@classmethod
|
37
|
-
def markdown_to_notion(cls, text: str) ->
|
41
|
+
def markdown_to_notion(cls, text: str) -> NotionBlockResult:
|
38
42
|
"""Convert markdown todo item to Notion to_do block."""
|
39
43
|
done_match = TodoElement.DONE_PATTERN.match(text)
|
40
44
|
if done_match:
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
from notionary.blocks.markdown_node import MarkdownNode
|
5
|
+
|
6
|
+
|
7
|
+
class TodoMarkdownBlockParams(BaseModel):
|
8
|
+
text: str
|
9
|
+
checked: bool = False
|
10
|
+
marker: str = "-"
|
11
|
+
|
12
|
+
|
13
|
+
class TodoMarkdownNode(MarkdownNode):
|
14
|
+
"""
|
15
|
+
Programmatic interface for creating Markdown todo items (checkboxes).
|
16
|
+
Supports checked and unchecked states.
|
17
|
+
Example: - [ ] Task, - [x] Done
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, text: str, checked: bool = False, marker: str = "-"):
|
21
|
+
self.text = text
|
22
|
+
self.checked = checked
|
23
|
+
self.marker = marker if marker in {"-", "*", "+"} else "-"
|
24
|
+
|
25
|
+
@classmethod
|
26
|
+
def from_params(cls, params: TodoMarkdownBlockParams) -> TodoMarkdownNode:
|
27
|
+
return cls(text=params.text, checked=params.checked, marker=params.marker)
|
28
|
+
|
29
|
+
def to_markdown(self) -> str:
|
30
|
+
checkbox = "[x]" if self.checked else "[ ]"
|
31
|
+
return f"{self.marker} {checkbox} {self.text}"
|
File without changes
|
@@ -1,8 +1,12 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional, List, Tuple, Callable
|
3
3
|
|
4
|
-
from notionary.blocks import
|
5
|
-
|
4
|
+
from notionary.blocks import (
|
5
|
+
NotionBlockElement,
|
6
|
+
NotionBlockResult,
|
7
|
+
ElementPromptContent,
|
8
|
+
ElementPromptBuilder,
|
9
|
+
)
|
6
10
|
|
7
11
|
|
8
12
|
class ToggleElement(NotionBlockElement):
|
@@ -26,7 +30,7 @@ class ToggleElement(NotionBlockElement):
|
|
26
30
|
return block.get("type") == "toggle"
|
27
31
|
|
28
32
|
@classmethod
|
29
|
-
def markdown_to_notion(cls, text: str) ->
|
33
|
+
def markdown_to_notion(cls, text: str) -> NotionBlockResult:
|
30
34
|
"""Convert markdown toggle line to Notion toggle block."""
|
31
35
|
toggle_match = ToggleElement.TOGGLE_PATTERN.match(text.strip())
|
32
36
|
if not toggle_match:
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional, List
|
4
|
+
from pydantic import BaseModel
|
5
|
+
from notionary.blocks.markdown_node import MarkdownNode
|
6
|
+
|
7
|
+
|
8
|
+
class ToggleMarkdownBlockParams(BaseModel):
|
9
|
+
title: str
|
10
|
+
content: Optional[List[str]] = None
|
11
|
+
|
12
|
+
|
13
|
+
class ToggleMarkdownNode(MarkdownNode):
|
14
|
+
"""
|
15
|
+
Programmatic interface for creating Notion-style Markdown toggle blocks
|
16
|
+
with pipe-prefixed nested content.
|
17
|
+
Example:
|
18
|
+
+++ Details
|
19
|
+
| Here are the details.
|
20
|
+
| You can add more lines.
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self, title: str, content: Optional[List[str]] = None):
|
24
|
+
self.title = title
|
25
|
+
self.content = content or []
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def from_params(cls, params: ToggleMarkdownBlockParams) -> ToggleMarkdownNode:
|
29
|
+
return cls(title=params.title, content=params.content)
|
30
|
+
|
31
|
+
def to_markdown(self) -> str:
|
32
|
+
result = f"+++ {self.title}"
|
33
|
+
if self.content:
|
34
|
+
result += "\n" + "\n".join([f"| {line}" for line in self.content])
|
35
|
+
return result
|
File without changes
|
notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py}
RENAMED
@@ -1,9 +1,13 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional, List, Tuple, Callable
|
3
3
|
|
4
|
-
from notionary.blocks import
|
5
|
-
|
6
|
-
|
4
|
+
from notionary.blocks import (
|
5
|
+
ElementPromptContent,
|
6
|
+
ElementPromptBuilder,
|
7
|
+
NotionBlockElement,
|
8
|
+
NotionBlockResult,
|
9
|
+
)
|
10
|
+
from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
|
7
11
|
|
8
12
|
|
9
13
|
class ToggleableHeadingElement(NotionBlockElement):
|
@@ -29,7 +33,7 @@ class ToggleableHeadingElement(NotionBlockElement):
|
|
29
33
|
return heading_data.get("is_toggleable", False) is True
|
30
34
|
|
31
35
|
@staticmethod
|
32
|
-
def markdown_to_notion(text: str) ->
|
36
|
+
def markdown_to_notion(text: str) -> NotionBlockResult:
|
33
37
|
"""Convert markdown collapsible heading to Notion toggleable heading block."""
|
34
38
|
header_match = ToggleableHeadingElement.PATTERN.match(text)
|
35
39
|
if not header_match:
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional, List
|
4
|
+
from pydantic import BaseModel
|
5
|
+
from notionary.blocks.markdown_node import MarkdownNode
|
6
|
+
|
7
|
+
|
8
|
+
class ToggleableHeadingMarkdownBlockParams(BaseModel):
|
9
|
+
text: str
|
10
|
+
level: int = 1
|
11
|
+
content: Optional[List[str]] = None
|
12
|
+
|
13
|
+
|
14
|
+
class ToggleableHeadingMarkdownNode(MarkdownNode):
|
15
|
+
"""
|
16
|
+
Programmatic interface for creating collapsible Markdown headings (toggleable headings).
|
17
|
+
Pipe-prefixed lines are used for the collapsible content.
|
18
|
+
Example:
|
19
|
+
+# Section
|
20
|
+
| Hidden content
|
21
|
+
+## Subsection
|
22
|
+
| Details
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self, text: str, level: int = 1, content: Optional[list[str]] = None):
|
26
|
+
if not (1 <= level <= 3):
|
27
|
+
raise ValueError("Only heading levels 1-3 are supported (H1, H2, H3)")
|
28
|
+
self.text = text
|
29
|
+
self.level = level
|
30
|
+
self.content = content or []
|
31
|
+
|
32
|
+
@classmethod
|
33
|
+
def from_params(
|
34
|
+
cls, params: ToggleableHeadingMarkdownBlockParams
|
35
|
+
) -> ToggleableHeadingMarkdownNode:
|
36
|
+
return cls(text=params.text, level=params.level, content=params.content)
|
37
|
+
|
38
|
+
def to_markdown(self) -> str:
|
39
|
+
prefix = "+" + ("#" * self.level)
|
40
|
+
result = f"{prefix} {self.text}"
|
41
|
+
if self.content:
|
42
|
+
result += "\n" + "\n".join([f"| {line}" for line in self.content])
|
43
|
+
return result
|
File without changes
|