notionary 0.2.27__py3-none-any.whl → 0.2.28__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 +5 -20
- notionary/blocks/client.py +87 -215
- notionary/blocks/enums.py +167 -0
- notionary/blocks/rich_text/markdown_rich_text_converter.py +266 -0
- notionary/blocks/rich_text/models.py +164 -0
- notionary/blocks/rich_text/name_id_resolver/__init__.py +11 -0
- notionary/blocks/rich_text/name_id_resolver/database.py +31 -0
- notionary/blocks/rich_text/name_id_resolver/page.py +34 -0
- notionary/blocks/rich_text/name_id_resolver/person.py +37 -0
- notionary/blocks/rich_text/name_id_resolver/port.py +11 -0
- notionary/blocks/rich_text/rich_text_markdown_converter.py +132 -0
- notionary/blocks/rich_text/rich_text_patterns.py +39 -0
- notionary/blocks/schemas.py +746 -0
- notionary/comments/client.py +52 -187
- notionary/comments/factory.py +40 -0
- notionary/comments/models.py +5 -127
- notionary/comments/schemas.py +240 -0
- notionary/comments/service.py +34 -0
- notionary/data_source/http/client.py +11 -0
- notionary/data_source/http/data_source_instance_client.py +94 -0
- notionary/data_source/properties/models.py +406 -0
- notionary/data_source/query/builder.py +429 -0
- notionary/data_source/query/resolver.py +114 -0
- notionary/data_source/query/schema.py +304 -0
- notionary/data_source/query/validator.py +73 -0
- notionary/data_source/schemas.py +27 -0
- notionary/data_source/service.py +353 -0
- notionary/database/client.py +30 -135
- notionary/database/database_metadata_update_client.py +19 -0
- notionary/database/schemas.py +29 -0
- notionary/database/service.py +169 -0
- notionary/exceptions/__init__.py +33 -0
- notionary/exceptions/api.py +41 -0
- notionary/exceptions/base.py +2 -0
- notionary/exceptions/block_parsing.py +16 -0
- notionary/exceptions/data_source/__init__.py +6 -0
- notionary/exceptions/data_source/builder.py +182 -0
- notionary/exceptions/data_source/properties.py +34 -0
- notionary/exceptions/properties.py +58 -0
- notionary/exceptions/search.py +33 -0
- notionary/file_upload/client.py +18 -30
- notionary/file_upload/models.py +7 -8
- notionary/file_upload/{notion_file_upload.py → service.py} +29 -64
- notionary/http/client.py +205 -0
- notionary/http/models.py +49 -0
- notionary/page/blocks/client.py +1 -0
- notionary/page/content/factory.py +68 -0
- notionary/page/content/markdown/__init__.py +5 -0
- notionary/page/content/markdown/builder.py +304 -0
- notionary/page/content/markdown/nodes/__init__.py +54 -0
- notionary/page/content/markdown/nodes/audio.py +23 -0
- notionary/page/content/markdown/nodes/base.py +12 -0
- notionary/page/content/markdown/nodes/bookmark.py +25 -0
- notionary/page/content/markdown/nodes/breadcrumb.py +14 -0
- notionary/page/content/markdown/nodes/bulleted_list.py +18 -0
- notionary/page/content/markdown/nodes/callout.py +32 -0
- notionary/page/content/markdown/nodes/code.py +30 -0
- notionary/page/content/markdown/nodes/columns.py +51 -0
- notionary/page/content/markdown/nodes/divider.py +14 -0
- notionary/page/content/markdown/nodes/embed.py +23 -0
- notionary/page/content/markdown/nodes/equation.py +19 -0
- notionary/page/content/markdown/nodes/file.py +23 -0
- notionary/page/content/markdown/nodes/heading.py +16 -0
- notionary/page/content/markdown/nodes/image.py +23 -0
- notionary/page/content/markdown/nodes/mixins/caption.py +12 -0
- notionary/page/content/markdown/nodes/numbered_list.py +15 -0
- notionary/page/content/markdown/nodes/paragraph.py +14 -0
- notionary/page/content/markdown/nodes/pdf.py +23 -0
- notionary/page/content/markdown/nodes/quote.py +15 -0
- notionary/page/content/markdown/nodes/space.py +14 -0
- notionary/page/content/markdown/nodes/table.py +45 -0
- notionary/page/content/markdown/nodes/table_of_contents.py +14 -0
- notionary/page/content/markdown/nodes/todo.py +22 -0
- notionary/page/content/markdown/nodes/toggle.py +28 -0
- notionary/page/content/markdown/nodes/toggleable_heading.py +35 -0
- notionary/page/content/markdown/nodes/video.py +23 -0
- notionary/page/content/parser/context.py +49 -0
- notionary/page/content/parser/factory.py +219 -0
- notionary/page/content/parser/parsers/__init__.py +60 -0
- notionary/page/content/parser/parsers/audio.py +40 -0
- notionary/page/content/parser/parsers/base.py +30 -0
- notionary/page/content/parser/parsers/bookmark.py +33 -0
- notionary/page/content/parser/parsers/breadcrumb.py +33 -0
- notionary/page/content/parser/parsers/bulleted_list.py +41 -0
- notionary/page/content/parser/parsers/callout.py +129 -0
- notionary/page/content/parser/parsers/caption.py +55 -0
- notionary/page/content/parser/parsers/code.py +81 -0
- notionary/page/content/parser/parsers/column.py +117 -0
- notionary/page/content/parser/parsers/column_list.py +81 -0
- notionary/page/content/parser/parsers/divider.py +33 -0
- notionary/page/content/parser/parsers/embed.py +33 -0
- notionary/page/content/parser/parsers/equation.py +65 -0
- notionary/page/content/parser/parsers/file.py +42 -0
- notionary/page/content/parser/parsers/heading.py +58 -0
- notionary/page/content/parser/parsers/image.py +42 -0
- notionary/page/content/parser/parsers/numbered_list.py +45 -0
- notionary/page/content/parser/parsers/paragraph.py +36 -0
- notionary/page/content/parser/parsers/pdf.py +42 -0
- notionary/page/content/parser/parsers/quote.py +65 -0
- notionary/page/content/parser/parsers/space.py +35 -0
- notionary/page/content/parser/parsers/table.py +144 -0
- notionary/page/content/parser/parsers/table_of_contents.py +32 -0
- notionary/page/content/parser/parsers/todo.py +58 -0
- notionary/page/content/parser/parsers/toggle.py +127 -0
- notionary/page/content/parser/parsers/toggleable_heading.py +150 -0
- notionary/page/content/parser/parsers/video.py +42 -0
- notionary/page/content/parser/post_processing/handlers/__init__.py +5 -0
- notionary/page/content/parser/post_processing/handlers/rich_text_length.py +93 -0
- notionary/page/content/parser/post_processing/handlers/rich_text_length_truncation.py +93 -0
- notionary/page/content/parser/post_processing/port.py +9 -0
- notionary/page/content/parser/post_processing/service.py +16 -0
- notionary/page/content/parser/pre_processsing/handlers/__init__.py +9 -0
- notionary/page/content/parser/pre_processsing/handlers/column_syntax.py +80 -0
- notionary/page/content/parser/pre_processsing/handlers/port.py +7 -0
- notionary/page/content/parser/pre_processsing/handlers/whitespace.py +68 -0
- notionary/page/content/parser/pre_processsing/service.py +15 -0
- notionary/page/content/parser/service.py +69 -0
- notionary/page/content/renderer/context.py +48 -0
- notionary/page/content/renderer/factory.py +240 -0
- notionary/page/content/renderer/post_processing/handlers/__init__.py +5 -0
- notionary/page/content/renderer/post_processing/handlers/numbered_list_placeholdere.py +62 -0
- notionary/page/content/renderer/post_processing/port.py +7 -0
- notionary/page/content/renderer/post_processing/service.py +15 -0
- notionary/page/content/renderer/renderers/__init__.py +57 -0
- notionary/page/content/renderer/renderers/audio.py +31 -0
- notionary/page/content/renderer/renderers/base.py +31 -0
- notionary/page/content/renderer/renderers/bookmark.py +25 -0
- notionary/page/content/renderer/renderers/breadcrumb.py +21 -0
- notionary/page/content/renderer/renderers/bulleted_list.py +48 -0
- notionary/page/content/renderer/renderers/callout.py +65 -0
- notionary/page/content/renderer/renderers/captioned_block.py +58 -0
- notionary/page/content/renderer/renderers/code.py +34 -0
- notionary/page/content/renderer/renderers/column.py +44 -0
- notionary/page/content/renderer/renderers/column_list.py +31 -0
- notionary/page/content/renderer/renderers/divider.py +22 -0
- notionary/page/content/renderer/renderers/embed.py +25 -0
- notionary/page/content/renderer/renderers/equation.py +37 -0
- notionary/page/content/renderer/renderers/fallback.py +24 -0
- notionary/page/content/renderer/renderers/file.py +40 -0
- notionary/page/content/renderer/renderers/heading.py +69 -0
- notionary/page/content/renderer/renderers/image.py +31 -0
- notionary/page/content/renderer/renderers/numbered_list.py +41 -0
- notionary/page/content/renderer/renderers/paragraph.py +40 -0
- notionary/page/content/renderer/renderers/pdf.py +31 -0
- notionary/page/content/renderer/renderers/quote.py +49 -0
- notionary/page/content/renderer/renderers/table.py +115 -0
- notionary/page/content/renderer/renderers/table_of_contents.py +26 -0
- notionary/page/content/renderer/renderers/table_row.py +17 -0
- notionary/page/content/renderer/renderers/todo.py +56 -0
- notionary/page/content/renderer/renderers/toggle.py +53 -0
- notionary/page/content/renderer/renderers/toggleable_heading.py +78 -0
- notionary/page/content/renderer/renderers/video.py +31 -0
- notionary/page/content/renderer/service.py +50 -0
- notionary/page/content/service.py +65 -0
- notionary/page/content/syntax/models.py +68 -0
- notionary/page/content/syntax/service.py +453 -0
- notionary/page/page_context.py +7 -16
- notionary/page/page_http_client.py +15 -0
- notionary/page/page_metadata_update_client.py +19 -0
- notionary/page/properties/client.py +144 -0
- notionary/page/properties/factory.py +26 -0
- notionary/page/properties/models.py +307 -0
- notionary/page/properties/service.py +257 -0
- notionary/page/schemas.py +13 -0
- notionary/page/service.py +222 -0
- notionary/shared/entity/client.py +29 -0
- notionary/shared/entity/dto_parsers.py +53 -0
- notionary/shared/entity/entity_metadata_update_client.py +41 -0
- notionary/shared/entity/schemas.py +45 -0
- notionary/shared/entity/service.py +171 -0
- notionary/shared/models/cover.py +20 -0
- notionary/shared/models/file.py +21 -0
- notionary/shared/models/icon.py +28 -0
- notionary/shared/models/parent.py +41 -0
- notionary/shared/properties/type.py +30 -0
- notionary/user/__init__.py +4 -8
- notionary/user/base.py +89 -0
- notionary/user/bot.py +70 -0
- notionary/user/client.py +22 -111
- notionary/user/person.py +41 -0
- notionary/user/schemas.py +67 -0
- notionary/user/service.py +65 -0
- notionary/utils/async_retry.py +39 -0
- notionary/utils/date.py +51 -0
- notionary/utils/fuzzy.py +56 -0
- notionary/{util/logging_mixin.py → utils/mixins/logging.py} +4 -16
- notionary/utils/pagination.py +50 -0
- notionary/utils/singleton.py +13 -0
- notionary/utils/uuid_utils.py +20 -0
- notionary/workspace/__init__.py +3 -0
- notionary/workspace/client.py +62 -0
- notionary/workspace/query/builder.py +60 -0
- notionary/workspace/query/models.py +60 -0
- notionary/workspace/query/service.py +93 -0
- notionary/workspace/schemas.py +21 -0
- notionary/workspace/service.py +116 -0
- {notionary-0.2.27.dist-info → notionary-0.2.28.dist-info}/METADATA +54 -49
- notionary-0.2.28.dist-info/RECORD +200 -0
- {notionary-0.2.27.dist-info → notionary-0.2.28.dist-info}/WHEEL +1 -1
- {notionary-0.2.27.dist-info → notionary-0.2.28.dist-info/licenses}/LICENSE +9 -9
- notionary/base_notion_client.py +0 -219
- notionary/blocks/__init__.py +0 -5
- notionary/blocks/_bootstrap.py +0 -271
- notionary/blocks/audio/__init__.py +0 -11
- notionary/blocks/audio/audio_element.py +0 -158
- notionary/blocks/audio/audio_markdown_node.py +0 -24
- notionary/blocks/audio/audio_models.py +0 -10
- notionary/blocks/base_block_element.py +0 -42
- notionary/blocks/bookmark/__init__.py +0 -12
- notionary/blocks/bookmark/bookmark_element.py +0 -83
- notionary/blocks/bookmark/bookmark_markdown_node.py +0 -28
- notionary/blocks/bookmark/bookmark_models.py +0 -15
- notionary/blocks/breadcrumbs/__init__.py +0 -15
- notionary/blocks/breadcrumbs/breadcrumb_element.py +0 -39
- notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +0 -13
- notionary/blocks/breadcrumbs/breadcrumb_models.py +0 -12
- notionary/blocks/bulleted_list/__init__.py +0 -15
- notionary/blocks/bulleted_list/bulleted_list_element.py +0 -74
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +0 -20
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -17
- notionary/blocks/callout/__init__.py +0 -12
- notionary/blocks/callout/callout_element.py +0 -99
- notionary/blocks/callout/callout_markdown_node.py +0 -19
- notionary/blocks/callout/callout_models.py +0 -33
- notionary/blocks/child_database/__init__.py +0 -14
- notionary/blocks/child_database/child_database_element.py +0 -59
- notionary/blocks/child_database/child_database_models.py +0 -12
- notionary/blocks/child_page/__init__.py +0 -9
- notionary/blocks/child_page/child_page_element.py +0 -94
- notionary/blocks/child_page/child_page_models.py +0 -12
- notionary/blocks/code/__init__.py +0 -11
- notionary/blocks/code/code_element.py +0 -149
- notionary/blocks/code/code_markdown_node.py +0 -80
- notionary/blocks/code/code_models.py +0 -94
- notionary/blocks/column/__init__.py +0 -25
- notionary/blocks/column/column_element.py +0 -65
- notionary/blocks/column/column_list_element.py +0 -52
- notionary/blocks/column/column_list_markdown_node.py +0 -34
- notionary/blocks/column/column_markdown_node.py +0 -42
- notionary/blocks/column/column_models.py +0 -26
- notionary/blocks/divider/__init__.py +0 -12
- notionary/blocks/divider/divider_element.py +0 -41
- notionary/blocks/divider/divider_markdown_node.py +0 -11
- notionary/blocks/divider/divider_models.py +0 -12
- notionary/blocks/embed/__init__.py +0 -12
- notionary/blocks/embed/embed_element.py +0 -98
- notionary/blocks/embed/embed_markdown_node.py +0 -19
- notionary/blocks/embed/embed_models.py +0 -14
- notionary/blocks/equation/__init__.py +0 -13
- notionary/blocks/equation/equation_element.py +0 -133
- notionary/blocks/equation/equation_element_markdown_node.py +0 -23
- notionary/blocks/equation/equation_models.py +0 -11
- notionary/blocks/file/__init__.py +0 -23
- notionary/blocks/file/file_element.py +0 -133
- notionary/blocks/file/file_element_markdown_node.py +0 -24
- notionary/blocks/file/file_element_models.py +0 -39
- notionary/blocks/heading/__init__.py +0 -19
- notionary/blocks/heading/heading_element.py +0 -112
- notionary/blocks/heading/heading_markdown_node.py +0 -16
- notionary/blocks/heading/heading_models.py +0 -29
- notionary/blocks/image_block/__init__.py +0 -11
- notionary/blocks/image_block/image_element.py +0 -130
- notionary/blocks/image_block/image_markdown_node.py +0 -25
- notionary/blocks/image_block/image_models.py +0 -10
- notionary/blocks/markdown/markdown_builder.py +0 -525
- notionary/blocks/markdown/markdown_document_model.py +0 -0
- notionary/blocks/markdown/markdown_node.py +0 -25
- notionary/blocks/mixins/captions/__init__.py +0 -4
- notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +0 -31
- notionary/blocks/mixins/captions/caption_mixin.py +0 -92
- notionary/blocks/mixins/file_upload/__init__.py +0 -3
- notionary/blocks/mixins/file_upload/file_upload_mixin.py +0 -320
- notionary/blocks/models.py +0 -174
- notionary/blocks/numbered_list/__init__.py +0 -16
- notionary/blocks/numbered_list/numbered_list_element.py +0 -65
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +0 -17
- notionary/blocks/numbered_list/numbered_list_models.py +0 -17
- notionary/blocks/paragraph/__init__.py +0 -15
- notionary/blocks/paragraph/paragraph_element.py +0 -58
- notionary/blocks/paragraph/paragraph_markdown_node.py +0 -16
- notionary/blocks/paragraph/paragraph_models.py +0 -16
- notionary/blocks/pdf/__init__.py +0 -11
- notionary/blocks/pdf/pdf_element.py +0 -146
- notionary/blocks/pdf/pdf_markdown_node.py +0 -24
- notionary/blocks/pdf/pdf_models.py +0 -11
- notionary/blocks/quote/__init__.py +0 -14
- notionary/blocks/quote/quote_element.py +0 -75
- notionary/blocks/quote/quote_markdown_node.py +0 -16
- notionary/blocks/quote/quote_models.py +0 -18
- notionary/blocks/registry/__init__.py +0 -3
- notionary/blocks/registry/block_registry.py +0 -150
- notionary/blocks/rich_text/__init__.py +0 -33
- notionary/blocks/rich_text/rich_text_models.py +0 -221
- notionary/blocks/rich_text/text_inline_formatter.py +0 -456
- notionary/blocks/syntax_prompt_builder.py +0 -137
- notionary/blocks/table/__init__.py +0 -19
- notionary/blocks/table/table_element.py +0 -225
- notionary/blocks/table/table_markdown_node.py +0 -42
- notionary/blocks/table/table_models.py +0 -28
- notionary/blocks/table_of_contents/__init__.py +0 -17
- notionary/blocks/table_of_contents/table_of_contents_element.py +0 -80
- notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +0 -21
- notionary/blocks/table_of_contents/table_of_contents_models.py +0 -18
- notionary/blocks/todo/__init__.py +0 -12
- notionary/blocks/todo/todo_element.py +0 -81
- notionary/blocks/todo/todo_markdown_node.py +0 -21
- notionary/blocks/todo/todo_models.py +0 -18
- notionary/blocks/toggle/__init__.py +0 -12
- notionary/blocks/toggle/toggle_element.py +0 -112
- notionary/blocks/toggle/toggle_markdown_node.py +0 -31
- notionary/blocks/toggle/toggle_models.py +0 -17
- notionary/blocks/toggleable_heading/__init__.py +0 -11
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +0 -115
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +0 -34
- notionary/blocks/types.py +0 -130
- notionary/blocks/video/__init__.py +0 -11
- notionary/blocks/video/video_element.py +0 -187
- notionary/blocks/video/video_element_models.py +0 -10
- notionary/blocks/video/video_markdown_node.py +0 -26
- notionary/comments/__init__.py +0 -26
- notionary/database/__init__.py +0 -4
- notionary/database/database.py +0 -480
- notionary/database/database_filter_builder.py +0 -173
- notionary/database/database_provider.py +0 -227
- notionary/database/exceptions.py +0 -13
- notionary/database/factory.py +0 -0
- notionary/database/models.py +0 -337
- notionary/database/notion_database.py +0 -487
- notionary/file_upload/__init__.py +0 -7
- notionary/page/client.py +0 -124
- notionary/page/markdown_whitespace_processor.py +0 -129
- notionary/page/models.py +0 -322
- notionary/page/notion_page.py +0 -712
- notionary/page/page_content_deleting_service.py +0 -117
- notionary/page/page_content_writer.py +0 -80
- notionary/page/property_formatter.py +0 -99
- notionary/page/reader/handler/__init__.py +0 -19
- notionary/page/reader/handler/base_block_renderer.py +0 -44
- notionary/page/reader/handler/block_processing_context.py +0 -35
- notionary/page/reader/handler/block_rendering_context.py +0 -48
- notionary/page/reader/handler/column_list_renderer.py +0 -51
- notionary/page/reader/handler/column_renderer.py +0 -60
- notionary/page/reader/handler/equation_renderer.py +0 -0
- notionary/page/reader/handler/line_renderer.py +0 -73
- notionary/page/reader/handler/numbered_list_renderer.py +0 -85
- notionary/page/reader/handler/toggle_renderer.py +0 -69
- notionary/page/reader/handler/toggleable_heading_renderer.py +0 -89
- notionary/page/reader/page_content_retriever.py +0 -81
- notionary/page/search_filter_builder.py +0 -132
- notionary/page/utils.py +0 -60
- notionary/page/writer/handler/__init__.py +0 -24
- notionary/page/writer/handler/code_handler.py +0 -72
- notionary/page/writer/handler/column_handler.py +0 -141
- notionary/page/writer/handler/column_list_handler.py +0 -139
- notionary/page/writer/handler/equation_handler.py +0 -74
- notionary/page/writer/handler/line_handler.py +0 -35
- notionary/page/writer/handler/line_processing_context.py +0 -54
- notionary/page/writer/handler/regular_line_handler.py +0 -86
- notionary/page/writer/handler/table_handler.py +0 -66
- notionary/page/writer/handler/toggle_handler.py +0 -159
- notionary/page/writer/handler/toggleable_heading_handler.py +0 -174
- notionary/page/writer/markdown_to_notion_converter.py +0 -139
- notionary/page/writer/markdown_to_notion_converter_context.py +0 -30
- notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
- notionary/page/writer/notion_text_length_processor.py +0 -150
- notionary/schemas/__init__.py +0 -3
- notionary/schemas/base.py +0 -73
- notionary/shared/__init__.py +0 -3
- notionary/shared/name_to_id_resolver.py +0 -203
- notionary/telemetry/__init__.py +0 -19
- notionary/telemetry/service.py +0 -136
- notionary/telemetry/views.py +0 -73
- notionary/user/base_notion_user.py +0 -53
- notionary/user/models.py +0 -84
- notionary/user/notion_bot_user.py +0 -226
- notionary/user/notion_user.py +0 -255
- notionary/user/notion_user_manager.py +0 -101
- notionary/util/__init__.py +0 -15
- notionary/util/concurrency_limiter.py +0 -0
- notionary/util/factory_decorator.py +0 -0
- notionary/util/factory_only.py +0 -37
- notionary/util/fuzzy.py +0 -75
- notionary/util/page_id_utils.py +0 -27
- notionary/util/singleton.py +0 -18
- notionary/util/singleton_metaclass.py +0 -22
- notionary/workspace.py +0 -105
- notionary-0.2.27.dist-info/RECORD +0 -202
@@ -0,0 +1,266 @@
|
|
1
|
+
import re
|
2
|
+
from collections.abc import Callable
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from re import Match
|
5
|
+
from typing import ClassVar
|
6
|
+
|
7
|
+
from notionary.blocks.rich_text.models import MentionType, RichText, RichTextType, TextAnnotations
|
8
|
+
from notionary.blocks.rich_text.name_id_resolver import (
|
9
|
+
DatabaseNameIdResolver,
|
10
|
+
NameIdResolver,
|
11
|
+
PageNameIdResolver,
|
12
|
+
PersonNameIdResolver,
|
13
|
+
)
|
14
|
+
from notionary.blocks.rich_text.rich_text_patterns import RichTextPatterns
|
15
|
+
from notionary.blocks.schemas import BlockColor
|
16
|
+
|
17
|
+
|
18
|
+
@dataclass
|
19
|
+
class PatternMatch:
|
20
|
+
match: Match
|
21
|
+
handler: Callable[[Match], RichText | list[RichText]]
|
22
|
+
position: int
|
23
|
+
|
24
|
+
@property
|
25
|
+
def matched_text(self) -> str:
|
26
|
+
return self.match.group(0)
|
27
|
+
|
28
|
+
@property
|
29
|
+
def end_position(self) -> int:
|
30
|
+
return self.position + len(self.matched_text)
|
31
|
+
|
32
|
+
|
33
|
+
@dataclass
|
34
|
+
class PatternHandler:
|
35
|
+
pattern: str
|
36
|
+
handler: Callable[[Match], RichText | list[RichText]]
|
37
|
+
|
38
|
+
|
39
|
+
class MarkdownRichTextConverter:
|
40
|
+
VALID_COLORS: ClassVar[set[str]] = {color.value for color in BlockColor}
|
41
|
+
|
42
|
+
def __init__(
|
43
|
+
self,
|
44
|
+
*,
|
45
|
+
page_resolver: NameIdResolver | None = None,
|
46
|
+
database_resolver: NameIdResolver | None = None,
|
47
|
+
person_resolver: NameIdResolver | None = None,
|
48
|
+
):
|
49
|
+
self.page_resolver = page_resolver or PageNameIdResolver()
|
50
|
+
self.database_resolver = database_resolver or DatabaseNameIdResolver()
|
51
|
+
self.person_resolver = person_resolver or PersonNameIdResolver()
|
52
|
+
self.format_handlers = self._setup_format_handlers()
|
53
|
+
|
54
|
+
def _setup_format_handlers(self) -> list[PatternHandler]:
|
55
|
+
return [
|
56
|
+
PatternHandler(RichTextPatterns.BOLD, self._handle_bold_pattern),
|
57
|
+
PatternHandler(RichTextPatterns.ITALIC, self._handle_italic_pattern),
|
58
|
+
PatternHandler(RichTextPatterns.ITALIC_UNDERSCORE, self._handle_italic_pattern),
|
59
|
+
PatternHandler(RichTextPatterns.UNDERLINE, self._handle_underline_pattern),
|
60
|
+
PatternHandler(RichTextPatterns.STRIKETHROUGH, self._handle_strikethrough_pattern),
|
61
|
+
PatternHandler(RichTextPatterns.CODE, self._handle_code_pattern),
|
62
|
+
PatternHandler(RichTextPatterns.LINK, self._handle_link_pattern),
|
63
|
+
PatternHandler(RichTextPatterns.INLINE_EQUATION, self._handle_equation_pattern),
|
64
|
+
PatternHandler(RichTextPatterns.COLOR, self._handle_color_pattern),
|
65
|
+
PatternHandler(RichTextPatterns.PAGE_MENTION, self._handle_page_mention_pattern),
|
66
|
+
PatternHandler(RichTextPatterns.DATABASE_MENTION, self._handle_database_mention_pattern),
|
67
|
+
PatternHandler(RichTextPatterns.USER_MENTION, self._handle_user_mention_pattern),
|
68
|
+
]
|
69
|
+
|
70
|
+
async def to_rich_text(self, text: str) -> list[RichText]:
|
71
|
+
if not text:
|
72
|
+
return []
|
73
|
+
return await self._split_text_into_segments(text)
|
74
|
+
|
75
|
+
async def _split_text_into_segments(self, text: str) -> list[RichText]:
|
76
|
+
segments: list[RichText] = []
|
77
|
+
remaining_text = text
|
78
|
+
|
79
|
+
while remaining_text:
|
80
|
+
pattern_match = self._find_earliest_pattern_match(remaining_text)
|
81
|
+
|
82
|
+
if not pattern_match:
|
83
|
+
segments.append(RichText.from_plain_text(remaining_text))
|
84
|
+
break
|
85
|
+
|
86
|
+
plain_text_before = remaining_text[: pattern_match.position]
|
87
|
+
if plain_text_before:
|
88
|
+
segments.append(RichText.from_plain_text(plain_text_before))
|
89
|
+
|
90
|
+
pattern_result = await self._process_pattern_match(pattern_match)
|
91
|
+
self._add_pattern_result_to_segments(segments, pattern_result)
|
92
|
+
|
93
|
+
remaining_text = remaining_text[pattern_match.end_position :]
|
94
|
+
|
95
|
+
return segments
|
96
|
+
|
97
|
+
def _find_earliest_pattern_match(self, text: str) -> PatternMatch | None:
|
98
|
+
"""Find the pattern that appears earliest in the text."""
|
99
|
+
earliest_match = None
|
100
|
+
earliest_position = len(text)
|
101
|
+
|
102
|
+
for pattern_handler in self.format_handlers:
|
103
|
+
match = re.search(pattern_handler.pattern, text)
|
104
|
+
if match and match.start() < earliest_position:
|
105
|
+
earliest_match = PatternMatch(match=match, handler=pattern_handler.handler, position=match.start())
|
106
|
+
earliest_position = match.start()
|
107
|
+
|
108
|
+
return earliest_match
|
109
|
+
|
110
|
+
async def _process_pattern_match(self, pattern_match: PatternMatch) -> RichText | list[RichText]:
|
111
|
+
handler_method = pattern_match.handler
|
112
|
+
|
113
|
+
if self._is_async_handler(handler_method):
|
114
|
+
return await handler_method(pattern_match.match)
|
115
|
+
else:
|
116
|
+
return handler_method(pattern_match.match)
|
117
|
+
|
118
|
+
def _is_async_handler(self, handler_method: Callable) -> bool:
|
119
|
+
async_handlers = {
|
120
|
+
self._handle_page_mention_pattern,
|
121
|
+
self._handle_database_mention_pattern,
|
122
|
+
self._handle_color_pattern, # Color pattern needs async for recursive parsing
|
123
|
+
self._handle_user_mention_pattern,
|
124
|
+
}
|
125
|
+
return handler_method in async_handlers
|
126
|
+
|
127
|
+
def _add_pattern_result_to_segments(
|
128
|
+
self, segments: list[RichText], pattern_result: RichText | list[RichText]
|
129
|
+
) -> None:
|
130
|
+
if isinstance(pattern_result, list):
|
131
|
+
segments.extend(pattern_result)
|
132
|
+
elif pattern_result:
|
133
|
+
segments.append(pattern_result)
|
134
|
+
|
135
|
+
async def _handle_color_pattern(self, match: Match) -> list[RichText]:
|
136
|
+
color, content = match.group(1).lower(), match.group(2)
|
137
|
+
|
138
|
+
if color not in self.VALID_COLORS:
|
139
|
+
return [RichText.from_plain_text(f"({match.group(1)}:{content})")]
|
140
|
+
|
141
|
+
parsed_segments = await self._split_text_into_segments(content)
|
142
|
+
|
143
|
+
colored_segments = []
|
144
|
+
for segment in parsed_segments:
|
145
|
+
if segment.type == RichTextType.TEXT:
|
146
|
+
colored_segment = self._apply_color_to_text_segment(segment, color)
|
147
|
+
colored_segments.append(colored_segment)
|
148
|
+
else:
|
149
|
+
colored_segments.append(segment)
|
150
|
+
|
151
|
+
return colored_segments
|
152
|
+
|
153
|
+
def _apply_color_to_text_segment(self, segment: RichText, color: str) -> RichText:
|
154
|
+
if segment.type != RichTextType.TEXT:
|
155
|
+
return segment
|
156
|
+
|
157
|
+
has_link = segment.text and segment.text.link
|
158
|
+
|
159
|
+
if has_link:
|
160
|
+
return self._apply_color_to_link_segment(segment, color)
|
161
|
+
else:
|
162
|
+
return self._apply_color_to_plain_text_segment(segment, color)
|
163
|
+
|
164
|
+
def _apply_color_to_link_segment(self, segment: RichText, color: str) -> RichText:
|
165
|
+
formatting = self._extract_formatting_attributes(segment.annotations)
|
166
|
+
|
167
|
+
return RichText.for_link(segment.plain_text, segment.text.link.url, color=color, **formatting)
|
168
|
+
|
169
|
+
def _apply_color_to_plain_text_segment(self, segment: RichText, color: str) -> RichText:
|
170
|
+
if segment.type != RichTextType.TEXT:
|
171
|
+
return segment
|
172
|
+
|
173
|
+
formatting = self._extract_formatting_attributes(segment.annotations)
|
174
|
+
|
175
|
+
return RichText.from_plain_text(segment.plain_text, color=color, **formatting)
|
176
|
+
|
177
|
+
def _extract_formatting_attributes(self, annotations: TextAnnotations) -> dict[str, bool]:
|
178
|
+
if not annotations:
|
179
|
+
return {
|
180
|
+
"bold": False,
|
181
|
+
"italic": False,
|
182
|
+
"strikethrough": False,
|
183
|
+
"underline": False,
|
184
|
+
"code": False,
|
185
|
+
}
|
186
|
+
|
187
|
+
return {
|
188
|
+
"bold": annotations.bold,
|
189
|
+
"italic": annotations.italic,
|
190
|
+
"strikethrough": annotations.strikethrough,
|
191
|
+
"underline": annotations.underline,
|
192
|
+
"code": annotations.code,
|
193
|
+
}
|
194
|
+
|
195
|
+
async def _handle_page_mention_pattern(self, match: Match) -> RichText:
|
196
|
+
identifier = match.group(1)
|
197
|
+
return await self._create_mention_or_fallback(
|
198
|
+
identifier=identifier,
|
199
|
+
resolve_func=self.page_resolver.resolve_name_to_id,
|
200
|
+
create_mention_func=RichText.mention_page,
|
201
|
+
mention_type=MentionType.PAGE,
|
202
|
+
)
|
203
|
+
|
204
|
+
async def _handle_database_mention_pattern(self, match: Match) -> RichText:
|
205
|
+
identifier = match.group(1)
|
206
|
+
return await self._create_mention_or_fallback(
|
207
|
+
identifier=identifier,
|
208
|
+
resolve_func=self.database_resolver.resolve_name_to_id,
|
209
|
+
create_mention_func=RichText.mention_database,
|
210
|
+
mention_type=MentionType.DATABASE,
|
211
|
+
)
|
212
|
+
|
213
|
+
async def _handle_user_mention_pattern(self, match: Match) -> RichText:
|
214
|
+
identifier = match.group(1)
|
215
|
+
return await self._create_mention_or_fallback(
|
216
|
+
identifier=identifier,
|
217
|
+
resolve_func=self.person_resolver.resolve_name_to_id,
|
218
|
+
create_mention_func=RichText.mention_user,
|
219
|
+
mention_type=MentionType.USER,
|
220
|
+
)
|
221
|
+
|
222
|
+
async def _create_mention_or_fallback(
|
223
|
+
self,
|
224
|
+
identifier: str,
|
225
|
+
resolve_func: Callable[[str], str | None],
|
226
|
+
create_mention_func: Callable[[str], RichText],
|
227
|
+
mention_type: MentionType,
|
228
|
+
) -> RichText:
|
229
|
+
try:
|
230
|
+
resolved_id = await resolve_func(identifier)
|
231
|
+
|
232
|
+
if resolved_id:
|
233
|
+
return create_mention_func(resolved_id)
|
234
|
+
else:
|
235
|
+
return self._create_unresolved_mention_fallback(identifier, mention_type)
|
236
|
+
|
237
|
+
except Exception:
|
238
|
+
# If resolution throws an error, fallback to plain text
|
239
|
+
return self._create_unresolved_mention_fallback(identifier, mention_type)
|
240
|
+
|
241
|
+
def _create_unresolved_mention_fallback(self, identifier: str, mention_type: MentionType) -> RichText:
|
242
|
+
fallback_text = f"@{mention_type.value}[{identifier}]"
|
243
|
+
return RichText.for_caption(fallback_text)
|
244
|
+
|
245
|
+
def _handle_bold_pattern(self, match: Match) -> RichText:
|
246
|
+
return RichText.from_plain_text(match.group(1), bold=True)
|
247
|
+
|
248
|
+
def _handle_italic_pattern(self, match: Match) -> RichText:
|
249
|
+
return RichText.from_plain_text(match.group(1), italic=True)
|
250
|
+
|
251
|
+
def _handle_underline_pattern(self, match: Match) -> RichText:
|
252
|
+
return RichText.from_plain_text(match.group(1), underline=True)
|
253
|
+
|
254
|
+
def _handle_strikethrough_pattern(self, match: Match) -> RichText:
|
255
|
+
return RichText.from_plain_text(match.group(1), strikethrough=True)
|
256
|
+
|
257
|
+
def _handle_code_pattern(self, match: Match) -> RichText:
|
258
|
+
return RichText.from_plain_text(match.group(1), code=True)
|
259
|
+
|
260
|
+
def _handle_link_pattern(self, match: Match) -> RichText:
|
261
|
+
link_text, url = match.group(1), match.group(2)
|
262
|
+
return RichText.for_link(link_text, url)
|
263
|
+
|
264
|
+
def _handle_equation_pattern(self, match: Match) -> RichText:
|
265
|
+
expression = match.group(1)
|
266
|
+
return RichText.equation_inline(expression)
|
@@ -0,0 +1,164 @@
|
|
1
|
+
from enum import StrEnum
|
2
|
+
from typing import Self
|
3
|
+
|
4
|
+
from pydantic import BaseModel
|
5
|
+
|
6
|
+
from notionary.blocks.enums import BlockColor
|
7
|
+
|
8
|
+
|
9
|
+
class RichTextType(StrEnum):
|
10
|
+
TEXT = "text"
|
11
|
+
MENTION = "mention"
|
12
|
+
EQUATION = "equation"
|
13
|
+
|
14
|
+
|
15
|
+
class MentionType(StrEnum):
|
16
|
+
USER = "user"
|
17
|
+
PAGE = "page"
|
18
|
+
DATABASE = "database"
|
19
|
+
DATE = "date"
|
20
|
+
LINK_PREVIEW = "link_preview"
|
21
|
+
TEMPLATE_MENTION = "template_mention"
|
22
|
+
|
23
|
+
|
24
|
+
class TemplateMentionType(StrEnum):
|
25
|
+
USER = "template_mention_user"
|
26
|
+
DATE = "template_mention_date"
|
27
|
+
|
28
|
+
|
29
|
+
class TextAnnotations(BaseModel):
|
30
|
+
bold: bool = False
|
31
|
+
italic: bool = False
|
32
|
+
strikethrough: bool = False
|
33
|
+
underline: bool = False
|
34
|
+
code: bool = False
|
35
|
+
color: BlockColor | None = None
|
36
|
+
|
37
|
+
|
38
|
+
class LinkObject(BaseModel):
|
39
|
+
url: str
|
40
|
+
|
41
|
+
|
42
|
+
class TextContent(BaseModel):
|
43
|
+
content: str
|
44
|
+
link: LinkObject | None = None
|
45
|
+
|
46
|
+
|
47
|
+
class EquationObject(BaseModel):
|
48
|
+
expression: str
|
49
|
+
|
50
|
+
|
51
|
+
class MentionUserRef(BaseModel):
|
52
|
+
id: str # Notion user id
|
53
|
+
|
54
|
+
|
55
|
+
class MentionPageRef(BaseModel):
|
56
|
+
id: str
|
57
|
+
|
58
|
+
|
59
|
+
class MentionDatabaseRef(BaseModel):
|
60
|
+
id: str
|
61
|
+
|
62
|
+
|
63
|
+
class MentionLinkPreview(BaseModel):
|
64
|
+
url: str
|
65
|
+
|
66
|
+
|
67
|
+
class MentionDate(BaseModel):
|
68
|
+
# entspricht Notion date object (start Pflicht, end/time_zone optional)
|
69
|
+
start: str # ISO 8601 date or datetime
|
70
|
+
end: str | None = None
|
71
|
+
time_zone: str | None = None
|
72
|
+
|
73
|
+
|
74
|
+
class MentionTemplateMention(BaseModel):
|
75
|
+
# Notion hat zwei Template-Mention-Typen
|
76
|
+
type: TemplateMentionType
|
77
|
+
|
78
|
+
|
79
|
+
class MentionObject(BaseModel):
|
80
|
+
type: MentionType
|
81
|
+
user: MentionUserRef | None = None
|
82
|
+
page: MentionPageRef | None = None
|
83
|
+
database: MentionDatabaseRef | None = None
|
84
|
+
date: MentionDate | None = None
|
85
|
+
link_preview: MentionLinkPreview | None = None
|
86
|
+
template_mention: MentionTemplateMention | None = None
|
87
|
+
|
88
|
+
|
89
|
+
class RichText(BaseModel):
|
90
|
+
type: RichTextType = RichTextType.TEXT
|
91
|
+
|
92
|
+
text: TextContent | None = None
|
93
|
+
annotations: TextAnnotations | None = None
|
94
|
+
plain_text: str = ""
|
95
|
+
href: str | None = None
|
96
|
+
|
97
|
+
mention: MentionObject | None = None
|
98
|
+
|
99
|
+
equation: EquationObject | None = None
|
100
|
+
|
101
|
+
@classmethod
|
102
|
+
def from_plain_text(cls, content: str, **ann) -> Self:
|
103
|
+
return cls(
|
104
|
+
type=RichTextType.TEXT,
|
105
|
+
text=TextContent(content=content),
|
106
|
+
annotations=TextAnnotations(**ann) if ann else TextAnnotations(),
|
107
|
+
plain_text=content,
|
108
|
+
)
|
109
|
+
|
110
|
+
@classmethod
|
111
|
+
def for_caption(cls, content: str) -> Self:
|
112
|
+
return cls(
|
113
|
+
type=RichTextType.TEXT,
|
114
|
+
text=TextContent(content=content),
|
115
|
+
annotations=None,
|
116
|
+
plain_text=content,
|
117
|
+
)
|
118
|
+
|
119
|
+
@classmethod
|
120
|
+
def for_code_block(cls, content: str) -> Self:
|
121
|
+
# keine annotations setzen → Notion Code-Highlight bleibt an
|
122
|
+
return cls.for_caption(content)
|
123
|
+
|
124
|
+
@classmethod
|
125
|
+
def for_link(cls, content: str, url: str, **ann) -> Self:
|
126
|
+
return cls(
|
127
|
+
type=RichTextType.TEXT,
|
128
|
+
text=TextContent(content=content, link=LinkObject(url=url)),
|
129
|
+
annotations=TextAnnotations(**ann) if ann else TextAnnotations(),
|
130
|
+
plain_text=content,
|
131
|
+
)
|
132
|
+
|
133
|
+
@classmethod
|
134
|
+
def mention_user(cls, user_id: str) -> Self:
|
135
|
+
return cls(
|
136
|
+
type=RichTextType.MENTION,
|
137
|
+
mention=MentionObject(type=MentionType.USER, user=MentionUserRef(id=user_id)),
|
138
|
+
annotations=TextAnnotations(),
|
139
|
+
)
|
140
|
+
|
141
|
+
@classmethod
|
142
|
+
def mention_page(cls, page_id: str) -> Self:
|
143
|
+
return cls(
|
144
|
+
type=RichTextType.MENTION,
|
145
|
+
mention=MentionObject(type=MentionType.PAGE, page=MentionPageRef(id=page_id)),
|
146
|
+
annotations=TextAnnotations(),
|
147
|
+
)
|
148
|
+
|
149
|
+
@classmethod
|
150
|
+
def mention_database(cls, database_id: str) -> Self:
|
151
|
+
return cls(
|
152
|
+
type=RichTextType.MENTION,
|
153
|
+
mention=MentionObject(type=MentionType.DATABASE, database=MentionDatabaseRef(id=database_id)),
|
154
|
+
annotations=TextAnnotations(),
|
155
|
+
)
|
156
|
+
|
157
|
+
@classmethod
|
158
|
+
def equation_inline(cls, expression: str) -> Self:
|
159
|
+
return cls(
|
160
|
+
type=RichTextType.EQUATION,
|
161
|
+
equation=EquationObject(expression=expression),
|
162
|
+
annotations=TextAnnotations(),
|
163
|
+
plain_text=expression,
|
164
|
+
)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
from .database import DatabaseNameIdResolver
|
2
|
+
from .page import PageNameIdResolver
|
3
|
+
from .person import PersonNameIdResolver
|
4
|
+
from .port import NameIdResolver
|
5
|
+
|
6
|
+
__all__ = [
|
7
|
+
"DatabaseNameIdResolver",
|
8
|
+
"NameIdResolver",
|
9
|
+
"PageNameIdResolver",
|
10
|
+
"PersonNameIdResolver",
|
11
|
+
]
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from typing import override
|
2
|
+
|
3
|
+
from notionary.blocks.rich_text.name_id_resolver.port import NameIdResolver
|
4
|
+
from notionary.workspace.query.service import WorkspaceQueryService
|
5
|
+
|
6
|
+
|
7
|
+
class DatabaseNameIdResolver(NameIdResolver):
|
8
|
+
def __init__(self, search_service: WorkspaceQueryService | None = None) -> None:
|
9
|
+
self.search_service = search_service or WorkspaceQueryService()
|
10
|
+
|
11
|
+
@override
|
12
|
+
async def resolve_name_to_id(self, name: str) -> str | None:
|
13
|
+
if not name:
|
14
|
+
return None
|
15
|
+
|
16
|
+
cleaned_name = name.strip()
|
17
|
+
database = await self.search_service.find_database(query=cleaned_name)
|
18
|
+
return database.id if database else None
|
19
|
+
|
20
|
+
@override
|
21
|
+
async def resolve_id_to_name(self, database_id: str) -> str | None:
|
22
|
+
if not database_id:
|
23
|
+
return None
|
24
|
+
|
25
|
+
try:
|
26
|
+
from notionary import NotionDatabase
|
27
|
+
|
28
|
+
database = await NotionDatabase.from_id(database_id)
|
29
|
+
return database.title if database else None
|
30
|
+
except Exception:
|
31
|
+
return None
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from typing import override
|
2
|
+
|
3
|
+
from notionary.blocks.rich_text.name_id_resolver.port import NameIdResolver
|
4
|
+
from notionary.workspace.query.service import WorkspaceQueryService
|
5
|
+
|
6
|
+
|
7
|
+
class PageNameIdResolver(NameIdResolver):
|
8
|
+
def __init__(self, search_service: WorkspaceQueryService | None = None) -> None:
|
9
|
+
self.search_service = search_service or WorkspaceQueryService()
|
10
|
+
|
11
|
+
@override
|
12
|
+
async def resolve_name_to_id(self, name: str) -> str | None:
|
13
|
+
if not name:
|
14
|
+
return None
|
15
|
+
|
16
|
+
cleaned_name = name.strip()
|
17
|
+
return await self._resolve_page_id(cleaned_name)
|
18
|
+
|
19
|
+
@override
|
20
|
+
async def resolve_id_to_name(self, page_id: str) -> str | None:
|
21
|
+
if not page_id:
|
22
|
+
return None
|
23
|
+
|
24
|
+
try:
|
25
|
+
from notionary import NotionPage
|
26
|
+
|
27
|
+
page = await NotionPage.from_id(page_id)
|
28
|
+
return page.title if page else None
|
29
|
+
except Exception:
|
30
|
+
return None
|
31
|
+
|
32
|
+
async def _resolve_page_id(self, name: str) -> str | None:
|
33
|
+
page = await self.search_service.find_page(query=name)
|
34
|
+
return page.id if page else None
|
@@ -0,0 +1,37 @@
|
|
1
|
+
from typing import override
|
2
|
+
|
3
|
+
from notionary.blocks.rich_text.name_id_resolver.port import NameIdResolver
|
4
|
+
from notionary.user.client import UserHttpClient
|
5
|
+
from notionary.user.person import PersonUser
|
6
|
+
|
7
|
+
|
8
|
+
class PersonNameIdResolver(NameIdResolver):
|
9
|
+
def __init__(self, person_user_factory=None, http_client: UserHttpClient | None = None) -> None:
|
10
|
+
if person_user_factory is None:
|
11
|
+
person_user_factory = PersonUser
|
12
|
+
self.person_user_factory = person_user_factory
|
13
|
+
self.http_client = http_client
|
14
|
+
|
15
|
+
@override
|
16
|
+
async def resolve_name_to_id(self, name: str | None) -> str | None:
|
17
|
+
if not name or not name.strip():
|
18
|
+
return None
|
19
|
+
|
20
|
+
name = name.strip()
|
21
|
+
|
22
|
+
try:
|
23
|
+
user = await self.person_user_factory.from_name(name, self.http_client)
|
24
|
+
return user.id if user else None
|
25
|
+
except Exception:
|
26
|
+
return None
|
27
|
+
|
28
|
+
@override
|
29
|
+
async def resolve_id_to_name(self, user_id: str | None) -> str | None:
|
30
|
+
if not user_id or not user_id.strip():
|
31
|
+
return None
|
32
|
+
|
33
|
+
try:
|
34
|
+
user = await self.person_user_factory.from_id(user_id.strip(), self.http_client)
|
35
|
+
return user.name if user else None
|
36
|
+
except Exception:
|
37
|
+
return None
|
@@ -0,0 +1,132 @@
|
|
1
|
+
from typing import ClassVar
|
2
|
+
|
3
|
+
from notionary.blocks.rich_text.models import (
|
4
|
+
MentionDate,
|
5
|
+
MentionType,
|
6
|
+
RichText,
|
7
|
+
RichTextType,
|
8
|
+
)
|
9
|
+
from notionary.blocks.rich_text.name_id_resolver import (
|
10
|
+
DatabaseNameIdResolver,
|
11
|
+
NameIdResolver,
|
12
|
+
PageNameIdResolver,
|
13
|
+
PersonNameIdResolver,
|
14
|
+
)
|
15
|
+
from notionary.blocks.schemas import BlockColor
|
16
|
+
|
17
|
+
|
18
|
+
class RichTextToMarkdownConverter:
|
19
|
+
VALID_COLORS: ClassVar[set[str]] = {color.value for color in BlockColor}
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
*,
|
24
|
+
page_resolver: NameIdResolver | None = None,
|
25
|
+
database_resolver: NameIdResolver | None = None,
|
26
|
+
person_resolver: NameIdResolver | None = None,
|
27
|
+
) -> None:
|
28
|
+
self.page_resolver = page_resolver or PageNameIdResolver()
|
29
|
+
self.database_resolver = database_resolver or DatabaseNameIdResolver()
|
30
|
+
self.person_resolver = person_resolver or PersonNameIdResolver()
|
31
|
+
|
32
|
+
async def to_markdown(self, rich_text: list[RichText]) -> str:
|
33
|
+
if not rich_text:
|
34
|
+
return ""
|
35
|
+
|
36
|
+
parts: list[str] = []
|
37
|
+
|
38
|
+
for rich_obj in rich_text:
|
39
|
+
formatted_text = await self._convert_rich_text_to_markdown(rich_obj)
|
40
|
+
parts.append(formatted_text)
|
41
|
+
|
42
|
+
return "".join(parts)
|
43
|
+
|
44
|
+
async def _convert_rich_text_to_markdown(self, obj: RichText) -> str:
|
45
|
+
if obj.type == RichTextType.EQUATION and obj.equation:
|
46
|
+
return f"${obj.equation.expression}$"
|
47
|
+
|
48
|
+
if obj.type == RichTextType.MENTION:
|
49
|
+
mention_markdown = await self._extract_mention_markdown(obj)
|
50
|
+
if mention_markdown:
|
51
|
+
return mention_markdown
|
52
|
+
|
53
|
+
content = obj.plain_text or (obj.text.content if obj.text else "")
|
54
|
+
return self._apply_text_formatting_to_content(obj, content)
|
55
|
+
|
56
|
+
async def _extract_mention_markdown(self, obj: RichText) -> str | None:
|
57
|
+
if not obj.mention:
|
58
|
+
return None
|
59
|
+
|
60
|
+
mention = obj.mention
|
61
|
+
|
62
|
+
if mention.type == MentionType.PAGE and mention.page:
|
63
|
+
return await self._extract_page_mention_markdown(mention.page.id)
|
64
|
+
|
65
|
+
elif mention.type == MentionType.DATABASE and mention.database:
|
66
|
+
return await self._extract_database_mention_markdown(mention.database.id)
|
67
|
+
|
68
|
+
elif mention.type == MentionType.USER and mention.user:
|
69
|
+
return await self._extract_user_mention_markdown(mention.user.id)
|
70
|
+
|
71
|
+
elif mention.type == MentionType.DATE and mention.date:
|
72
|
+
return self._extract_date_mention_markdown(mention.date)
|
73
|
+
|
74
|
+
return None
|
75
|
+
|
76
|
+
async def _extract_page_mention_markdown(self, page_id: str) -> str:
|
77
|
+
page_name = await self.page_resolver.resolve_id_to_name(page_id)
|
78
|
+
return f"@page[{page_name or page_id}]"
|
79
|
+
|
80
|
+
async def _extract_database_mention_markdown(self, database_id: str) -> str:
|
81
|
+
database_name = await self.database_resolver.resolve_id_to_name(database_id)
|
82
|
+
return f"@database[{database_name or database_id}]"
|
83
|
+
|
84
|
+
async def _extract_user_mention_markdown(self, user_id: str) -> str:
|
85
|
+
user_name = await self.person_resolver.resolve_id_to_name(user_id)
|
86
|
+
return f"@user[{user_name or user_id}]"
|
87
|
+
|
88
|
+
def _extract_date_mention_markdown(self, date_mention: MentionDate) -> str:
|
89
|
+
date_range = date_mention.start
|
90
|
+
if date_mention.end:
|
91
|
+
date_range += f"–{date_mention.end}"
|
92
|
+
return f"@date[{date_range}]"
|
93
|
+
|
94
|
+
def _apply_text_formatting_to_content(self, obj: RichText, content: str) -> str:
|
95
|
+
if obj.text and obj.text.link:
|
96
|
+
content = f"[{content}]({obj.text.link.url})"
|
97
|
+
|
98
|
+
if not obj.annotations:
|
99
|
+
return content
|
100
|
+
|
101
|
+
annotations = obj.annotations
|
102
|
+
|
103
|
+
if annotations.code:
|
104
|
+
content = f"`{content}`"
|
105
|
+
if annotations.strikethrough:
|
106
|
+
content = f"~~{content}~~"
|
107
|
+
if annotations.underline:
|
108
|
+
content = f"__{content}__"
|
109
|
+
if annotations.italic:
|
110
|
+
content = f"*{content}*"
|
111
|
+
if annotations.bold:
|
112
|
+
content = f"**{content}**"
|
113
|
+
|
114
|
+
if annotations.color != BlockColor.DEFAULT and annotations.color in self.VALID_COLORS:
|
115
|
+
content = f"({annotations.color}:{content})"
|
116
|
+
|
117
|
+
return content
|
118
|
+
|
119
|
+
|
120
|
+
async def convert_rich_text_to_markdown(
|
121
|
+
rich_text: list[RichText],
|
122
|
+
*,
|
123
|
+
page_resolver: NameIdResolver | None = None,
|
124
|
+
database_resolver: NameIdResolver | None = None,
|
125
|
+
person_resolver: NameIdResolver | None = None,
|
126
|
+
) -> str:
|
127
|
+
converter = RichTextToMarkdownConverter(
|
128
|
+
page_resolver=page_resolver,
|
129
|
+
database_resolver=database_resolver,
|
130
|
+
person_resolver=person_resolver,
|
131
|
+
)
|
132
|
+
return await converter.to_markdown(rich_text)
|