From 207c41243872d879a87b9af22129f7f711a2e454 Mon Sep 17 00:00:00 2001 From: Benjamin Mayrargue Date: Sun, 26 Nov 2023 19:10:27 +0100 Subject: [PATCH] Better parsing and html generation --- .../GetBlockChildren.json | 356 ++++++++++ .../JsonData/AboutThis.Children.json | 646 ++++++++++++++++++ .../JsonData/AboutThis.Column.1.Children.json | 53 ++ .../JsonData/AboutThis.Column.2.Children.json | 82 +++ .../AboutThis.ColumnList.Children.json | 54 ++ .../JsonData/AboutThis.expected.html | 16 + .../JsonData/AboutThis.json | 49 ++ .../JsonData/GetBlockChildren.json | 29 + .../NotionSharp.ApiClient.Tests.csproj | 3 + NotionSharp.ApiClient.Tests/TestJson.cs | 60 ++ ...estNotionOfficial.cs => TestNotionBase.cs} | 19 +- .../TestNotionBlocks.cs | 41 ++ NotionSharp.ApiClient.Tests/TestNotionHtml.cs | 125 ++++ .../Lib/HtmlRendering/HtmlBlockExtensions.cs | 43 ++ .../Lib/HtmlRendering/HtmlRenderer.cs | 578 +++++++++------- .../Lib/HttpNotionSession.cs | 6 +- .../Lib/PublicApi/Model/Block.cs | 127 +++- .../Lib/PublicApi/Model/Common/Parent.cs | 5 - .../NotionSessionExtensions.cs | 37 +- .../NotionSharp.ApiClient.csproj | 8 +- 20 files changed, 2050 insertions(+), 287 deletions(-) create mode 100644 NotionSharp.ApiClient.Tests/GetBlockChildren.json create mode 100644 NotionSharp.ApiClient.Tests/JsonData/AboutThis.Children.json create mode 100644 NotionSharp.ApiClient.Tests/JsonData/AboutThis.Column.1.Children.json create mode 100644 NotionSharp.ApiClient.Tests/JsonData/AboutThis.Column.2.Children.json create mode 100644 NotionSharp.ApiClient.Tests/JsonData/AboutThis.ColumnList.Children.json create mode 100644 NotionSharp.ApiClient.Tests/JsonData/AboutThis.expected.html create mode 100644 NotionSharp.ApiClient.Tests/JsonData/AboutThis.json create mode 100644 NotionSharp.ApiClient.Tests/TestJson.cs rename NotionSharp.ApiClient.Tests/{TestNotionOfficial.cs => TestNotionBase.cs} (91%) create mode 100644 NotionSharp.ApiClient.Tests/TestNotionBlocks.cs create mode 100644 NotionSharp.ApiClient.Tests/TestNotionHtml.cs create mode 100644 NotionSharp.ApiClient/Lib/HtmlRendering/HtmlBlockExtensions.cs diff --git a/NotionSharp.ApiClient.Tests/GetBlockChildren.json b/NotionSharp.ApiClient.Tests/GetBlockChildren.json new file mode 100644 index 0000000..6bdd326 --- /dev/null +++ b/NotionSharp.ApiClient.Tests/GetBlockChildren.json @@ -0,0 +1,356 @@ +{ + "results": [ + { + "id": "05ff1fbd-0fce-4b14-9b39-60ed54d870e3", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "paragraph", + "created_time": "2020-05-25T12:31:00+00:00", + "last_edited_time": "2020-05-27T10:17:00+00:00", + "has_children": false, + "paragraph": { + "rich_text": [ + { + "type": "text", + "plain_text": "This article describes a tool: the map of procrastination.", + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "text": { + "content": "This article describes a tool: the map of procrastination." + } + } + ] + }, + "object": "block" + }, + { + "object": "block", + "id": "3c29dedf-00a5-4915-b137-120c61f5e5d8", + "parent": { + "type": "page_id", + "page_id": "13d6da82-2f93-43fa-8ec1-4c89b8184d5a" + }, + "created_time": "2022-12-15T00:18:00.000Z", + "last_edited_time": "2022-12-15T00:18:00.000Z", + "created_by": { + "object": "user", + "id": "c2f20311-9e54-4d11-8c79-7398424ae41e" + }, + "last_edited_by": { + "object": "user", + "id": "c2f20311-9e54-4d11-8c79-7398424ae41e" + }, + "has_children": false, + "archived": false, + "type": "file", + "file": { + "caption": [], + "type": "file", + "file": { + "url": "https://testFileUrl", + "expiry_time": "2022-12-15T01:20:12.928Z" + } + } + }, + { + "id": "d9276245-a7ca-404a-9bad-320d86c2875d", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "heading_1", + "created_time": "2020-05-25T12:30:00+00:00", + "last_edited_time": "2020-05-27T10:18:00+00:00", + "has_children": false, + "heading_1": { + "rich_text": [ + { + "type": "text", + "plain_text": "\u2753 What is procrastination", + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "text": { + "content": "\u2753 What is procrastination" + } + } + ] + }, + "object": "block" + }, + { + "id": "f017c6d5-5a47-4608-8b9d-d9a53a7e7087", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "paragraph", + "created_time": "2020-04-30T13:43:00+00:00", + "last_edited_time": "2020-05-27T10:18:00+00:00", + "has_children": false, + "paragraph": { + "rich_text": [ + { + "type": "text", + "plain_text": "Simply read the map \uD83E\uDD17", + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "text": { + "content": "Simply read the map \uD83E\uDD17" + } + } + ] + }, + "object": "block" + }, + { + "id": "e8ba007d-70f0-4ef8-bacc-51a7cf70d5ab", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "image", + "created_time": "2020-05-25T12:59:00+00:00", + "last_edited_time": "2022-02-23T12:31:00+00:00", + "has_children": false, + "image": {}, + "object": "block" + }, + { + "id": "94d9b729-92ee-4e0f-8c7a-9dcc68f1b23a", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "paragraph", + "created_time": "2020-05-25T12:59:00+00:00", + "last_edited_time": "2020-05-25T12:59:00+00:00", + "has_children": false, + "paragraph": { + "rich_text": [] + }, + "object": "block" + }, + { + "id": "ed1b7471-4c02-4837-bc20-624c7a20df9b", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "heading_1", + "created_time": "2020-04-29T07:46:00+00:00", + "last_edited_time": "2020-05-27T10:19:00+00:00", + "has_children": false, + "heading_1": { + "rich_text": [ + { + "type": "text", + "plain_text": "Next chapter", + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "text": { + "content": "Next chapter" + } + } + ] + }, + "object": "block" + }, + { + "id": "dabefad7-fdc2-4614-8229-2270d8d08bdf", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "paragraph", + "created_time": "2020-04-29T07:47:00+00:00", + "last_edited_time": "2020-04-29T07:47:00+00:00", + "has_children": false, + "paragraph": { + "rich_text": [ + { + "type": "text", + "plain_text": "I love this stuf!", + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "text": { + "content": "I love this stuf!" + } + } + ] + }, + "object": "block" + }, + { + "id": "e58fe5e9-d02b-4b53-9f8d-c50d31c46213", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "paragraph", + "created_time": "2020-04-29T07:47:00+00:00", + "last_edited_time": "2020-04-29T07:47:00+00:00", + "has_children": false, + "paragraph": { + "rich_text": [] + }, + "object": "block" + }, + { + "id": "b35b6266-1e80-41ca-9aa2-e30667d45d2e", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "heading_1", + "created_time": "2020-04-29T07:47:00+00:00", + "last_edited_time": "2020-05-27T10:19:00+00:00", + "has_children": false, + "heading_1": { + "rich_text": [ + { + "type": "text", + "plain_text": "Another chapter", + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "text": { + "content": "Another chapter" + } + } + ] + }, + "object": "block" + }, + { + "id": "1a878642-54fd-4b89-af6d-cafe933af53c", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "paragraph", + "created_time": "2020-04-29T07:47:00+00:00", + "last_edited_time": "2020-04-29T07:48:00+00:00", + "has_children": false, + "paragraph": { + "rich_text": [ + { + "type": "text", + "plain_text": "It\u0027s the best stuf of this world.", + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "text": { + "content": "It\u0027s the best stuf of this world." + } + } + ] + }, + "object": "block" + }, + { + "id": "2a436124-a82e-4590-b595-7a8e33f29006", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "paragraph", + "created_time": "2020-04-29T07:48:00+00:00", + "last_edited_time": "2020-04-29T07:48:00+00:00", + "has_children": false, + "paragraph": { + "rich_text": [ + { + "type": "text", + "plain_text": "I love writing so much !", + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "text": { + "content": "I love writing so much !" + } + } + ] + }, + "object": "block" + }, + { + "id": "9de0bf72-cd49-467a-bf7e-6966bb793cba", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "paragraph", + "created_time": "2020-04-29T07:48:00+00:00", + "last_edited_time": "2020-04-29T07:48:00+00:00", + "has_children": false, + "paragraph": { + "rich_text": [] + }, + "object": "block" + }, + { + "id": "24f5b571-ebc6-420f-8cf1-f723f9176385", + "parent": { + "type": "page_id", + "page_id": "4e4999b4-161a-449d-bbd1-bdbce690c7cb" + }, + "type": "paragraph", + "created_time": "2020-04-29T07:47:00+00:00", + "last_edited_time": "2020-04-29T07:47:00+00:00", + "has_children": false, + "paragraph": { + "rich_text": [] + }, + "object": "block" + } + ], + "has_more": false, + "object": "list" +} \ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/JsonData/AboutThis.Children.json b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.Children.json new file mode 100644 index 0000000..8a58b2c --- /dev/null +++ b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.Children.json @@ -0,0 +1,646 @@ +{ + "object": "list", + "results": [ + { + "object": "block", + "id": "a5a61607-488d-49ae-b2ee-60a8413deba1", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-04-30T13:38:00.000Z", + "last_edited_time": "2020-05-27T10:20:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "This content is written on a Notion page. It is a demo page for our csharp ", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "This content is written on a Notion page. It is a demo page for our csharp ", + "href": null + }, + { + "type": "text", + "text": { + "content": "notion.so", + "link": { + "url": "http://notion.so/" + } + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "notion.so", + "href": "http://notion.so/" + }, + { + "type": "text", + "text": { + "content": " api client.", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": " api client.", + "href": null + } + ], + "color": "default" + } + }, + { + "object": "block", + "id": "5b08e855-e221-41db-a442-bdc6384414e2", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-05-27T10:21:00.000Z", + "last_edited_time": "2020-05-27T10:21:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [], + "color": "default" + } + }, + { + "object": "block", + "id": "78598042-5f17-4ae3-8ef7-270e86aa7ae6", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-05-27T10:20:00.000Z", + "last_edited_time": "2020-05-27T10:21:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "heading_1", + "heading_1": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "Some API", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "Some API", + "href": null + } + ], + "is_toggleable": false, + "color": "default" + } + }, + { + "object": "block", + "id": "7919eb3f-cffe-46cb-95d0-8a4f27830a88", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-04-30T13:39:00.000Z", + "last_edited_time": "2020-05-27T10:20:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "It contains some lines to test the api client, especially these methods:", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "It contains some lines to test the api client, especially these methods:", + "href": null + } + ], + "color": "default" + } + }, + { + "object": "block", + "id": "85148c18-9dcc-4b27-9ca9-b74a15957c40", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-04-30T13:39:00.000Z", + "last_edited_time": "2020-04-30T13:39:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "bulleted_list_item", + "bulleted_list_item": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "GetHtml", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "GetHtml", + "href": null + } + ], + "color": "default" + } + }, + { + "object": "block", + "id": "31a6469a-8509-49e8-9033-7d34dfdd11c1", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-04-30T13:39:00.000Z", + "last_edited_time": "2020-04-30T13:39:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "bulleted_list_item", + "bulleted_list_item": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "LoadPageChunk", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "LoadPageChunk", + "href": null + } + ], + "color": "default" + } + }, + { + "object": "block", + "id": "72dd306a-1c1e-4c8a-afb0-424c7138cf60", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-04-30T13:40:00.000Z", + "last_edited_time": "2020-04-30T13:40:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "bulleted_list_item", + "bulleted_list_item": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "GetSyndicationFeed", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "GetSyndicationFeed", + "href": null + } + ], + "color": "default" + } + }, + { + "object": "block", + "id": "977c714e-b1a4-4911-8afd-6653ae54cd1b", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-05-27T10:20:00.000Z", + "last_edited_time": "2020-05-27T10:20:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [], + "color": "default" + } + }, + { + "object": "block", + "id": "1e1c8000-dcfa-4dd0-a955-8fe7fd5c6fdf", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-04-30T13:40:00.000Z", + "last_edited_time": "2020-05-27T10:20:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "♓ The last one is the most powerful: it creates a rss like feed from a ", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "♓ The last one is the most powerful: it creates a rss like feed from a ", + "href": null + }, + { + "type": "text", + "text": { + "content": "notion", + "link": { + "url": "http://notion.so/" + } + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "notion", + "href": "http://notion.so/" + }, + { + "type": "text", + "text": { + "content": " collection. The only difficulty is to obtain a login cookie that is long enough to live on a server.", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": " collection. The only difficulty is to obtain a login cookie that is long enough to live on a server.", + "href": null + } + ], + "color": "default" + } + }, + { + "object": "block", + "id": "2866f22f-16b3-4cb3-a381-f91f52c9b443", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-05-27T10:20:00.000Z", + "last_edited_time": "2023-11-26T16:03:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [], + "color": "default" + } + }, + { + "object": "block", + "id": "19766178-d501-4a42-890f-2af001d9f05c", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2020-05-27T10:20:00.000Z", + "last_edited_time": "2023-11-26T16:04:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "callout", + "callout": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "This is a CallOut !", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "This is a CallOut !", + "href": null + } + ], + "icon": { + "type": "emoji", + "emoji": "📢" + }, + "color": "gray_background" + } + }, + { + "object": "block", + "id": "9a68013d-d6ec-4a3f-96ed-2f392bad52b8", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2023-11-26T16:03:00.000Z", + "last_edited_time": "2023-11-26T16:04:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [], + "color": "default" + } + }, + { + "object": "block", + "id": "66930c42-ed4c-48dd-be36-2e8e1b9a4459", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2023-11-26T16:05:00.000Z", + "last_edited_time": "2023-11-26T16:05:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": true, + "archived": false, + "type": "column_list", + "column_list": {} + }, + { + "object": "block", + "id": "d3ca09fc-48da-41ef-afb8-a1307a1e9cfa", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2023-11-26T16:06:00.000Z", + "last_edited_time": "2023-11-26T16:07:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [], + "color": "default" + } + }, + { + "object": "block", + "id": "a8fd2706-3222-467e-a9c5-3cb47df50b43", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2023-11-26T16:06:00.000Z", + "last_edited_time": "2023-11-26T16:07:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "quote", + "quote": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "The End Quote", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "plain_text": "The End Quote", + "href": null + } + ], + "color": "default" + } + }, + { + "object": "block", + "id": "502ec809-7326-4a71-a546-89a6d1e92cf8", + "parent": { + "type": "page_id", + "page_id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585" + }, + "created_time": "2023-11-26T16:07:00.000Z", + "last_edited_time": "2023-11-26T16:07:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [], + "color": "default" + } + } + ], + "next_cursor": null, + "has_more": false, + "type": "block", + "block": {}, + "request_id": "75224f1c-5e52-418f-9898-d8497f5b0479" +} \ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/JsonData/AboutThis.Column.1.Children.json b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.Column.1.Children.json new file mode 100644 index 0000000..2254fc7 --- /dev/null +++ b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.Column.1.Children.json @@ -0,0 +1,53 @@ +{ + "object": "list", + "results": [ + { + "object": "block", + "id": "adfef9bd-a913-473e-9de2-e5dd2812fe09", + "parent": { + "type": "block_id", + "block_id": "53cbb49f-eb91-4407-9072-3c158ed1ef18" + }, + "created_time": "2023-11-26T16:04:00.000Z", + "last_edited_time": "2023-11-26T16:06:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "First Column (25%)\n", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "orange" + }, + "plain_text": "First Column (25%)\n", + "href": null + } + ], + "color": "default" + } + } + ], + "next_cursor": null, + "has_more": false, + "type": "block", + "block": {}, + "request_id": "381034c1-3d81-4ef5-8b4d-7d683a8a67a2" +} \ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/JsonData/AboutThis.Column.2.Children.json b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.Column.2.Children.json new file mode 100644 index 0000000..1e40b8b --- /dev/null +++ b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.Column.2.Children.json @@ -0,0 +1,82 @@ +{ + "object": "list", + "results": [ + { + "object": "block", + "id": "314477d6-8955-49c8-8ffa-f6302537369e", + "parent": { + "type": "block_id", + "block_id": "dd0e16ae-080c-4ed0-96be-666159f17343" + }, + "created_time": "2023-11-26T16:04:00.000Z", + "last_edited_time": "2023-11-26T16:07:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "paragraph", + "paragraph": { + "rich_text": [ + { + "type": "text", + "text": { + "content": "Second column (75%)", + "link": null + }, + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "green" + }, + "plain_text": "Second column (75%)", + "href": null + } + ], + "color": "default" + } + }, + { + "object": "block", + "id": "175e7376-ba5c-4b56-ad9c-c5390c817e09", + "parent": { + "type": "block_id", + "block_id": "dd0e16ae-080c-4ed0-96be-666159f17343" + }, + "created_time": "2023-11-26T16:05:00.000Z", + "last_edited_time": "2023-11-26T16:05:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": false, + "archived": false, + "type": "image", + "image": { + "caption": [], + "type": "file", + "file": { + "url": "https://prod-files-secure.s3.us-west-2.amazonaws.com/da89a8e0-9c55-4c13-9966-796e0f0c1bac/3b632032-656f-4e87-87b7-b95bcb786f9d/Untitled.png?X-Amz-Algorithm\u003dAWS4-HMAC-SHA256\u0026X-Amz-Content-Sha256\u003dUNSIGNED-PAYLOAD\u0026X-Amz-Credential\u003dAKIAT73L2G45HZZMZUHI%2F20231126%2Fus-west-2%2Fs3%2Faws4_request\u0026X-Amz-Date\u003d20231126T174313Z\u0026X-Amz-Expires\u003d3600\u0026X-Amz-Signature\u003d6e2d343e80ed9cc1abcae604239b385bf7695c5f1fa431878035c3fc00403681\u0026X-Amz-SignedHeaders\u003dhost\u0026x-id\u003dGetObject", + "expiry_time": "2023-11-26T18:43:13.706Z" + } + } + } + ], + "next_cursor": null, + "has_more": false, + "type": "block", + "block": {}, + "request_id": "99e5b57d-9b3f-456b-954c-636682eea3b6" +} \ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/JsonData/AboutThis.ColumnList.Children.json b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.ColumnList.Children.json new file mode 100644 index 0000000..114ecb1 --- /dev/null +++ b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.ColumnList.Children.json @@ -0,0 +1,54 @@ +{ + "object": "list", + "results": [ + { + "object": "block", + "id": "53cbb49f-eb91-4407-9072-3c158ed1ef18", + "parent": { + "type": "block_id", + "block_id": "66930c42-ed4c-48dd-be36-2e8e1b9a4459" + }, + "created_time": "2023-11-26T16:05:00.000Z", + "last_edited_time": "2023-11-26T16:06:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": true, + "archived": false, + "type": "column", + "column": {} + }, + { + "object": "block", + "id": "dd0e16ae-080c-4ed0-96be-666159f17343", + "parent": { + "type": "block_id", + "block_id": "66930c42-ed4c-48dd-be36-2e8e1b9a4459" + }, + "created_time": "2023-11-26T16:05:00.000Z", + "last_edited_time": "2023-11-26T16:06:00.000Z", + "created_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "last_edited_by": { + "object": "user", + "id": "ab9257e1-d027-4494-8792-71d90b63dd35" + }, + "has_children": true, + "archived": false, + "type": "column", + "column": {} + } + ], + "next_cursor": null, + "has_more": false, + "type": "block", + "block": {}, + "request_id": "d49e5f30-f3b5-4d99-bbb1-f4060052d63c" +} \ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/JsonData/AboutThis.expected.html b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.expected.html new file mode 100644 index 0000000..31f192d --- /dev/null +++ b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.expected.html @@ -0,0 +1,16 @@ +
This content is written on a Notion page. It is a demo page for our csharp
api client.
+
+

Some API

+
It contains some lines to test the api client, especially these methods:
+ + + +
+
♓ The last one is the most powerful: it creates a rss like feed from a
collection. The only difficulty is to obtain a login cookie that is long enough to live on a server.
+
+
+
This is a CallOut !
+
+
+
The End Quote
+
\ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/JsonData/AboutThis.json b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.json new file mode 100644 index 0000000..8791222 --- /dev/null +++ b/NotionSharp.ApiClient.Tests/JsonData/AboutThis.json @@ -0,0 +1,49 @@ +{ + "results": [ + { + "id": "c7b44455-3d31-4a5b-b82c-b7e3d85ba585", + "created_time": "2020-04-30T13:37:00+00:00", + "last_edited_time": "2023-11-26T16:07:00+00:00", + "archived": false, + "created_by": { + "id": "ab9257e1-d027-4494-8792-71d90b63dd35", + "object": "user" + }, + "last_edited_by": { + "id": "ab9257e1-d027-4494-8792-71d90b63dd35", + "object": "user" + }, + "url": "https://www.notion.so/About-this-c7b444553d314a5bb82cb7e3d85ba585", + "public_url": "https://wise-spirit-737.notion.site/About-this-c7b444553d314a5bb82cb7e3d85ba585", + "parent": { + "type": "page_id" + }, + "properties": { + "title": { + "title": [ + { + "type": "text", + "plain_text": "About this", + "annotations": { + "bold": false, + "italic": false, + "strikethrough": false, + "underline": false, + "code": false, + "color": "default" + }, + "text": { + "content": "About this" + } + } + ], + "id": "title", + "type": "title" + } + }, + "object": "page" + } + ], + "has_more": false, + "object": "list" +} \ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/JsonData/GetBlockChildren.json b/NotionSharp.ApiClient.Tests/JsonData/GetBlockChildren.json index 7498c42..6bdd326 100644 --- a/NotionSharp.ApiClient.Tests/JsonData/GetBlockChildren.json +++ b/NotionSharp.ApiClient.Tests/JsonData/GetBlockChildren.json @@ -31,6 +31,35 @@ }, "object": "block" }, + { + "object": "block", + "id": "3c29dedf-00a5-4915-b137-120c61f5e5d8", + "parent": { + "type": "page_id", + "page_id": "13d6da82-2f93-43fa-8ec1-4c89b8184d5a" + }, + "created_time": "2022-12-15T00:18:00.000Z", + "last_edited_time": "2022-12-15T00:18:00.000Z", + "created_by": { + "object": "user", + "id": "c2f20311-9e54-4d11-8c79-7398424ae41e" + }, + "last_edited_by": { + "object": "user", + "id": "c2f20311-9e54-4d11-8c79-7398424ae41e" + }, + "has_children": false, + "archived": false, + "type": "file", + "file": { + "caption": [], + "type": "file", + "file": { + "url": "https://testFileUrl", + "expiry_time": "2022-12-15T01:20:12.928Z" + } + } + }, { "id": "d9276245-a7ca-404a-9bad-320d86c2875d", "parent": { diff --git a/NotionSharp.ApiClient.Tests/NotionSharp.ApiClient.Tests.csproj b/NotionSharp.ApiClient.Tests/NotionSharp.ApiClient.Tests.csproj index b157e24..8a7fd52 100644 --- a/NotionSharp.ApiClient.Tests/NotionSharp.ApiClient.Tests.csproj +++ b/NotionSharp.ApiClient.Tests/NotionSharp.ApiClient.Tests.csproj @@ -26,6 +26,9 @@ PreserveNewest + + PreserveNewest + diff --git a/NotionSharp.ApiClient.Tests/TestJson.cs b/NotionSharp.ApiClient.Tests/TestJson.cs new file mode 100644 index 0000000..99302de --- /dev/null +++ b/NotionSharp.ApiClient.Tests/TestJson.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NotionSharp.ApiClient.Lib.HtmlRendering; + +namespace NotionSharpTest; + +public static class JsonTextSerializerOptions +{ + public static JsonSerializerOptions SpecialOptions { get; } = new (JsonSerializerDefaults.General) + { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower + }; +} + +public class TestDefault +{ + [JsonIgnore] + public JsonElement TheValue => Values["value"]; + + [JsonExtensionData] + public Dictionary Values { get; set; } +} + +[TestClass] +public class TestJson +{ + /// + /// NOTE: NOW FIXED. STILL HAVE TO UPDATE CODE TO USE THAT NEW FEATURE. + /// https://github.com/dotnet/runtime/issues/68895 + /// System.Text.Json does not fill JsonExtensionData when a property with the same name already exists or has the JsonIgnoreAttribute + /// For .net8 ... + /// + [TestMethod] + public void TestJsonExtensionData_Inherited() + { + var json = @"{ ""value"": { ""content"": ""test"" } }"; + var r = JsonSerializer.Deserialize(json, JsonTextSerializerOptions.SpecialOptions); + Assert.IsNotNull(r); + Assert.IsNotNull(r.Values); + Assert.AreEqual(1, r.Values.Count); + Assert.IsTrue(r.Values.ContainsKey("value")); + Assert.IsNotNull(r.TheValue); + } + + [TestMethod] + public void TestEmoji() + { + var emojiString = "🇫🇷"; + var url = emojiString.GetTwitterEmojiUrl().ToString(); + Assert.AreEqual("https://cdn.jsdelivr.net/gh/twitter/twemoji/assets/svg/1f1eb-1f1f7.svg", url); + + emojiString = "♻️"; + url = emojiString.GetTwitterEmojiUrl().ToString(); + Assert.AreEqual("https://cdn.jsdelivr.net/gh/twitter/twemoji/assets/svg/267b.svg", url); + } +} \ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/TestNotionOfficial.cs b/NotionSharp.ApiClient.Tests/TestNotionBase.cs similarity index 91% rename from NotionSharp.ApiClient.Tests/TestNotionOfficial.cs rename to NotionSharp.ApiClient.Tests/TestNotionBase.cs index 8b9ef48..2cfbb18 100644 --- a/NotionSharp.ApiClient.Tests/TestNotionOfficial.cs +++ b/NotionSharp.ApiClient.Tests/TestNotionBase.cs @@ -11,7 +11,7 @@ namespace NotionSharp.ApiClient.Tests; [TestClass] -public class TestNotionOfficial +public class TestNotionBase { /// /// Automatic paging @@ -145,7 +145,6 @@ public async Task TestJsonCase() Assert.AreEqual( """{"query":"theQuery","sort":{"direction":1,"timestamp":0},"filter":{"property":"page"},"page_size":50}""", jsonString); } - //GetBlockChildren.json [TestMethod] public async Task TestGetBlockChildrenJson() { @@ -153,6 +152,9 @@ public async Task TestGetBlockChildrenJson() var json = JsonSerializer.Deserialize>(getBlockChildren, HttpNotionSession.NotionJsonSerializationOptions); Assert.IsNotNull(json); + + var blockFile = json.Results.First(b => b.Type == "file"); + Assert.AreEqual("https://testFileUrl", blockFile.File.File.Url); } [TestMethod] @@ -236,4 +238,17 @@ public async Task TestGetPublicBlogContent() Assert.IsNotNull((pages[0].Properties["title"] as TitlePropertyItem).Title[0].PlainText); } + + [TestMethod] + public async Task TestPageAndChildrenDeserialization() + { + var pageJson = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "JsonData", "AboutThis.json")); + var page = JsonSerializer.Deserialize>(pageJson, HttpNotionSession.NotionJsonSerializationOptions); + Assert.IsNotNull(page); + + var blocksJson = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "JsonData", "AboutThis.Children.json")); + var blocks = JsonSerializer.Deserialize>(blocksJson, HttpNotionSession.NotionJsonSerializationOptions); + Assert.IsNotNull(blocks); + } + } \ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/TestNotionBlocks.cs b/NotionSharp.ApiClient.Tests/TestNotionBlocks.cs new file mode 100644 index 0000000..a643ae8 --- /dev/null +++ b/NotionSharp.ApiClient.Tests/TestNotionBlocks.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NotionSharp.ApiClient.Lib; +using NotionSharp.ApiClient.Tests.Lib; + +namespace NotionSharp.ApiClient.Tests; + +[TestClass] +public class TestNotionBlocks +{ + + [TestMethod] + public async Task TestBlockColumnList() + { + // var blocksJson = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "JsonData", "AboutThis.Children.json")); + // var blocks = JsonSerializer.Deserialize>(blocksJson, HttpNotionSession.NotionJsonSerializationOptions); + // Assert.IsNotNull(blocks); + // var columnList = blocks.Results.First(b => b.Type == BlockTypes.ColumnList); + + var columnJson = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "JsonData", "AboutThis.ColumnList.Children.json")); + var columns = JsonSerializer.Deserialize>(columnJson, HttpNotionSession.NotionJsonSerializationOptions); + Assert.IsNotNull(columns?.Results); + Assert.AreEqual(2, columns!.Results.Count); + + // var session = new NotionSession(TestUtils.CreateOfficialNotionSessionInfo()); + // await session.GetChildren(columns.Results[0]); + // await session.GetChildren(columns.Results[1]); + + var column1Json = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "JsonData", "AboutThis.Column.1.Children.json")); + var column1 = JsonSerializer.Deserialize>(column1Json, HttpNotionSession.NotionJsonSerializationOptions); + Assert.IsNotNull(column1.Results); + var column2Json = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "JsonData", "AboutThis.Column.2.Children.json")); + var column2 = JsonSerializer.Deserialize>(column2Json, HttpNotionSession.NotionJsonSerializationOptions); + Assert.IsNotNull(column2.Results); + } +} \ No newline at end of file diff --git a/NotionSharp.ApiClient.Tests/TestNotionHtml.cs b/NotionSharp.ApiClient.Tests/TestNotionHtml.cs new file mode 100644 index 0000000..d9d74e5 --- /dev/null +++ b/NotionSharp.ApiClient.Tests/TestNotionHtml.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NotionSharp.ApiClient; +using NotionSharp.ApiClient.Tests.Lib; + +namespace NotionSharpTest; + +[TestClass] +public class TestNotionHtml +{ + [TestMethod] + public async Task TestGetPage() + { + var session = new NotionSession(TestUtils.CreateOfficialNotionSessionInfo()); + //Get any page returned by search + var page = await session.Search(query: "About this", filterOptions: FilterOptions.ObjectPage).FirstAsync(); + Assert.IsNotNull(page); + Assert.IsNotNull(page.Id); + Assert.IsNotNull(page.Properties); + Assert.IsNotNull(page.Parent); + Assert.AreEqual("About this", page.Title().Title[0].PlainText); + + var blocks = await session.GetBlockChildren(page.Id) + .Where(childBlock => BlockTypes.SupportedBlocks.Contains(childBlock.Type)) + .ToListAsync().ConfigureAwait(false); + + var blockWithChildren = new Queue(blocks.Where(b => b.HasChildren && BlockTypes.BlocksWithChildren.Contains(b.Type))); + while (blockWithChildren.Count != 0) + { + var block = blockWithChildren.Dequeue(); + await session.GetChildren(block); + //recursive + var children = block.Children.Where(b => b.HasChildren && BlockTypes.BlocksWithChildren.Contains(b.Type)); + foreach (var child in children) + blockWithChildren.Enqueue(child); + } + + var expectedHtml = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "JsonData", "AboutThis.expected.html")); + var html = await session.GetHtml(page); + Assert.AreEqual(expectedHtml, html); + } + +// [TestMethod] +// public void TestGetHtml() +// { +// var json = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, "JsonData", "LoadPageChunkResult1.json")); +// var chunks = JsonSerializer.Deserialize(json, JsonTextSerializerOptions.Options); +// Assert.IsNotNull(chunks); +// Assert.IsNotNull(chunks.RecordMap); +// Assert.IsNotNull(chunks.RecordMap.Block.First().Value.TheValue); +// +// var content = chunks.RecordMap.GetHtml(throwIfBlockMissing: false); +// Assert.IsNotNull(content); +// Assert.IsTrue(content.Length > 5000); +// } +// +// [TestMethod] +// public void TestGetHtmlAbstract() +// { +// var json = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, @"..\..\..\JsonData", "LoadPageChunkResult1.json")); +// var chunks = JsonSerializer.Deserialize(json, JsonTextSerializerOptions.Options); +// Assert.IsNotNull(chunks); +// var content = chunks.RecordMap.GetHtmlAbstract(); +// Assert.IsNotNull(content); +// Assert.AreEqual(@"
Creating a good Xamarin Forms control - Part 3 - UI Day 4
+//
In the previous article I proposed the foundations of a win-win architecture for a good Xamarin Forms control using a multi targeting project.
+//
Today I am presenting a way to create a control with a renderer that auto register itself, greatly simplifying the control's usage in teams, but also its documentation and its maintenance.
+//
+// ", content); +// } +// +// [TestMethod] +// public void TestGetHtml_BlogTestPage1() +// { +// var json = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, @"..\..\..\JsonData", "BlogTestPage1.json")); +// var chunks = JsonSerializer.Deserialize(json, JsonTextSerializerOptions.Options); +// Assert.IsNotNull(chunks); +// +// var content = chunks.RecordMap.GetHtml(throwIfBlockMissing: false); +// Assert.IsNotNull(content); +// Assert.IsTrue(content.StartsWith(@"
Creating a good Xamarin Forms control - Part 3 - UI Day 4
")); +// } +// +// [TestMethod] +// public void TestGetHtml_BlogTestPage1new() +// { +// var json = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, @"..\..\..\JsonData", "BlogTestPage1new.json")); +// var chunks = JsonSerializer.Deserialize(json, JsonTextSerializerOptions.Options); +// Assert.IsNotNull(chunks); +// +// var content = chunks.RecordMap.GetHtml(throwIfBlockMissing: false); +// Assert.IsNotNull(content); +// Assert.IsTrue(content.Contains(@"class=""notion-text-block notion-image-caption"">A V")); +// } +// +// [TestMethod] +// public void TestGetHtml_SubBullets() +// { +// var json = File.ReadAllText(Path.Combine(Environment.CurrentDirectory, @"..\..\..\JsonData", "SubBullets.json")); +// var chunks = JsonSerializer.Deserialize(json, JsonTextSerializerOptions.Options); +// Assert.IsNotNull(chunks); +// +// var content = chunks.RecordMap.GetHtml(throwIfBlockMissing: false); +// Assert.IsNotNull(content); +// Assert.IsTrue(content.StartsWith(@"

⚡Welcome ⚡

+//
Here at Vapolia we are fond of coding. With 25 years of experience within small and large companies, we are particulary good at understanding your needs !
+//
+//
🤟 Our main services:
+//
+//
  • Gather your ideas into an understandable specification
  • +//
    🌞 we convert your vision into a specification understandable by everyone - from the product owner to the developer's team.
    +//
    +//
+//
  • You need a mobile app
  • +//
    📱 let us create it for you, we use the lastest cross platform technologies. You want it connected to the cloud? No problem!
    +//
    +//
+// ")); +// } + +} \ No newline at end of file diff --git a/NotionSharp.ApiClient/Lib/HtmlRendering/HtmlBlockExtensions.cs b/NotionSharp.ApiClient/Lib/HtmlRendering/HtmlBlockExtensions.cs new file mode 100644 index 0000000..5193a94 --- /dev/null +++ b/NotionSharp.ApiClient/Lib/HtmlRendering/HtmlBlockExtensions.cs @@ -0,0 +1,43 @@ +using System.ComponentModel; +using System.Text; + +namespace NotionSharp.ApiClient.Lib.HtmlRendering; + +public static class HtmlBlockExtensions +{ + public static string? GetColor(this NotionColor color) + { + //TODO: replace tolower by ToSnakeCase + return ((string?)TypeDescriptor.GetConverter(color).ConvertTo(color, typeof(string)))?.ToLower(); + } + + /// + /// TODO: switch to https://github.com/twitter/twemoji + /// + /// an emoji string + /// A url of a svg file + public static StringBuilder? GetTwitterEmojiUrl(this string emojiString) + { + var enc = new UTF32Encoding(true, false); + var bytes = enc.GetBytes(emojiString); + + var sbCodepointEmoji = new StringBuilder(); + for (var i = 0; i < bytes.Length; i += 4) + { + var value = bytes[i] << 24 | bytes[i + 1] << 16 | bytes[i + 2] << 8 | bytes[i + 3]; + if (value is 0xFE0E or 0xFE0F) + continue; + sbCodepointEmoji.Append($"{value:x}-"); + } + + if (sbCodepointEmoji.Length > 0 && sbCodepointEmoji[^1] == '-') + sbCodepointEmoji.Remove(sbCodepointEmoji.Length - 1, 1); + + if (sbCodepointEmoji.Length == 0) + return null; + + sbCodepointEmoji.Insert(0, "https://cdn.jsdelivr.net/gh/twitter/twemoji/assets/svg/"); + sbCodepointEmoji.Append(".svg"); + return sbCodepointEmoji; + } +} \ No newline at end of file diff --git a/NotionSharp.ApiClient/Lib/HtmlRendering/HtmlRenderer.cs b/NotionSharp.ApiClient/Lib/HtmlRendering/HtmlRenderer.cs index f0316ee..a9fb7b6 100644 --- a/NotionSharp.ApiClient/Lib/HtmlRendering/HtmlRenderer.cs +++ b/NotionSharp.ApiClient/Lib/HtmlRendering/HtmlRenderer.cs @@ -3,269 +3,368 @@ using System.Linq; using System.Text; -namespace NotionSharp.ApiClient.Lib.HtmlRendering +namespace NotionSharp.ApiClient.Lib.HtmlRendering; + +public class HtmlRenderer { - public class HtmlRenderer + /// + /// Get an HTML extract of the page + /// + /// the page's child blocks + /// true to return only all html before the first sub-header + /// An HTML string + public virtual string GetHtml(List blocks, bool stopBeforeFirstSubHeader = false) { - /// - /// Get an HTML extract of the page - /// - /// the page's child blocks - /// true to return only all html before the first sub-header - /// An HTML string - public virtual string GetHtml(IEnumerable blocks, bool stopBeforeFirstSubHeader = false) - { - var sb = new StringBuilder(); + var sb = new StringBuilder(); - foreach (var block in blocks) - if(!Transform(block, sb, stopBeforeFirstSubHeader)) - break; - - return sb.ToString(); + foreach (var block in blocks) + { + if (!Transform(block, sb, stopBeforeFirstSubHeader)) + break; } - protected virtual bool Transform(Block? block, StringBuilder sb, bool stopBeforeFirstSubHeader) - { - if (block == null) - return true; + return sb.ToString(); + } + + protected virtual bool Transform(Block? block, StringBuilder sb, bool stopBeforeFirstSubHeader) + { + if (block == null) + return true; - switch (block.Type) - { - case BlockTypes.Heading1: - TransformHeading1(block, sb); - break; - case BlockTypes.Heading2: - if (stopBeforeFirstSubHeader) - return false; - TransformHeading2(block, sb); - break; - case BlockTypes.Heading3: - TransformHeading3(block, sb); - break; - case BlockTypes.Paragraph: - TransformParagraph(block, sb); - break; - case BlockTypes.ChildPage: - //block.ChildPage - //TODO - break; - case BlockTypes.ToDo: - //TODO input type=radio - sb.Append("
  • "); - Append(block.ToDo, sb); - sb.AppendLine("
"); - break; - case BlockTypes.Toggle: - //TODO input type=checkbox - sb.Append("
  • "); - Append(block.Toggle, sb); - sb.AppendLine("
"); - break; - case BlockTypes.BulletedListItem: - TransformBulletedListItem(block, sb); - break; - case BlockTypes.NumberedListItem: - TransformNumberedListItem(block, sb); - break; - case BlockTypes.Image: - TransformImage(block.Image, block.Id, sb); - break; + switch (block.Type) + { + case BlockTypes.Heading1: + TransformHeading1(block, sb); + break; + case BlockTypes.Heading2: + if (stopBeforeFirstSubHeader) + return false; + TransformHeading2(block, sb); + break; + case BlockTypes.Heading3: + TransformHeading3(block, sb); + break; + case BlockTypes.Paragraph: + TransformParagraph(block, sb); + break; + case BlockTypes.ChildPage: + //block.ChildPage + //TODO + break; + case BlockTypes.ToDo: + //TODO input type=radio + sb.Append("
  • "); + Append(block.ToDo, sb); + sb.AppendLine("
"); + break; + case BlockTypes.Toggle: + //TODO input type=checkbox + sb.Append("
  • "); + Append(block.Toggle, sb); + sb.AppendLine("
"); + break; + case BlockTypes.BulletedListItem: + TransformBulletedListItem(block, sb); + break; + case BlockTypes.NumberedListItem: + TransformNumberedListItem(block, sb); + break; + case BlockTypes.Image: + TransformImage(block.Image, sb); + break; + // case BlockTypes.File: + // TransformFile(block.File, sb); + // break; + case BlockTypes.Quote: + TransformQuote(block, sb); + break; + case BlockTypes.Callout: + TransformCallout(block, sb); + break; + case BlockTypes.ColumnList: + TransformColumnList(block, sb); + break; - case BlockTypes.Unsupported: - break; + case BlockTypes.Unsupported: + break; #if DEBUG - default: - throw new ArgumentException($"Unknown block type {block.Type}"); + default: + throw new ArgumentException($"Unknown block type {block.Type}"); #endif - } - - return true; } - protected virtual void TransformImage(BlockImage data, string blockId, StringBuilder sb) - { - if (data.External != null) - { - sb.Append("
"); - var imageUrl = $"{data.External?.Url}?table=block&id={blockId}&cache=v2"; - sb.Append(""); - sb.AppendLine("
"); - } - } - - protected virtual void TransformBulletedListItem(Block block, StringBuilder sb) - { - sb.Append("
  • "); - Append(block.BulletedListItem, sb); - sb.AppendLine("
"); - } + return true; + } - protected virtual void TransformNumberedListItem(Block block, StringBuilder sb) + protected virtual void TransformImage(NotionFile? data, StringBuilder sb) + { + var url = data?.External?.Url ?? data?.File?.Url; + if (url != null) { - sb.Append("
  1. "); - Append(block.NumberedListItem, sb); - sb.AppendLine("
"); + sb.Append("
") + .Append("") + .AppendLine("
"); } + } - protected virtual void TransformHeading1(Block block, StringBuilder sb) - { - sb.Append("

"); - Append(block.Heading1?.RichText, sb).AppendLine("

"); - } - protected virtual void TransformHeading2(Block block, StringBuilder sb) - { - sb.Append("

"); - Append(block.Heading1?.RichText, sb).AppendLine("

"); - } - protected virtual void TransformHeading3(Block block, StringBuilder sb) - { - sb.Append("

"); - Append(block.Heading1?.RichText, sb).AppendLine("

"); - } - protected virtual void TransformParagraph(Block block, StringBuilder sb) + protected virtual void TransformQuote(Block block, StringBuilder sb) + { + var quote = block.Quote!; + var color = quote.Color.GetColor(); + + sb.Append("
"); + sb.Append("\">"); + + Append(quote, sb); + sb.AppendLine("
"); + } + + protected virtual void TransformCallout(Block block, StringBuilder sb) + { + var callout = block.Callout!; + var color = callout.Color.GetColor(); + + sb.Append("""
"); + + if (callout.Icon is NotionEmoji emoji) + sb.AppendLine($""""""); + else if(callout.Icon is NotionFile file + && Uri.TryCreate(file.External?.Url ?? file.File?.Url, UriKind.Absolute, out var iconUrl)) + sb.AppendLine($""""""); + + Append(callout.RichText, sb); + sb.AppendLine("
"); + } + + /// + /// Manage a column list + /// + /// + /// Children are exclusively columns. + /// Each column also has children that represent the column's content. + /// + protected virtual void TransformColumnList(Block block, StringBuilder sb) + { + if(!block.HasChildren) + return; + + var result = TransformColumnListInner(block, sb); + + var columnIndex = 0; + var totalColumns = block.Children.Count; + foreach (var columnBlock in block.Children) { - sb.Append("
"); - Append(block.Paragraph, sb); - sb.AppendLine("
"); + var endColumn = result.StartColumn(columnIndex); + foreach (var contentBlock in columnBlock.Children) + Transform(contentBlock, sb, false); + endColumn(); + + if (columnIndex < totalColumns - 1) + result.TransformColumnSeparator(columnIndex); + + columnIndex++; } + + result.EndColumnList(); + } + + + /// + /// Render a column + /// + protected virtual (Func StartColumn, + Action TransformColumnSeparator, + Action EndColumnList) + TransformColumnListInner(Block columnBlock, StringBuilder sb) + { + //Start column list + sb.Append("
"); + var totalColumns = columnBlock.Children.Count; + + //Return actions to render each column + return ( + //Render a column + (columnIndex) => + { + //Start this column + const int ratio = 1; //column.Ratio is not available in public api + sb.Append(FormattableString.Invariant($"""
""")); + //Return an action that renders the end of this column + return () => sb.Append("
"); + }, + //Render the column list separator + columnIndex => sb.Append("""
"""), + //Render the column list close tag + () => sb.Append("
") + ); + } + + protected virtual void TransformBulletedListItem(Block block, StringBuilder sb) + { + sb.Append("
  • "); + Append(block.BulletedListItem, sb); + sb.AppendLine("
"); + } + + protected virtual void TransformNumberedListItem(Block block, StringBuilder sb) + { + sb.Append("
  1. "); + Append(block.NumberedListItem, sb); + sb.AppendLine("
"); + } + + protected virtual void TransformHeading1(Block block, StringBuilder sb) + { + sb.Append("

"); + Append(block.Heading1?.RichText, sb).AppendLine("

"); + } + protected virtual void TransformHeading2(Block block, StringBuilder sb) + { + sb.Append("

"); + Append(block.Heading1?.RichText, sb).AppendLine("

"); + } + protected virtual void TransformHeading3(Block block, StringBuilder sb) + { + sb.Append("

"); + Append(block.Heading1?.RichText, sb).AppendLine("

"); + } + protected virtual void TransformParagraph(Block block, StringBuilder sb) + { + sb.Append("
"); + Append(block.Paragraph, sb); + sb.AppendLine("
"); + } - protected virtual void Append(BlockTextAndChildren? block, StringBuilder sb) + protected virtual void Append(BlockTextAndChildren? block, StringBuilder sb) + { + Append(block?.RichText, sb); + if (block?.Children != null) { - Append(block?.RichText, sb); - if (block?.Children != null) - { - foreach (var child in block.Children) - Transform(child, sb, false); - } + foreach (var child in block.Children) + Transform(child, sb, false); } + } - protected virtual StringBuilder Append(List? data, StringBuilder sb) - { - if (data == null) - return sb; + protected virtual StringBuilder Append(List? data, StringBuilder sb) + { + if (data == null) + return sb; - foreach (var line in data.Where(l => l != null)) - Append(line, sb); + foreach (var line in data.Where(l => l != null)) + Append(line, sb); + return sb; + } + + protected virtual StringBuilder Append(RichText? line, StringBuilder sb) + { + if (line == null) return sb; - } - protected virtual StringBuilder Append(RichText? line, StringBuilder sb) + sb.Append("
"); + var tag = line.HasAttribute ? (line.Href != null ? "a" : "span") : null; + + if (tag != null) { - if (line == null) - return sb; + sb.Append("<").Append(tag); - sb.Append("
"); - var tag = line.HasAttribute ? (line.Href != null ? "a" : "span") : null; + if (line.Href != null) + sb.Append(" href=\"").Append(Uri.EscapeUriString(line.Href)).Append("\""); - if (tag != null) + if (line.HasStyle) { - sb.Append("<").Append(tag); - - if (line.Href != null) - sb.Append(" href=\"").Append(Uri.EscapeUriString(line.Href)).Append("\""); - - if (line.HasStyle) - { - sb.Append(" class=\""); - if (line.Annotation.Bold) - sb.Append(" notion-bold"); - if (line.Annotation.Italic) - sb.Append(" notion-italic"); - if (line.Annotation.Strikethrough) - sb.Append(" notion-strikethrough"); - if (line.Annotation.Underline) - sb.Append(" notion-underline"); - if (line.Annotation.Color != null) - sb.Append(" notion-color-").Append(line.Annotation.Color); - if (line.Annotation?.Code != null) - sb.Append(" notion-code"); - sb.Append("\""); - } - - sb.Append(">"); - switch (line.Type) - { - case RichText.TypeText: - Append(line.Text, sb); - break; - case RichText.TypeEquation: - Append(line.Equation, sb); - break; - case RichText.TypeLink: - AppendUrl(line.Url, sb); - break; - case RichText.TypeMention: - Append(line.Mention, sb); - break; -#if DEBUG - default: - throw new ArgumentException($"Unknown RichText type {line.Type}"); -#endif - } - sb.Append(""); + sb.Append(" class=\""); + if (line.Annotation.Bold) + sb.Append(" notion-bold"); + if (line.Annotation.Italic) + sb.Append(" notion-italic"); + if (line.Annotation.Strikethrough) + sb.Append(" notion-strikethrough"); + if (line.Annotation.Underline) + sb.Append(" notion-underline"); + if (line.Annotation.Color != null) + sb.Append(" notion-color-").Append(line.Annotation.Color); + if (line.Annotation?.Code != null) + sb.Append(" notion-code"); + sb.Append("\""); } - else + + sb.Append(">"); + switch (line.Type) { - Append(line.Text, sb); + case RichText.TypeText: + Append(line.Text, sb); + break; + case RichText.TypeEquation: + Append(line.Equation, sb); + break; + case RichText.TypeLink: + AppendUrl(line.Url, sb); + break; + case RichText.TypeMention: + Append(line.Mention, sb); + break; +#if DEBUG + default: + throw new ArgumentException($"Unknown RichText type {line.Type}"); +#endif } - sb.Append("
"); - - return sb; + sb.Append(""); } - - protected virtual StringBuilder Append(RichTextText? text, StringBuilder sb) + else { - if (text == null) - return sb; + Append(line.Text, sb); + } + sb.Append("
"); - var hasLink = text.Link?.Url != null; - if (hasLink) - sb.Append(""); - sb.Append(text.Content); - if (hasLink) - sb.Append(""); + return sb; + } + protected virtual StringBuilder Append(RichTextText? text, StringBuilder sb) + { + if (text == null) return sb; - } + var hasLink = text.Link?.Url != null; + if (hasLink) + sb.Append(""); + sb.Append(text.Content); + if (hasLink) + sb.Append(""); - protected virtual StringBuilder AppendUrl(string? url, StringBuilder sb) - { - if (url != null) - sb.Append("").Append(url).Append(""); - return sb; - } + return sb; + } - protected virtual StringBuilder Append(RichTextEquation? equation, StringBuilder sb) - { - //TODO - return sb; - } + + protected virtual StringBuilder AppendUrl(string? url, StringBuilder sb) + { + if (url != null) + sb.Append("").Append(url).Append(""); + return sb; + } + + protected virtual StringBuilder Append(RichTextEquation? equation, StringBuilder sb) + { + //TODO + return sb; + } - protected virtual StringBuilder Append(Mention? mention, StringBuilder sb) - { - //TODO - return sb; - } + protected virtual StringBuilder Append(Mention? mention, StringBuilder sb) + { + //TODO + return sb; } - - +} + + // //Exclude code, page, bookmark // AcceptedBlockTypes = new List { "text", "header", "sub_header", "sub_sub_header", "bulleted_list", "image", "quote", "column_list", "column", "callout" }, -// TransformQuote = (data, block) => -// { -// sb.Append("
").AppendText(data).AppendLine("
"); -// return true; -// }, -// TransformImage = (data, block) => -// { -// if (data != null) -// sb.Append("
").AppendImage(data, block.Id).AppendLine("
"); -// -// return true; -// }, // TransformColumnList = (data, block) => // { // //Start of notion-column_list-block @@ -297,45 +396,4 @@ protected virtual StringBuilder Append(Mention? mention, StringBuilder sb) - // public static StringBuilder AppendImage(this StringBuilder sb, BlockImageData data, Guid blockId) - // { - // if (data != null) - // { - // var imageUrl = $"{data.ImageUrl}?table=block&id={blockId:D}&cache=v2"; - // if (data.Format != null) - // sb.Append(""); - // else - // sb.Append(""); - // - // if (!String.IsNullOrWhiteSpace(data.Caption)) - // sb.Append("
").Append(data.Caption).Append("
"); - // } - // - // return sb; - // } - // - // public static StringBuilder? GetTwitterEmojiUrl(this string emojiString) - // { - // var enc = new UTF32Encoding(true, false); - // var bytes = enc.GetBytes(emojiString); - // - // var sbCodepointEmoji = new StringBuilder(); - // for (var i = 0; i < bytes.Length; i += 4) - // { - // var value = bytes[i]<<24 | bytes[i + 1]<<16 | bytes[i + 2]<<8 | bytes[i + 3]; - // if(value == 0xFE0E || value == 0xFE0F) - // continue; - // sbCodepointEmoji.Append($"{value:x}-"); - // } - // - // if (sbCodepointEmoji.Length > 0 && sbCodepointEmoji[^1] == '-') - // sbCodepointEmoji.Remove(sbCodepointEmoji.Length - 1, 1); - // - // if (sbCodepointEmoji.Length == 0) - // return null; - // - // sbCodepointEmoji.Insert(0, "//cdn.jsdelivr.net/gh/twitter/twemoji/assets/svg/"); - // sbCodepointEmoji.Append(".svg"); - // return sbCodepointEmoji; - // } -} + diff --git a/NotionSharp.ApiClient/Lib/HttpNotionSession.cs b/NotionSharp.ApiClient/Lib/HttpNotionSession.cs index 8b42deb..864d0d5 100644 --- a/NotionSharp.ApiClient/Lib/HttpNotionSession.cs +++ b/NotionSharp.ApiClient/Lib/HttpNotionSession.cs @@ -41,8 +41,10 @@ namespace NotionSharp.ApiClient.Lib; [JsonSerializable(typeof(BlockTextAndChildren))] [JsonSerializable(typeof(BlockTextAndChildrenAndCheck))] [JsonSerializable(typeof(BlockChildPage))] -[JsonSerializable(typeof(BlockImage))] -[JsonSerializable(typeof(External))] +[JsonSerializable(typeof(NotionFile))] +[JsonSerializable(typeof(NotionFileContent))] +[JsonSerializable(typeof(NotionFileExternal))] +[JsonSerializable(typeof(NotionEmoji))] #endregion [JsonSerializable(typeof(Bot))] diff --git a/NotionSharp.ApiClient/Lib/PublicApi/Model/Block.cs b/NotionSharp.ApiClient/Lib/PublicApi/Model/Block.cs index 047179e..3f7a057 100644 --- a/NotionSharp.ApiClient/Lib/PublicApi/Model/Block.cs +++ b/NotionSharp.ApiClient/Lib/PublicApi/Model/Block.cs @@ -1,9 +1,14 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Text.Json.Serialization; +using NotionSharp.ApiClient.Lib.Helpers; namespace NotionSharp.ApiClient; +/// +/// https://developers.notion.com/reference/block +/// public static class BlockTypes { public const string Unsupported = "unsupported"; @@ -17,14 +22,81 @@ public static class BlockTypes public const string Toggle = "toggle"; public const string ChildPage = "child_page"; public const string Image = "image"; + public const string Quote = "quote"; + public const string File = "file"; + public const string Callout = "callout"; + public const string ColumnList = "column_list"; - public static readonly string[] Types = + public static readonly string[] SupportedBlocks = { - Unsupported, Paragraph, Heading1, Heading2, Heading3, BulletedListItem, NumberedListItem, ToDo, Toggle, ChildPage, - Image + BulletedListItem, Callout, //ChildDatabase, + //ChildPage, + ColumnList, + //When the is_toggleable property is true + Heading1, Heading2, Heading3, + NumberedListItem, Paragraph, Image, Quote, File, //SyncedBlock, Table, Template, + ToDo, Toggle + }; + + public static readonly string[] BlocksWithChildren = + { + BulletedListItem, Callout, //ChildDatabase, + ChildPage, ColumnList, + //When the is_toggleable property is true + Heading1, Heading2, Heading3, + NumberedListItem, Paragraph, Quote, //SyncedBlock, Table, Template, + ToDo, Toggle }; } +sealed class MyJsonStringEnumConverter() : JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower); + +[JsonConverter(typeof(MyJsonStringEnumConverter))] +public enum NotionColor +{ + Default, + Blue, + BlueBackground, + Brown, + BrownBackground, + Gray, + GrayBackground, + Green, + GreenBackground, + Orange, + OrangeBackground, + Yellow, + YellowBackground, + Pink, + PinkBackground, + Purple, + PurpleBackground, + Red, + RedBackground, +} + + +/* +Block types that support child blocks +Some block types contain nested blocks. The following block types support child blocks: + +Bulleted list item +Callout +Child database +Child page +Column +Heading 1, when the is_toggleable property is true +Heading 2, when the is_toggleable property is true +Heading 3, when the is_toggleable property is true +Numbered list item +Paragraph +Quote +Synced block +Table +Template +To do +Toggle +*/ public class Block : NamedObject, IBlockId { #region common props @@ -42,10 +114,20 @@ public Block() public string Type { get; init; } public DateTimeOffset CreatedTime { get; init; } + + /// + /// Partial User + /// + public User CreatedBy { get; init; } public DateTimeOffset LastEditedTime { get; init; } + public User LastEditedBy { get; init; } + public bool Archived { get; init; } public bool HasChildren { get; init; } + + [JsonIgnore] + public List Children { get; set; } #endregion #region Only one of those is set. Depends on Type. @@ -63,18 +145,47 @@ public Block() public BlockChildPage? ChildPage { get; init; } - //[JsonPropertyName("image")] - public BlockImage? Image { get; init; } + public NotionFile? Image { get; init; } + public NotionFile? File { get; init; } + public BlockTextAndChildrenAndColor? Quote { get; set; } + public BlockCallout? Callout { get; init; } #endregion } -public record BlockText([property: JsonPropertyName("rich_text")] List RichText); +public record BlockText(List RichText); public record BlockTextAndChildren(List Children, List RichText) : BlockText(RichText); public record BlockTextAndChildrenAndCheck(List Children, List RichText) : BlockTextAndChildren(Children, RichText) { public bool Checked { get; init; } } +public record BlockTextAndChildrenAndColor(List Children, List RichText) : BlockTextAndChildren(Children, RichText) +{ + public NotionColor Color { get; init; } +} public record BlockChildPage(string Title); -public record BlockImage(External? External); -public record External(string Url); \ No newline at end of file + +public record BlockCallout(List RichText, NotionColor Color, NotionBaseType Icon) +{ + /// + /// File or Emoji + /// + public NotionBaseType Icon { get; init; } = Icon; +} + +#region File and Emoji +public record NotionFile(string Type, NotionFileContent? File, NotionFileExternal? External) : NotionBaseType(Type); +public record NotionFileExternal(string Url); +public record NotionFileContent(string Url, string ExpiryTime); + +public record NotionEmoji(string Type, string Emoji) : NotionBaseType(Type); +#endregion + +[JsonConverter(typeof(BufferedJsonPolymorphicConverterFactory))] +[BufferedJsonPolymorphic(TypeDiscriminatorPropertyName = "type")] +[BufferedJsonDerivedType(typeof(NotionFile), "file")] +[BufferedJsonDerivedType(typeof(NotionEmoji), "emoji")] +public abstract record NotionBaseType(string Type); + +// public record BlockColumnList(List Columns); +// public record BlockColumnData(Block ColumnBlock, float Ratio); diff --git a/NotionSharp.ApiClient/Lib/PublicApi/Model/Common/Parent.cs b/NotionSharp.ApiClient/Lib/PublicApi/Model/Common/Parent.cs index d602f81..172f8c0 100644 --- a/NotionSharp.ApiClient/Lib/PublicApi/Model/Common/Parent.cs +++ b/NotionSharp.ApiClient/Lib/PublicApi/Model/Common/Parent.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace NotionSharp.ApiClient; public class Parent @@ -9,11 +7,8 @@ public class Parent ///
public string Type { get; init; } - [JsonPropertyName("database_id")] public string? DatabaseId { get; init; } - [JsonPropertyName("block_id")] public string? BlockId { get; init; } public string? Workspace { get; init; } - [JsonPropertyName("page_id")] public string? PageId { get; init; } } \ No newline at end of file diff --git a/NotionSharp.ApiClient/NotionSessionExtensions.cs b/NotionSharp.ApiClient/NotionSessionExtensions.cs index 030ad9b..b751a7a 100644 --- a/NotionSharp.ApiClient/NotionSessionExtensions.cs +++ b/NotionSharp.ApiClient/NotionSessionExtensions.cs @@ -76,9 +76,12 @@ public static async IAsyncEnumerable Search(this NotionSession session, if (response.StatusCode is not (>= 200 and < 300)) yield break; - var s = await response.GetStringAsync(); - +#if DEBUG + var jsonOriginal = await response.GetStringAsync(); +#endif + var result = await response.GetJsonAsync>(); + if(result?.Results == null || cancel.IsCancellationRequested) yield break; foreach (var item in result.Results) @@ -153,6 +156,9 @@ public static async IAsyncEnumerable GetBlockChildren(this NotionSession { //var result = await request.GetJsonAsync>(cancel).ConfigureAwait(false); var response = await request.AllowAnyHttpStatus().SendAsync(HttpMethod.Get, cancellationToken: cancel); +#if DEBUG + var jsonOriginal = await response.GetStringAsync(); +#endif var result = await response.GetJsonAsync>().ConfigureAwait(false);; if (result == null || response.StatusCode is 400) @@ -160,7 +166,7 @@ public static async IAsyncEnumerable GetBlockChildren(this NotionSession var errorResult = await response.GetStringAsync(); throw new NotionApiException(null, errorResult); } - + if(result.Results == null) yield break; foreach (var item in result.Results) @@ -226,18 +232,37 @@ public static async IAsyncEnumerable GetPageProperty(this NotionSe } } + /// + /// Fetch the child blocks of this block + /// + /// + /// + /// + public static async Task GetChildren(this NotionSession session, Block block, CancellationToken cancel = default) + { + block.Children = await session.GetBlockChildren(block.Id, cancel: cancel) + .Where(childBlock => BlockTypes.SupportedBlocks.Contains(childBlock.Type)) + .ToListAsync(cancel).ConfigureAwait(false); + } public static async Task GetHtml(this NotionSession session, Page page, CancellationToken cancel = default) { var blocks = await session.GetBlockChildren(page.Id, cancel: cancel) - .Where(b => b.Type != BlockTypes.ChildPage) + .Where(childBlock => BlockTypes.SupportedBlocks.Contains(childBlock.Type)) .ToListAsync(cancel).ConfigureAwait(false); if (blocks.Count == 0) return string.Empty; - var htmlRenderer = new HtmlRenderer(); - return htmlRenderer.GetHtml(blocks); + var blockWithChildren = blocks.Where(b => b.HasChildren && BlockTypes.BlocksWithChildren.Contains(b.Type)).ToList(); + foreach (var block in blockWithChildren) + { + await session.GetChildren(block, cancel); + //recursive + blockWithChildren.AddRange(block.Children.Where(b => b.HasChildren && BlockTypes.BlocksWithChildren.Contains(b.Type))); + } + + return new HtmlRenderer().GetHtml(blocks); } diff --git a/NotionSharp.ApiClient/NotionSharp.ApiClient.csproj b/NotionSharp.ApiClient/NotionSharp.ApiClient.csproj index d3b0712..05c8e3d 100644 --- a/NotionSharp.ApiClient/NotionSharp.ApiClient.csproj +++ b/NotionSharp.ApiClient/NotionSharp.ApiClient.csproj @@ -18,8 +18,8 @@ - 1.0.14 - + 2.0.0 + -pre1 $(DefineConstants); @@ -44,8 +44,8 @@ false https://github.com/softlion/NotionSharp - 1.0.0 - initial version + 1.0.14 + initial version of the official NotionSharp API