notionary 0.2.28__py3-none-any.whl → 0.3.1__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 +9 -2
- notionary/blocks/__init__.py +5 -0
- notionary/blocks/client.py +6 -4
- notionary/blocks/enums.py +28 -1
- notionary/blocks/rich_text/markdown_rich_text_converter.py +14 -0
- notionary/blocks/rich_text/models.py +14 -0
- notionary/blocks/rich_text/name_id_resolver/__init__.py +2 -0
- notionary/blocks/rich_text/name_id_resolver/data_source.py +32 -0
- notionary/blocks/rich_text/rich_text_markdown_converter.py +12 -0
- notionary/blocks/rich_text/rich_text_patterns.py +3 -0
- notionary/blocks/schemas.py +42 -10
- notionary/comments/__init__.py +5 -0
- notionary/comments/client.py +7 -10
- notionary/comments/factory.py +4 -6
- notionary/data_source/http/data_source_instance_client.py +14 -4
- notionary/data_source/properties/{models.py → schemas.py} +4 -8
- notionary/data_source/query/__init__.py +9 -0
- notionary/data_source/query/builder.py +38 -10
- notionary/data_source/query/schema.py +13 -10
- notionary/data_source/query/validator.py +11 -11
- notionary/data_source/schema/registry.py +104 -0
- notionary/data_source/schema/service.py +136 -0
- notionary/data_source/schemas.py +1 -1
- notionary/data_source/service.py +29 -103
- notionary/database/service.py +17 -60
- notionary/exceptions/__init__.py +5 -1
- notionary/exceptions/block_parsing.py +21 -0
- notionary/exceptions/search.py +24 -0
- notionary/http/client.py +9 -10
- notionary/http/models.py +5 -4
- notionary/page/content/factory.py +10 -3
- notionary/page/content/markdown/builder.py +76 -154
- notionary/page/content/markdown/nodes/__init__.py +0 -2
- notionary/page/content/markdown/nodes/audio.py +1 -1
- notionary/page/content/markdown/nodes/base.py +1 -1
- notionary/page/content/markdown/nodes/bookmark.py +1 -1
- notionary/page/content/markdown/nodes/breadcrumb.py +1 -1
- notionary/page/content/markdown/nodes/bulleted_list.py +31 -8
- notionary/page/content/markdown/nodes/callout.py +12 -10
- notionary/page/content/markdown/nodes/code.py +3 -5
- notionary/page/content/markdown/nodes/columns.py +39 -21
- notionary/page/content/markdown/nodes/container.py +64 -0
- notionary/page/content/markdown/nodes/divider.py +1 -1
- notionary/page/content/markdown/nodes/embed.py +1 -1
- notionary/page/content/markdown/nodes/equation.py +1 -1
- notionary/page/content/markdown/nodes/file.py +1 -1
- notionary/page/content/markdown/nodes/heading.py +26 -6
- notionary/page/content/markdown/nodes/image.py +1 -1
- notionary/page/content/markdown/nodes/mixins/__init__.py +5 -0
- notionary/page/content/markdown/nodes/mixins/caption.py +1 -1
- notionary/page/content/markdown/nodes/numbered_list.py +28 -5
- notionary/page/content/markdown/nodes/paragraph.py +1 -1
- notionary/page/content/markdown/nodes/pdf.py +1 -1
- notionary/page/content/markdown/nodes/quote.py +17 -5
- notionary/page/content/markdown/nodes/space.py +1 -1
- notionary/page/content/markdown/nodes/table.py +1 -1
- notionary/page/content/markdown/nodes/table_of_contents.py +1 -1
- notionary/page/content/markdown/nodes/todo.py +23 -7
- notionary/page/content/markdown/nodes/toggle.py +13 -14
- notionary/page/content/markdown/nodes/video.py +1 -1
- notionary/page/content/parser/context.py +98 -21
- notionary/page/content/parser/factory.py +1 -10
- notionary/page/content/parser/parsers/__init__.py +0 -2
- notionary/page/content/parser/parsers/audio.py +1 -1
- notionary/page/content/parser/parsers/base.py +1 -1
- notionary/page/content/parser/parsers/bookmark.py +1 -1
- notionary/page/content/parser/parsers/breadcrumb.py +1 -1
- notionary/page/content/parser/parsers/bulleted_list.py +52 -8
- notionary/page/content/parser/parsers/callout.py +55 -84
- notionary/page/content/parser/parsers/caption.py +1 -1
- notionary/page/content/parser/parsers/code.py +5 -5
- notionary/page/content/parser/parsers/column.py +23 -64
- notionary/page/content/parser/parsers/column_list.py +45 -45
- notionary/page/content/parser/parsers/divider.py +1 -1
- notionary/page/content/parser/parsers/embed.py +1 -1
- notionary/page/content/parser/parsers/equation.py +1 -1
- notionary/page/content/parser/parsers/file.py +1 -1
- notionary/page/content/parser/parsers/heading.py +65 -8
- notionary/page/content/parser/parsers/image.py +1 -1
- notionary/page/content/parser/parsers/numbered_list.py +52 -8
- notionary/page/content/parser/parsers/paragraph.py +3 -2
- notionary/page/content/parser/parsers/pdf.py +1 -1
- notionary/page/content/parser/parsers/quote.py +75 -15
- notionary/page/content/parser/parsers/space.py +14 -8
- notionary/page/content/parser/parsers/table.py +1 -1
- notionary/page/content/parser/parsers/table_of_contents.py +1 -1
- notionary/page/content/parser/parsers/todo.py +57 -19
- notionary/page/content/parser/parsers/toggle.py +17 -74
- notionary/page/content/parser/parsers/video.py +1 -1
- notionary/page/content/parser/post_processing/handlers/rich_text_length.py +6 -4
- notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +43 -22
- notionary/page/content/parser/pre_processsing/handlers/__init__.py +4 -0
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +108 -54
- notionary/page/content/parser/pre_processsing/handlers/indentation.py +86 -0
- notionary/page/content/parser/pre_processsing/handlers/video_syntax.py +66 -0
- notionary/page/content/parser/pre_processsing/handlers/whitespace.py +14 -7
- notionary/page/content/parser/service.py +9 -0
- notionary/page/content/renderer/context.py +5 -2
- notionary/page/content/renderer/factory.py +2 -11
- notionary/page/content/renderer/post_processing/handlers/__init__.py +2 -2
- notionary/page/content/renderer/post_processing/handlers/numbered_list.py +156 -0
- notionary/page/content/renderer/renderers/__init__.py +0 -2
- notionary/page/content/renderer/renderers/base.py +1 -1
- notionary/page/content/renderer/renderers/bulleted_list.py +1 -1
- notionary/page/content/renderer/renderers/callout.py +6 -21
- notionary/page/content/renderer/renderers/captioned_block.py +1 -1
- notionary/page/content/renderer/renderers/column.py +28 -19
- notionary/page/content/renderer/renderers/column_list.py +24 -11
- notionary/page/content/renderer/renderers/heading.py +53 -27
- notionary/page/content/renderer/renderers/numbered_list.py +6 -5
- notionary/page/content/renderer/renderers/quote.py +1 -1
- notionary/page/content/renderer/renderers/todo.py +1 -1
- notionary/page/content/renderer/renderers/toggle.py +6 -7
- notionary/page/content/service.py +4 -1
- notionary/page/content/syntax/__init__.py +4 -0
- notionary/page/content/syntax/grammar.py +10 -0
- notionary/page/content/syntax/models.py +0 -2
- notionary/page/content/syntax/{service.py → registry.py} +31 -91
- notionary/page/properties/client.py +3 -3
- notionary/page/properties/models.py +3 -2
- notionary/page/properties/service.py +18 -3
- notionary/page/service.py +22 -80
- notionary/shared/entity/service.py +94 -36
- notionary/shared/models/cover.py +1 -1
- notionary/shared/typings.py +3 -0
- notionary/user/base.py +60 -11
- notionary/user/factory.py +0 -0
- notionary/utils/decorators.py +122 -0
- notionary/utils/fuzzy.py +18 -6
- notionary/utils/mixins/logging.py +38 -27
- notionary/utils/pagination.py +70 -16
- notionary/workspace/__init__.py +2 -1
- notionary/workspace/client.py +4 -2
- notionary/workspace/query/__init__.py +3 -0
- notionary/workspace/query/builder.py +25 -1
- notionary/workspace/query/models.py +12 -3
- notionary/workspace/query/service.py +57 -32
- notionary/workspace/service.py +31 -21
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/METADATA +35 -105
- notionary-0.3.1.dist-info/RECORD +211 -0
- notionary/page/content/markdown/nodes/toggleable_heading.py +0 -35
- notionary/page/content/parser/parsers/toggleable_heading.py +0 -150
- notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +0 -62
- notionary/page/content/renderer/renderers/toggleable_heading.py +0 -78
- notionary/utils/async_retry.py +0 -39
- notionary/utils/singleton.py +0 -13
- notionary-0.2.28.dist-info/RECORD +0 -200
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/WHEEL +0 -0
- {notionary-0.2.28.dist-info → notionary-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from enum import IntEnum
|
|
3
|
+
from typing import override
|
|
4
|
+
|
|
5
|
+
from notionary.page.content.renderer.post_processing.port import PostProcessor
|
|
6
|
+
from notionary.page.content.syntax.grammar import MarkdownGrammar
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _NumberingStyle(IntEnum):
|
|
10
|
+
NUMERIC = 0
|
|
11
|
+
ALPHABETIC = 1
|
|
12
|
+
ROMAN = 2
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class _ListNumberingState:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._counters_by_level: dict[int, int] = {}
|
|
18
|
+
self._current_level = -1
|
|
19
|
+
|
|
20
|
+
def advance_to_level(self, new_level: int) -> None:
|
|
21
|
+
self._forget_deeper_levels_than(new_level)
|
|
22
|
+
self._increment_counter_at_level(new_level)
|
|
23
|
+
self._current_level = new_level
|
|
24
|
+
|
|
25
|
+
def get_number_for_current_level(self) -> str:
|
|
26
|
+
counter = self._counters_by_level[self._current_level]
|
|
27
|
+
style = self._determine_numbering_style(self._current_level)
|
|
28
|
+
return self._format_number(counter, style)
|
|
29
|
+
|
|
30
|
+
def reset(self) -> None:
|
|
31
|
+
self._counters_by_level.clear()
|
|
32
|
+
self._current_level = -1
|
|
33
|
+
|
|
34
|
+
def _forget_deeper_levels_than(self, level: int) -> None:
|
|
35
|
+
self._counters_by_level = {
|
|
36
|
+
existing_level: count
|
|
37
|
+
for existing_level, count in self._counters_by_level.items()
|
|
38
|
+
if existing_level <= level
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def _increment_counter_at_level(self, level: int) -> None:
|
|
42
|
+
self._counters_by_level[level] = self._counters_by_level.get(level, 0) + 1
|
|
43
|
+
|
|
44
|
+
def _determine_numbering_style(self, nesting_level: int) -> _NumberingStyle:
|
|
45
|
+
return _NumberingStyle(nesting_level % 3)
|
|
46
|
+
|
|
47
|
+
def _format_number(self, counter: int, style: _NumberingStyle) -> str:
|
|
48
|
+
if style == _NumberingStyle.NUMERIC:
|
|
49
|
+
return str(counter)
|
|
50
|
+
elif style == _NumberingStyle.ALPHABETIC:
|
|
51
|
+
return self._to_alphabetic(counter)
|
|
52
|
+
else:
|
|
53
|
+
return self._to_roman(counter)
|
|
54
|
+
|
|
55
|
+
def _to_alphabetic(self, number: int) -> str:
|
|
56
|
+
result = ""
|
|
57
|
+
number -= 1
|
|
58
|
+
while number >= 0:
|
|
59
|
+
result = chr(ord("a") + (number % 26)) + result
|
|
60
|
+
number = number // 26 - 1
|
|
61
|
+
return result
|
|
62
|
+
|
|
63
|
+
def _to_roman(self, number: int) -> str:
|
|
64
|
+
conversions = [
|
|
65
|
+
(1000, "m"),
|
|
66
|
+
(900, "cm"),
|
|
67
|
+
(500, "d"),
|
|
68
|
+
(400, "cd"),
|
|
69
|
+
(100, "c"),
|
|
70
|
+
(90, "xc"),
|
|
71
|
+
(50, "l"),
|
|
72
|
+
(40, "xl"),
|
|
73
|
+
(10, "x"),
|
|
74
|
+
(9, "ix"),
|
|
75
|
+
(5, "v"),
|
|
76
|
+
(4, "iv"),
|
|
77
|
+
(1, "i"),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
result = ""
|
|
81
|
+
for arabic, roman in conversions:
|
|
82
|
+
while number >= arabic:
|
|
83
|
+
result += roman
|
|
84
|
+
number -= arabic
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class NumberedListPlaceholderReplacerPostProcessor(PostProcessor):
|
|
89
|
+
def __init__(self, markdown_grammar: MarkdownGrammar | None = None) -> None:
|
|
90
|
+
self._markdown_grammar = markdown_grammar or MarkdownGrammar()
|
|
91
|
+
self._spaces_per_nesting_level = self._markdown_grammar.spaces_per_nesting_level
|
|
92
|
+
self._numbered_list_placeholder = self._markdown_grammar.numbered_list_placeholder
|
|
93
|
+
|
|
94
|
+
@override
|
|
95
|
+
def process(self, markdown_text: str) -> str:
|
|
96
|
+
lines = markdown_text.splitlines()
|
|
97
|
+
return self._convert_placeholder_lists_to_numbered_lists(lines)
|
|
98
|
+
|
|
99
|
+
def _convert_placeholder_lists_to_numbered_lists(self, lines: list[str]) -> str:
|
|
100
|
+
result = []
|
|
101
|
+
list_state = _ListNumberingState()
|
|
102
|
+
|
|
103
|
+
for line_index, line in enumerate(lines):
|
|
104
|
+
if self._is_placeholder_list_item(line):
|
|
105
|
+
numbered_line = self._convert_to_numbered_item(line, list_state)
|
|
106
|
+
result.append(numbered_line)
|
|
107
|
+
elif self._is_blank_between_list_items(lines, line_index, result):
|
|
108
|
+
continue
|
|
109
|
+
else:
|
|
110
|
+
list_state.reset()
|
|
111
|
+
result.append(line)
|
|
112
|
+
|
|
113
|
+
return "\n".join(result)
|
|
114
|
+
|
|
115
|
+
def _convert_to_numbered_item(self, line: str, state: _ListNumberingState) -> str:
|
|
116
|
+
indentation = self._extract_indentation(line)
|
|
117
|
+
content = self._extract_content(line)
|
|
118
|
+
nesting_level = self._calculate_nesting_level(indentation)
|
|
119
|
+
|
|
120
|
+
state.advance_to_level(nesting_level)
|
|
121
|
+
number = state.get_number_for_current_level()
|
|
122
|
+
|
|
123
|
+
return f"{indentation}{number}. {content}"
|
|
124
|
+
|
|
125
|
+
def _calculate_nesting_level(self, indentation: str) -> int:
|
|
126
|
+
return len(indentation) // self._spaces_per_nesting_level
|
|
127
|
+
|
|
128
|
+
def _extract_indentation(self, line: str) -> str:
|
|
129
|
+
match = re.match(rf"^(\s*){re.escape(self._numbered_list_placeholder)}\.", line)
|
|
130
|
+
return match.group(1) if match else ""
|
|
131
|
+
|
|
132
|
+
def _extract_content(self, line: str) -> str:
|
|
133
|
+
match = re.match(rf"^\s*{re.escape(self._numbered_list_placeholder)}\.\s*(.*)", line)
|
|
134
|
+
return match.group(1) if match else ""
|
|
135
|
+
|
|
136
|
+
def _is_placeholder_list_item(self, line: str) -> bool:
|
|
137
|
+
return bool(re.match(rf"^\s*{re.escape(self._numbered_list_placeholder)}\.", line))
|
|
138
|
+
|
|
139
|
+
def _is_blank_between_list_items(self, lines: list[str], current_index: int, processed_lines: list[str]) -> bool:
|
|
140
|
+
if not self._is_blank(lines[current_index]):
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
previous_line_was_list_item = processed_lines and self._looks_like_numbered_list_item(processed_lines[-1])
|
|
144
|
+
if not previous_line_was_list_item:
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
next_line_is_list_item = current_index + 1 < len(lines) and self._is_placeholder_list_item(
|
|
148
|
+
lines[current_index + 1]
|
|
149
|
+
)
|
|
150
|
+
return next_line_is_list_item
|
|
151
|
+
|
|
152
|
+
def _is_blank(self, line: str) -> bool:
|
|
153
|
+
return not line.strip()
|
|
154
|
+
|
|
155
|
+
def _looks_like_numbered_list_item(self, line: str) -> bool:
|
|
156
|
+
return bool(re.match(r"^\s*(\d+|[a-z]+|[ivxlcdm]+)\.\s+", line))
|
|
@@ -23,7 +23,6 @@ from .table_of_contents import TableOfContentsRenderer
|
|
|
23
23
|
from .table_row import TableRowHandler
|
|
24
24
|
from .todo import TodoRenderer
|
|
25
25
|
from .toggle import ToggleRenderer
|
|
26
|
-
from .toggleable_heading import ToggleableHeadingRenderer
|
|
27
26
|
from .video import VideoRenderer
|
|
28
27
|
|
|
29
28
|
__all__ = [
|
|
@@ -52,6 +51,5 @@ __all__ = [
|
|
|
52
51
|
"TableRowHandler",
|
|
53
52
|
"TodoRenderer",
|
|
54
53
|
"ToggleRenderer",
|
|
55
|
-
"ToggleableHeadingRenderer",
|
|
56
54
|
"VideoRenderer",
|
|
57
55
|
]
|
|
@@ -4,7 +4,7 @@ from abc import ABC, abstractmethod
|
|
|
4
4
|
|
|
5
5
|
from notionary.blocks.schemas import Block
|
|
6
6
|
from notionary.page.content.renderer.context import MarkdownRenderingContext
|
|
7
|
-
from notionary.page.content.syntax
|
|
7
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class BlockRenderer(ABC):
|
|
@@ -4,7 +4,7 @@ from notionary.blocks.rich_text.rich_text_markdown_converter import RichTextToMa
|
|
|
4
4
|
from notionary.blocks.schemas import Block, BlockType
|
|
5
5
|
from notionary.page.content.renderer.context import MarkdownRenderingContext
|
|
6
6
|
from notionary.page.content.renderer.renderers.base import BlockRenderer
|
|
7
|
-
from notionary.page.content.syntax
|
|
7
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class BulletedListRenderer(BlockRenderer):
|
|
@@ -4,7 +4,7 @@ from notionary.blocks.rich_text.rich_text_markdown_converter import RichTextToMa
|
|
|
4
4
|
from notionary.blocks.schemas import Block, BlockType
|
|
5
5
|
from notionary.page.content.renderer.context import MarkdownRenderingContext
|
|
6
6
|
from notionary.page.content.renderer.renderers.base import BlockRenderer
|
|
7
|
-
from notionary.page.content.syntax
|
|
7
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class CalloutRenderer(BlockRenderer):
|
|
@@ -22,37 +22,22 @@ class CalloutRenderer(BlockRenderer):
|
|
|
22
22
|
|
|
23
23
|
@override
|
|
24
24
|
async def _process(self, context: MarkdownRenderingContext) -> None:
|
|
25
|
-
icon = await self._extract_callout_icon(context.block)
|
|
26
25
|
content = await self._extract_callout_content(context.block)
|
|
27
26
|
|
|
28
27
|
if not content:
|
|
29
28
|
context.markdown_result = ""
|
|
30
29
|
return
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# Build callout structure
|
|
35
|
-
# Extract just the base part before the regex pattern
|
|
36
|
-
callout_type = syntax.start_delimiter.split()[1] # Gets "callout" from "::: callout"
|
|
37
|
-
callout_header = f"{self._syntax_registry.MULTI_LINE_BLOCK_DELIMITER} {callout_type}"
|
|
38
|
-
if icon:
|
|
39
|
-
callout_header = f"{self._syntax_registry.MULTI_LINE_BLOCK_DELIMITER} {callout_type} {icon}"
|
|
31
|
+
icon = await self._extract_callout_icon(context.block)
|
|
40
32
|
|
|
41
|
-
|
|
42
|
-
callout_header = context.indent_text(callout_header)
|
|
33
|
+
callout_start_delimiter = self._syntax_registry.get_callout_syntax().start_delimiter
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
children_markdown = await context.render_children()
|
|
35
|
+
result = f'{callout_start_delimiter}({content} "{icon}")' if icon else f"{callout_start_delimiter}({content})"
|
|
46
36
|
|
|
47
|
-
callout_end = syntax.end_delimiter
|
|
48
37
|
if context.indent_level > 0:
|
|
49
|
-
|
|
38
|
+
result = context.indent_text(result)
|
|
50
39
|
|
|
51
|
-
|
|
52
|
-
if children_markdown:
|
|
53
|
-
context.markdown_result = f"{callout_header}\n{content}\n{children_markdown}\n{callout_end}"
|
|
54
|
-
else:
|
|
55
|
-
context.markdown_result = f"{callout_header}\n{content}\n{callout_end}"
|
|
40
|
+
context.markdown_result = result
|
|
56
41
|
|
|
57
42
|
async def _extract_callout_icon(self, block: Block) -> str:
|
|
58
43
|
if not block.callout or not block.callout.icon:
|
|
@@ -5,7 +5,7 @@ from notionary.blocks.rich_text.rich_text_markdown_converter import RichTextToMa
|
|
|
5
5
|
from notionary.blocks.schemas import Block
|
|
6
6
|
from notionary.page.content.renderer.context import MarkdownRenderingContext
|
|
7
7
|
from notionary.page.content.renderer.renderers.base import BlockRenderer
|
|
8
|
-
from notionary.page.content.syntax
|
|
8
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class CaptionedBlockRenderer(BlockRenderer):
|
|
@@ -13,32 +13,41 @@ class ColumnRenderer(BlockRenderer):
|
|
|
13
13
|
|
|
14
14
|
@override
|
|
15
15
|
async def _process(self, context: MarkdownRenderingContext) -> None:
|
|
16
|
-
column_start = self.
|
|
16
|
+
column_start = self._format_column_start(context.block, context.indent_level)
|
|
17
|
+
children_markdown = await self._render_children_with_indentation(context)
|
|
17
18
|
|
|
18
|
-
if
|
|
19
|
-
|
|
19
|
+
if children_markdown:
|
|
20
|
+
context.markdown_result = f"{column_start}\n{children_markdown}"
|
|
21
|
+
else:
|
|
22
|
+
context.markdown_result = column_start
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
def _format_column_start(self, block: Block, indent_level: int) -> str:
|
|
25
|
+
column_start = self._build_column_start_tag(block)
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
column_end = context.indent_text(column_end, spaces=context.indent_level * 4)
|
|
27
|
+
if indent_level > 0:
|
|
28
|
+
indent = " " * indent_level
|
|
29
|
+
column_start = f"{indent}{column_start}"
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
context.markdown_result = f"{column_start}\n{children_markdown}\n{column_end}"
|
|
30
|
-
else:
|
|
31
|
-
context.markdown_result = f"{column_start}\n{column_end}"
|
|
31
|
+
return column_start
|
|
32
32
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
base_start = syntax.start_delimiter
|
|
33
|
+
def _build_column_start_tag(self, block: Block) -> str:
|
|
34
|
+
delimiter = self._syntax_registry.get_column_syntax().start_delimiter
|
|
36
35
|
|
|
37
36
|
if not block.column:
|
|
38
|
-
return
|
|
37
|
+
return delimiter
|
|
39
38
|
|
|
40
39
|
width_ratio = block.column.width_ratio
|
|
41
40
|
if width_ratio:
|
|
42
|
-
return f"{
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
return f"{delimiter} {width_ratio}"
|
|
42
|
+
|
|
43
|
+
return delimiter
|
|
44
|
+
|
|
45
|
+
async def _render_children_with_indentation(self, context: MarkdownRenderingContext) -> str:
|
|
46
|
+
original_indent = context.indent_level
|
|
47
|
+
context.indent_level += 1
|
|
48
|
+
|
|
49
|
+
children_markdown = await context.render_children()
|
|
50
|
+
|
|
51
|
+
context.indent_level = original_indent
|
|
52
|
+
|
|
53
|
+
return children_markdown
|
|
@@ -13,19 +13,32 @@ class ColumnListRenderer(BlockRenderer):
|
|
|
13
13
|
|
|
14
14
|
@override
|
|
15
15
|
async def _process(self, context: MarkdownRenderingContext) -> None:
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
column_list_start = self._format_column_list_start(context.indent_level)
|
|
17
|
+
children_markdown = await self._render_children_with_indentation(context)
|
|
18
18
|
|
|
19
|
-
if
|
|
20
|
-
|
|
19
|
+
if children_markdown:
|
|
20
|
+
context.markdown_result = f"{column_list_start}\n{children_markdown}"
|
|
21
|
+
else:
|
|
22
|
+
context.markdown_result = column_list_start
|
|
23
|
+
|
|
24
|
+
def _format_column_list_start(self, indent_level: int) -> str:
|
|
25
|
+
delimiter = self._get_column_list_delimiter()
|
|
26
|
+
|
|
27
|
+
if indent_level > 0:
|
|
28
|
+
indent = " " * indent_level
|
|
29
|
+
return f"{indent}{delimiter}"
|
|
30
|
+
|
|
31
|
+
return delimiter
|
|
32
|
+
|
|
33
|
+
def _get_column_list_delimiter(self) -> str:
|
|
34
|
+
return self._syntax_registry.get_column_list_syntax().start_delimiter
|
|
35
|
+
|
|
36
|
+
async def _render_children_with_indentation(self, context: MarkdownRenderingContext) -> str:
|
|
37
|
+
original_indent = context.indent_level
|
|
38
|
+
context.indent_level += 1
|
|
21
39
|
|
|
22
40
|
children_markdown = await context.render_children()
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
if context.indent_level > 0:
|
|
26
|
-
column_list_end = context.indent_text(column_list_end)
|
|
42
|
+
context.indent_level = original_indent
|
|
27
43
|
|
|
28
|
-
|
|
29
|
-
context.markdown_result = f"{column_list_start}\n{children_markdown}\n{column_list_end}"
|
|
30
|
-
else:
|
|
31
|
-
context.markdown_result = f"{column_list_start}\n{column_list_end}"
|
|
44
|
+
return children_markdown
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
3
|
from notionary.blocks.rich_text.rich_text_markdown_converter import RichTextToMarkdownConverter
|
|
4
|
-
from notionary.blocks.schemas import Block, BlockType
|
|
4
|
+
from notionary.blocks.schemas import Block, BlockType, HeadingData
|
|
5
5
|
from notionary.page.content.renderer.context import MarkdownRenderingContext
|
|
6
6
|
from notionary.page.content.renderer.renderers.base import BlockRenderer
|
|
7
|
-
from notionary.page.content.syntax
|
|
7
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class HeadingRenderer(BlockRenderer):
|
|
11
|
+
MIN_HEADING_LEVEL = 1
|
|
12
|
+
MAX_HEADING_LEVEL = 3
|
|
13
|
+
|
|
11
14
|
def __init__(
|
|
12
15
|
self,
|
|
13
16
|
syntax_registry: SyntaxRegistry | None = None,
|
|
@@ -19,29 +22,47 @@ class HeadingRenderer(BlockRenderer):
|
|
|
19
22
|
|
|
20
23
|
@override
|
|
21
24
|
def _can_handle(self, block: Block) -> bool:
|
|
22
|
-
|
|
23
|
-
return not block.heading_1.is_toggleable
|
|
24
|
-
if block.type == BlockType.HEADING_2:
|
|
25
|
-
return not block.heading_2.is_toggleable
|
|
26
|
-
if block.type == BlockType.HEADING_3:
|
|
27
|
-
return not block.heading_3.is_toggleable
|
|
28
|
-
|
|
29
|
-
return False
|
|
25
|
+
return block.type in (BlockType.HEADING_1, BlockType.HEADING_2, BlockType.HEADING_3)
|
|
30
26
|
|
|
31
27
|
@override
|
|
32
28
|
async def _process(self, context: MarkdownRenderingContext) -> None:
|
|
33
29
|
level = self._get_heading_level(context.block)
|
|
34
30
|
title = await self._get_heading_title(context.block)
|
|
35
31
|
|
|
36
|
-
if not
|
|
32
|
+
if not self._is_valid_heading(level, title):
|
|
37
33
|
return
|
|
38
34
|
|
|
39
|
-
heading_markdown =
|
|
35
|
+
heading_markdown = self._format_heading(level, title, context.indent_level)
|
|
36
|
+
|
|
37
|
+
if self._is_toggleable(context.block):
|
|
38
|
+
context.markdown_result = await self._render_toggleable_heading(heading_markdown, context)
|
|
39
|
+
else:
|
|
40
|
+
context.markdown_result = heading_markdown
|
|
41
|
+
|
|
42
|
+
def _is_valid_heading(self, level: int, title: str) -> bool:
|
|
43
|
+
return self.MIN_HEADING_LEVEL <= level <= self.MAX_HEADING_LEVEL and bool(title)
|
|
44
|
+
|
|
45
|
+
def _format_heading(self, level: int, title: str, indent_level: int) -> str:
|
|
46
|
+
heading_prefix = self._syntax.start_delimiter * level
|
|
47
|
+
heading_markdown = f"{heading_prefix} {title}"
|
|
48
|
+
|
|
49
|
+
if indent_level > 0:
|
|
50
|
+
indent = " " * indent_level
|
|
51
|
+
heading_markdown = f"{indent}{heading_markdown}"
|
|
52
|
+
|
|
53
|
+
return heading_markdown
|
|
54
|
+
|
|
55
|
+
async def _render_toggleable_heading(self, heading_markdown: str, context: MarkdownRenderingContext) -> str:
|
|
56
|
+
original_indent = context.indent_level
|
|
57
|
+
context.indent_level += 1
|
|
58
|
+
|
|
59
|
+
children_markdown = await context.render_children()
|
|
40
60
|
|
|
41
|
-
|
|
42
|
-
heading_markdown = context.indent_text(heading_markdown)
|
|
61
|
+
context.indent_level = original_indent
|
|
43
62
|
|
|
44
|
-
|
|
63
|
+
if children_markdown:
|
|
64
|
+
return f"{heading_markdown}\n{children_markdown}"
|
|
65
|
+
return heading_markdown
|
|
45
66
|
|
|
46
67
|
def _get_heading_level(self, block: Block) -> int:
|
|
47
68
|
if block.type == BlockType.HEADING_1:
|
|
@@ -50,20 +71,25 @@ class HeadingRenderer(BlockRenderer):
|
|
|
50
71
|
return 2
|
|
51
72
|
elif block.type == BlockType.HEADING_3:
|
|
52
73
|
return 3
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
def _is_toggleable(self, block: Block) -> bool:
|
|
77
|
+
heading_data = self._get_heading_data(block)
|
|
78
|
+
return heading_data.is_toggleable if heading_data else False
|
|
55
79
|
|
|
56
80
|
async def _get_heading_title(self, block: Block) -> str:
|
|
57
|
-
|
|
58
|
-
heading_content = block.heading_1
|
|
59
|
-
elif block.type == BlockType.HEADING_2:
|
|
60
|
-
heading_content = block.heading_2
|
|
61
|
-
elif block.type == BlockType.HEADING_3:
|
|
62
|
-
heading_content = block.heading_3
|
|
63
|
-
else:
|
|
64
|
-
return ""
|
|
81
|
+
heading_data = self._get_heading_data(block)
|
|
65
82
|
|
|
66
|
-
if not
|
|
83
|
+
if not heading_data or not heading_data.rich_text:
|
|
67
84
|
return ""
|
|
68
85
|
|
|
69
|
-
return await self._rich_text_markdown_converter.to_markdown(
|
|
86
|
+
return await self._rich_text_markdown_converter.to_markdown(heading_data.rich_text)
|
|
87
|
+
|
|
88
|
+
def _get_heading_data(self, block: Block) -> HeadingData | None:
|
|
89
|
+
if block.type == BlockType.HEADING_1:
|
|
90
|
+
return block.heading_1
|
|
91
|
+
elif block.type == BlockType.HEADING_2:
|
|
92
|
+
return block.heading_2
|
|
93
|
+
elif block.type == BlockType.HEADING_3:
|
|
94
|
+
return block.heading_3
|
|
95
|
+
return None
|
|
@@ -6,21 +6,22 @@ from notionary.blocks.rich_text.rich_text_markdown_converter import (
|
|
|
6
6
|
from notionary.blocks.schemas import Block, BlockType
|
|
7
7
|
from notionary.page.content.renderer.context import MarkdownRenderingContext
|
|
8
8
|
from notionary.page.content.renderer.renderers.base import BlockRenderer
|
|
9
|
-
from notionary.page.content.syntax
|
|
9
|
+
from notionary.page.content.syntax import MarkdownGrammar, SyntaxRegistry
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
class NumberedListRenderer(BlockRenderer):
|
|
13
|
-
# Placeholder for numbered list fixer (post processing)
|
|
14
|
-
NUMBERED_LIST_PLACEHOLDER = "__NUM__"
|
|
15
|
-
|
|
16
13
|
def __init__(
|
|
17
14
|
self,
|
|
18
15
|
syntax_registry: SyntaxRegistry | None = None,
|
|
19
16
|
rich_text_markdown_converter: RichTextToMarkdownConverter | None = None,
|
|
17
|
+
markdown_grammar: MarkdownGrammar | None = None,
|
|
20
18
|
) -> None:
|
|
21
19
|
super().__init__(syntax_registry=syntax_registry)
|
|
22
20
|
self._rich_text_markdown_converter = rich_text_markdown_converter or RichTextToMarkdownConverter()
|
|
23
21
|
|
|
22
|
+
markdown_grammar = markdown_grammar or MarkdownGrammar()
|
|
23
|
+
self._numbered_list_placeholder = markdown_grammar.numbered_list_placeholder
|
|
24
|
+
|
|
24
25
|
@override
|
|
25
26
|
def _can_handle(self, block: Block) -> bool:
|
|
26
27
|
return block.type == BlockType.NUMBERED_LIST_ITEM
|
|
@@ -31,7 +32,7 @@ class NumberedListRenderer(BlockRenderer):
|
|
|
31
32
|
rich_text = list_item_data.rich_text if list_item_data else []
|
|
32
33
|
content = await self._rich_text_markdown_converter.to_markdown(rich_text)
|
|
33
34
|
|
|
34
|
-
item_line = context.indent_text(f"{self.
|
|
35
|
+
item_line = context.indent_text(f"{self._numbered_list_placeholder}. {content}")
|
|
35
36
|
|
|
36
37
|
children_markdown = await context.render_children_with_additional_indent(1)
|
|
37
38
|
|
|
@@ -4,7 +4,7 @@ from notionary.blocks.rich_text.rich_text_markdown_converter import RichTextToMa
|
|
|
4
4
|
from notionary.blocks.schemas import Block, BlockType
|
|
5
5
|
from notionary.page.content.renderer.context import MarkdownRenderingContext
|
|
6
6
|
from notionary.page.content.renderer.renderers.base import BlockRenderer
|
|
7
|
-
from notionary.page.content.syntax
|
|
7
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class QuoteRenderer(BlockRenderer):
|
|
@@ -5,7 +5,7 @@ from notionary.blocks.rich_text.rich_text_markdown_converter import RichTextToMa
|
|
|
5
5
|
from notionary.blocks.schemas import Block
|
|
6
6
|
from notionary.page.content.renderer.context import MarkdownRenderingContext
|
|
7
7
|
from notionary.page.content.renderer.renderers.base import BlockRenderer
|
|
8
|
-
from notionary.page.content.syntax
|
|
8
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class TodoRenderer(BlockRenderer):
|
|
@@ -5,7 +5,7 @@ from notionary.blocks.rich_text.rich_text_markdown_converter import RichTextToMa
|
|
|
5
5
|
from notionary.blocks.schemas import Block
|
|
6
6
|
from notionary.page.content.renderer.context import MarkdownRenderingContext
|
|
7
7
|
from notionary.page.content.renderer.renderers.base import BlockRenderer
|
|
8
|
-
from notionary.page.content.syntax
|
|
8
|
+
from notionary.page.content.syntax import SyntaxRegistry
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class ToggleRenderer(BlockRenderer):
|
|
@@ -34,16 +34,15 @@ class ToggleRenderer(BlockRenderer):
|
|
|
34
34
|
if context.indent_level > 0:
|
|
35
35
|
toggle_start = context.indent_text(toggle_start)
|
|
36
36
|
|
|
37
|
+
original_indent = context.indent_level
|
|
38
|
+
context.indent_level += 1
|
|
37
39
|
children_markdown = await context.render_children()
|
|
38
|
-
|
|
39
|
-
toggle_end = syntax.end_delimiter
|
|
40
|
-
if context.indent_level > 0:
|
|
41
|
-
toggle_end = context.indent_text(toggle_end)
|
|
40
|
+
context.indent_level = original_indent
|
|
42
41
|
|
|
43
42
|
if children_markdown:
|
|
44
|
-
context.markdown_result = f"{toggle_start}\n{children_markdown}
|
|
43
|
+
context.markdown_result = f"{toggle_start}\n{children_markdown}"
|
|
45
44
|
else:
|
|
46
|
-
context.markdown_result =
|
|
45
|
+
context.markdown_result = toggle_start
|
|
47
46
|
|
|
48
47
|
async def _extract_toggle_title(self, block: Block) -> str:
|
|
49
48
|
if not block.toggle or not block.toggle.rich_text:
|
|
@@ -6,7 +6,7 @@ from notionary.blocks.schemas import Block
|
|
|
6
6
|
from notionary.page.content.markdown.builder import MarkdownBuilder
|
|
7
7
|
from notionary.page.content.parser.service import MarkdownToNotionConverter
|
|
8
8
|
from notionary.page.content.renderer.service import NotionToMarkdownConverter
|
|
9
|
-
from notionary.utils.
|
|
9
|
+
from notionary.utils.decorators import async_retry, time_execution_async
|
|
10
10
|
from notionary.utils.mixins.logging import LoggingMixin
|
|
11
11
|
|
|
12
12
|
|
|
@@ -23,10 +23,12 @@ class PageContentService(LoggingMixin):
|
|
|
23
23
|
self._markdown_converter = markdown_converter
|
|
24
24
|
self._notion_to_markdown_converter = notion_to_markdown_converter
|
|
25
25
|
|
|
26
|
+
@time_execution_async()
|
|
26
27
|
async def get_as_markdown(self) -> str:
|
|
27
28
|
blocks = await self._block_client.get_block_tree(parent_block_id=self._page_id)
|
|
28
29
|
return await self._notion_to_markdown_converter.convert(blocks=blocks)
|
|
29
30
|
|
|
31
|
+
@time_execution_async()
|
|
30
32
|
async def clear(self) -> None:
|
|
31
33
|
children_response = await self._block_client.get_block_children(block_id=self._page_id)
|
|
32
34
|
|
|
@@ -41,6 +43,7 @@ class PageContentService(LoggingMixin):
|
|
|
41
43
|
self.logger.debug("Deleting block: %s", block.id)
|
|
42
44
|
await self._block_client.delete_block(block.id)
|
|
43
45
|
|
|
46
|
+
@time_execution_async()
|
|
44
47
|
async def append_markdown(self, content: str | Callable[[MarkdownBuilder], MarkdownBuilder]) -> None:
|
|
45
48
|
markdown = self._extract_markdown(content)
|
|
46
49
|
if not markdown:
|