notion_to_html 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5c9fc0703ad943d3ec964a2c1f9eb69750dcbbe49fbd271f2f2f213d290d1e5e
4
+ data.tar.gz: dfdd2d0f9738e991d90d083898d288dcb0b7019a9415fc82f9d83773ea030262
5
+ SHA512:
6
+ metadata.gz: fe05f80c2b3e452639a0019ad6d2f215e6953b05e13434cc390b0827a1a3294db07e8f71fac9992ec2cc8d57b8f87ec02e27191d98690c1c29abc91a2a108548
7
+ data.tar.gz: dd192dd44381b12863c88470fe48b4811b1b3fa57722ae6e8cd1ce2c72daa12ac4ea42207d841fa678cfb8dc91315f3e2e129ab31b38c32fba0bd7ce1301b1a2
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Guillermo Aguirre
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # NotionToHtml
2
+
3
+ NotionToHtml is a Ruby gem designed to integrate Notion with Ruby applications. It provides a set of tools for rendering Notion pages and blocks, allowing you to maintain a database of pages in Notion while rendering them real time in you application with ease.
4
+
5
+ Now you can use Notion to publish your pages directly to your Ruby web page with one click.
6
+
7
+ ## Table of Contents
8
+
9
+ - [NotionToHtml](#notiontohtml)
10
+ - [Table of Contents](#table-of-contents)
11
+ - [About](#about)
12
+ - [Installation](#installation)
13
+ - [Dependencies](#dependencies)
14
+ - [Setup](#setup)
15
+ - [Notion Integration](#notion-integration)
16
+ - [Create your integration in Notion](#create-your-integration-in-notion)
17
+ - [Get your API secret](#get-your-api-secret)
18
+ - [Give your integration page permissions](#give-your-integration-page-permissions)
19
+ - [Configuration](#configuration)
20
+ - [Usage](#usage)
21
+ - [Rendering](#rendering)
22
+ - [Pages](#pages)
23
+ - [Specific Page](#specific-page)
24
+ - [Customizing styles](#customizing-styles)
25
+ - [Overriding default styles](#overriding-default-styles)
26
+ - [Querying](#querying)
27
+ - [Filtering](#filtering)
28
+ - [Sorting](#sorting)
29
+ - [Examples](#examples)
30
+ - [Development](#development)
31
+ - [Contributing](#contributing)
32
+ - [License](#license)
33
+ - [Code of Conduct](#code-of-conduct)
34
+
35
+ ## About
36
+
37
+ NotionToHtml allows you to seamlessly integrate Notion pages and blocks into your Ruby application. It provides a set of renderers for various Notion block types, including headings, paragraphs, images, and more. With NotionToHtml, you can easily display and format Notion content in your views.
38
+
39
+ You just need to create a database in Notion, integrate it and start writing!
40
+
41
+ ## Installation
42
+
43
+ Add the gem to your application's Gemfile:
44
+ ```bash
45
+ bundle add notion_to_html
46
+ ```
47
+ Or install it yourself as:
48
+ ```bash
49
+ gem install notion_to_html
50
+ ```
51
+ ## Dependencies
52
+ NotionToHtml uses [tailwindcss](https://tailwindcss.com/) classes to define a default styling that mimics Notion's own styling, so make sure to inlcude it in your application.
53
+ If you wish to use something else you can always override the default styling provided, see []() for more details.
54
+
55
+ ## Setup
56
+ This gem is currently very opinionated on how it expects the Notion database to be defined. If you wish to customize this you can override the methods defined in [NotionToHtml::Service](./lib/notion_to_html/service.rb).
57
+
58
+ By default the database should have the following structure:
59
+ ![Database structure](/docs/images/database_structure.png)
60
+
61
+ - _name, description & slug_ as Text
62
+ - tags as Multi-Select
63
+ - public as Checkbox
64
+ - published as Date
65
+
66
+ Once you have the database created you will have to setup a Notion Integration, so the Notion API can communicate with your database. For this you will have to follow the [Create Your Integration In Notion](https://developers.notion.com/docs/create-a-notion-integration#create-your-integration-in-notion) tutorial.
67
+
68
+ If you wish to just quickly set it up, you can follow the relevant steps below, which are taken from that tutorial.
69
+
70
+ ### Notion Integration
71
+ #### Create your integration in Notion
72
+ The first step to building any integration (internal or public) is to create a new integration in Notion’s integrations dashboard: <https://www.notion.com/my-integrations>.
73
+ 1. Click `+ New Integration`.
74
+ ![Create integration](/docs/images/new_integration.png)
75
+ 2. Enter the integration name and select the associated workspace for the new integration.
76
+ ![Select workspace](/docs/images/new_integration_select_workspace.png)
77
+
78
+ #### Get your API secret
79
+ API requests require an API secret to be successfully authenticated.
80
+ 1. Visit the Configuration tab to get your integration’s API secret (or “Internal Integration Secret”).
81
+ ![API secret](/docs/images/get_api_key.png)
82
+ **Remember to keep your API secret a secret!**
83
+ Any value used to authenticate API requests should always be kept secret. Use environment variables and avoid committing sensitive data to your version control history.
84
+ If you do accidentally expose it, remember to “refresh” your secret.
85
+
86
+ #### Give your integration page permissions
87
+ The database that we’re about to create will be added to a parent Notion page in your workspace. For your integration to interact with the page, it needs explicit permission to read/write to that specific Notion page.
88
+
89
+ To give the integration permission, you will need to:
90
+ 1. Go to the page with the database you created above.
91
+ 2. Click on the ... More menu in the top-right corner of the page.
92
+ 3. Scroll down to + Add Connections.
93
+ 4. Search for your integration and select it.
94
+ 5. Confirm the integration can access the page and all of its child pages.
95
+ ![alt text](/docs/images/permissions.gif)
96
+ 6. You can then limit the integrations permission to just `Read Contents`:
97
+ ![alt text](/docs/images/permissions.png)
98
+
99
+ Now you're finally ready to config the gem!
100
+
101
+ ### Configuration
102
+ To configure NotionToHtml, you need to set up your Notion API token and database ID.
103
+ If you're using Rails add an initializer file in your Rails application, such as `config/initializers/notion_to_html.rb`, and include the following configuration block:
104
+ ```ruby
105
+ NotionToHtml.configure do |config|
106
+ config.notion_api_token = 'NOTION_API_TOKEN'
107
+ config.notion_database_id = 'NOTION_DATABASE_ID'
108
+ end
109
+ ```
110
+
111
+ To get these values:
112
+ 1. The NOTION_API_TOKEN is the same one from [the setup](#get-your-api-secret).
113
+ 2. To get the NOTION_DATABASE_ID, locate the 32-character string at the end of the page’s URL.
114
+ ```bash
115
+ https://www.notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=...
116
+ |--------- Database ID --------|
117
+ ```
118
+
119
+ **Remember to keep these values secret!**
120
+
121
+ Now you should be all setup!
122
+
123
+ ## Usage
124
+ ### Rendering
125
+ #### Pages
126
+ To get and render a preview of the pages of your database:
127
+ ```ruby
128
+ <% NotionToHtml::Service.get_pages.each do |page| %>
129
+ <%= article.formatted_published_at %>
130
+ <%= article.id %>
131
+ <%= article.formatted_title %>
132
+ <%= article.formatted_description %>
133
+ <% end %>
134
+ ```
135
+ #### Specific Page
136
+ To get and render a specific page:
137
+ ```ruby
138
+ <% page = NotionToHtml::Service.get_page(page_id) %>
139
+ <%= page.formatted_title %>
140
+ <%= page.formatted_published_at %>
141
+ <% page.formatted_blocks.each do |block| %>
142
+ <%= block %>
143
+ <% end %>
144
+ ```
145
+ #### Customizing styles
146
+ NotionToHtml ships with default css classes for each supported block. You can add your own set of styling on top by specifying the `class:` option when calling the formatter:
147
+ ```ruby
148
+ NotionToHtml::Service.get_page(page_id)
149
+ .formatted_title(class: 'text-4xl md:text-5xl font-bold')
150
+ ```
151
+ You can also specify classes for each type of supported block like this:
152
+ ```ruby
153
+ NotionToHtml::Service.get_page(page_id).formatted_blocks(
154
+ paragraph: 'text-lg',
155
+ heading_1: 'text-3xl md:text-4xl font-bold',
156
+ heading_2: 'text-white',
157
+ heading_3: 'font-bold',
158
+ quote: 'italic',
159
+ )
160
+ ```
161
+ #### Overriding default styles
162
+ If you feel like you want a clean slate regarding styling you can override the provided default styles by setting the `override_class` option to `true`:
163
+ ```ruby
164
+ NotionToHtml::Service.get_page(page_id)
165
+ .formatted_title(class: 'font-bold', override_class: true)
166
+ ```
167
+ It also works for `formatted_blocks`:
168
+ ```ruby
169
+ NotionToHtml::Service.get_page(page_id)
170
+ .formatted_blocks(
171
+ paragraph: { class: 'text-lg', override_class: true },
172
+ quote: 'italic'
173
+ )
174
+ ```
175
+ ### Querying
176
+ By default the `NotionToHtml::Service` is setup to follow the database structure sepcified above. This way it will only return pages that have been marked as `public`.
177
+
178
+ #### Filtering
179
+ You can filter the results by specifying a tag and/or a specific slug:
180
+ ```ruby
181
+ NotionToHtml::Service.get_pages(tag: 'web', slug: 'test-slug')
182
+ ```
183
+ #### Sorting
184
+ The default sorting is by the `published` Date column in the database
185
+
186
+ ### Examples
187
+ To see how the default renderings of the supported blocks look, go over to the [examples](/examples/).
188
+
189
+ ## Development
190
+
191
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
192
+
193
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
194
+
195
+ ## Contributing
196
+
197
+ Bug reports and pull requests are welcome on GitHub at https://github.com/guillermoaguirre1@gmail.com/notion_to_html. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](/CODE_OF_CONDUCT.md).
198
+
199
+ ## License
200
+
201
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
202
+
203
+ ## Code of Conduct
204
+
205
+ Everyone interacting in the Notion::Rails project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](/CODE_OF_CONDUCT.md).
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The NotionToHtml::BaseBlock class represents a block in a Notion page, handling its attributes and rendering.
4
+ # This class processes the raw data of a block fetched from the Notion API and makes
5
+ # it accessible through various attributes. It also provides methods to render formatted
6
+ # output for different block types like paragraphs, headings, lists, quotes, and media.
7
+
8
+ module NotionToHtml
9
+ class BaseBlock
10
+ include NotionToHtml::Renderers
11
+
12
+ # @return [String] the ID of the block.
13
+ attr_reader :id
14
+ # @return [String] the creation timestamp of the block.
15
+ attr_reader :created_time
16
+ # @return [String] the last edited timestamp of the block.
17
+ attr_reader :last_edited_time
18
+ # @return [String] the user who created the block.
19
+ attr_reader :created_by
20
+ # @return [String] the user who last edited the block.
21
+ attr_reader :last_edited_by
22
+ # @return [Hash] the parent of the block (e.g., page ID).
23
+ attr_reader :parent
24
+ # @return [Boolean] whether the block is archived.
25
+ attr_reader :archived
26
+ # @return [Boolean] whether the block has children.
27
+ attr_reader :has_children
28
+ # @return [String] the type of the block (e.g., 'paragraph', 'heading_1').
29
+ attr_reader :type
30
+ # @return [Hash] the properties of the block, specific to its type.
31
+ attr_reader :properties
32
+
33
+ # @return [Array<BaseBlock>] the children blocks of this block.
34
+ attr_accessor :children
35
+ # @return [Array<BaseBlock>] the sibling blocks of this block.
36
+ attr_accessor :siblings
37
+
38
+ # The list of block types that can be rendered.
39
+ BLOCK_TYPES = %w[
40
+ paragraph
41
+ heading_1
42
+ heading_2
43
+ heading_3
44
+ bulleted_list_item
45
+ numbered_list_item
46
+ quote
47
+ callout
48
+ code
49
+ image
50
+ embed
51
+ video
52
+ ].freeze
53
+
54
+ # Initializes a new BaseBlock object.
55
+ # @param data [Hash] The raw data of the block from the Notion API.
56
+ def initialize(data)
57
+ @id = data['id']
58
+ @created_time = data['created_time']
59
+ @last_edited_time = data['last_edited_time']
60
+ @created_by = data['created_by'] # TODO: handle user object
61
+ @last_edited_by = data['last_edited_by'] # TODO: handle user object
62
+ @parent = data['parent'] # TODO: handle page_id type
63
+ @archived = data['archived']
64
+ @has_children = data['has_children']
65
+ @children = []
66
+ @siblings = []
67
+ @type = data['type']
68
+ @properties = data[@type]
69
+ end
70
+
71
+ # Renders the block based on its type.
72
+ # @param options [Hash] Additional options for rendering the block.
73
+ # @return [String] The rendered block as HTML.
74
+ def render(options = {})
75
+ case @type
76
+ when 'paragraph'
77
+ render_paragraph(rich_text, class: options[:paragraph])
78
+ when 'heading_1'
79
+ render_heading_1(rich_text, class: options[:heading_1])
80
+ when 'heading_2'
81
+ render_heading_2(rich_text, class: options[:heading_2])
82
+ when 'heading_3'
83
+ render_heading_3(rich_text, class: options[:heading_3])
84
+ when 'table_of_contents'
85
+ render_table_of_contents
86
+ when 'bulleted_list_item'
87
+ render_bulleted_list_item(rich_text, @siblings, @children, 0, class: options[:bulleted_list_item])
88
+ when 'numbered_list_item'
89
+ render_numbered_list_item(rich_text, @siblings, @children, 0, class: options[:numbered_list_item])
90
+ when 'quote'
91
+ render_quote(rich_text, class: options[:quote])
92
+ when 'callout'
93
+ render_callout(rich_text, icon, class: options[:callout])
94
+ when 'code'
95
+ render_code(rich_text, class: options[:code], language: @properties['language'])
96
+ when 'image', 'embed'
97
+ render_image(*multi_media, class: options[:image])
98
+ when 'video'
99
+ render_video(*multi_media, class: options[:video])
100
+ else
101
+ 'Unsupported block'
102
+ end
103
+ end
104
+
105
+ # Retrieves the rich text content of the block.
106
+ # @return [Array<Hash>] The rich text content.
107
+ def rich_text
108
+ @properties['rich_text'] || []
109
+ end
110
+
111
+ # Retrieves the icon associated with the block.
112
+ # @return [Array<Hash>] The icon data.
113
+ def icon
114
+ icon = @properties['icon']
115
+ @properties['icon'][icon['type']] || []
116
+ end
117
+
118
+ # Retrieves the multimedia data for the block.
119
+ # @return [Array] The multimedia data (URL, expiry time, caption, type).
120
+ def multi_media
121
+ case @properties['type']
122
+ when 'file'
123
+ [@properties.dig('file', 'url'), @properties.dig('file', 'expiry_time'), @properties['caption'], 'file']
124
+ when 'external'
125
+ [@properties.dig('external', 'url'), nil, @properties['caption'], 'external']
126
+ else
127
+ [@properties['url'], nil, @properties['caption'], nil]
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The NotionToHtml::BasePage class represents a Notion page, handling its attributes and rendering.
4
+ # This class processes the raw data of a page fetched from the Notion API and makes
5
+ # it accessible through various attributes. It also provides methods to render formatted
6
+ # output for the page's title, description, and published date.
7
+
8
+ module NotionToHtml
9
+ class BasePage
10
+ include NotionToHtml::Renderers
11
+
12
+ # @return [String] the ID of the page.
13
+ attr_reader :id
14
+ # @return [String] the creation timestamp of the page.
15
+ attr_reader :created_time
16
+ # @return [String] the last edited timestamp of the page.
17
+ attr_reader :last_edited_time
18
+ # @return [String] the user who created the page.
19
+ attr_reader :created_by
20
+ # @return [String] the user who last edited the page.
21
+ attr_reader :last_edited_by
22
+ # @return [Hash, nil] the cover image of the page.
23
+ attr_reader :cover
24
+ # @return [Hash, nil] the icon of the page.
25
+ attr_reader :icon
26
+ # @return [Hash] the parent of the page (e.g., database ID).
27
+ attr_reader :parent
28
+ # @return [Boolean] whether the page is archived.
29
+ attr_reader :archived
30
+ # @return [Hash] the properties of the page.
31
+ attr_reader :properties
32
+ # @return [String, nil] the publication date of the page.
33
+ attr_reader :published_at
34
+ # @return [Array<Hash>, nil] the tags associated with the page.
35
+ attr_reader :tags
36
+ # @return [Array<Hash>, nil] the title of the page.
37
+ attr_reader :title
38
+ # @return [String, nil] the slug of the page.
39
+ attr_reader :slug
40
+ # @return [Array<Hash>, nil] the description of the page.
41
+ attr_reader :description
42
+ # @return [String] the URL of the page.
43
+ attr_reader :url
44
+
45
+ # Initializes a new BasePage object.
46
+ # @param data [Hash] The raw data of the page from the Notion API.
47
+ def initialize(data)
48
+ @id = data['id']
49
+ @created_time = data['created_time']
50
+ @last_edited_time = data['last_edited_time']
51
+ @created_by = data['created_by'] # TODO: handle user object
52
+ @last_edited_by = data['last_edited_by'] # TODO: handle user object
53
+ @cover = data['cover'] # TODO: handle external type
54
+ @icon = data['icon'] # TODO: handle emoji type
55
+ @parent = data['parent'] # TODO: handle database_id type
56
+ @archived = data['archived']
57
+ @properties = data['properties'] # TODO: handle properties object
58
+ process_properties
59
+ @url = data['url']
60
+ end
61
+
62
+ # Renders the formatted title of the page.
63
+ # @param options [Hash] Additional options for rendering the title.
64
+ # @return [String] The formatted title.
65
+ def formatted_title(options = {})
66
+ render_heading_1(@title, options)
67
+ end
68
+
69
+ # Renders the formatted description of the page.
70
+ # @param options [Hash] Additional options for rendering the description.
71
+ # @return [String] The formatted description.
72
+ def formatted_description(options = {})
73
+ render_paragraph(@description, options)
74
+ end
75
+
76
+ # Renders the formatted publication date of the page.
77
+ # @param options [Hash] Additional options for rendering the publication date.
78
+ # @return [String] The formatted publication date.
79
+ def formatted_published_at(options = {})
80
+ render_date(@published_at, options)
81
+ end
82
+
83
+ private
84
+
85
+ # Processes the properties of the page and assigns them to the relevant attributes.
86
+ # @return [void]
87
+ def process_properties
88
+ @tags = @properties['tags']
89
+ @title = @properties.dig('name', 'title')
90
+ @slug = @properties['slug']
91
+ @published_at = @properties.dig('published', 'date', 'start')
92
+ @description = @properties.dig('description', 'rich_text')
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The NotionToHtml::Page class represents a Notion page, containing both the metadata and blocks associated with the page.
4
+ # This class integrates metadata and blocks, providing methods to format and render the page's content.
5
+
6
+ module NotionToHtml
7
+ class Page
8
+ include NotionToHtml::Renderers
9
+
10
+ # @return [BasePage] The metadata of the page, encapsulating details like title, description, and published date.
11
+ attr_reader :metadata
12
+ # @return [Array<BaseBlock>] The blocks of the page, representing the content sections.
13
+ attr_reader :blocks
14
+
15
+ # Delegate methods to metadata for easy access to formatted title, description, and published date.
16
+ delegate :formatted_title, to: :metadata
17
+ delegate :formatted_description, to: :metadata
18
+ delegate :formatted_published_at, to: :metadata
19
+
20
+ # Initializes a new Page object.
21
+ # @param base_page [BasePage] The metadata of the page.
22
+ # @param base_blocks [Array<BaseBlock>] The blocks of the page.
23
+ def initialize(base_page, base_blocks)
24
+ @metadata = base_page
25
+ @blocks = base_blocks
26
+ end
27
+
28
+ # Formats and renders the blocks of the page.
29
+ # @param options [Hash] Additional options for rendering the blocks.
30
+ # @return [Array<String>] The rendered blocks as an array of HTML strings.
31
+ def formatted_blocks(options = {})
32
+ @blocks.map { |block| block.render(options) }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_view'
4
+
5
+ # The NotionToHtml::Renderers module provides functionality for rendering Notion content
6
+ # into HTML format. It includes various helper methods from ActionView to assist
7
+ # in rendering different Notion blocks like paragraphs, headings, lists, images,
8
+ # and more.
9
+
10
+ module NotionToHtml
11
+ module Renderers
12
+ include ActionView::Helpers::AssetTagHelper
13
+ include ActionView::Helpers::TagHelper
14
+ include ActionView::Helpers::UrlHelper
15
+ include ActionView::Context
16
+
17
+ # Default CSS classes for different types of Notion blocks.
18
+ DEFAULT_CSS_CLASSES = {
19
+ bulleted_list_item: 'list-disc list-inside break-words',
20
+ callout: 'flex flex-column p-4 rounded mt-4',
21
+ code: 'border-2 p-6 rounded w-full overflow-x-auto',
22
+ date: '',
23
+ heading_1: 'mb-4 mt-6 text-3xl font-semibold',
24
+ heading_2: 'mb-4 mt-6 text-2xl font-semibold',
25
+ heading_3: 'mb-2 mt-6 text-xl font-semibold',
26
+ image: '',
27
+ numbered_list_item: 'list-decimal list-inside break-words',
28
+ paragraph: '',
29
+ quote: 'border-l-4 border-black px-5 py-1',
30
+ video: 'w-full'
31
+ }.freeze
32
+
33
+ # Converts text annotations to corresponding CSS classes.
34
+ #
35
+ # @param annotations [Hash] the annotations hash containing keys like 'bold', 'italic', 'color', etc.
36
+ # @return [String] a string of CSS classes.
37
+ def annotation_to_css_class(annotations)
38
+ classes = annotations.keys.map do |key|
39
+ case key
40
+ when 'strikethrough'
41
+ 'line-through' if annotations[key]
42
+ when 'bold'
43
+ 'font-bold' if annotations[key]
44
+ when 'code'
45
+ 'inline-code' if annotations[key]
46
+ when 'color'
47
+ "text-#{annotations["color"]}-600" if annotations[key] != 'default'
48
+ else
49
+ annotations[key] ? key : nil
50
+ end
51
+ end
52
+ classes.compact.join(' ')
53
+ end
54
+
55
+ # Renders a rich text property into HTML.
56
+ #
57
+ # @param properties [Array] the rich text array containing text fragments and annotations.
58
+ # @param options [Hash] additional options for rendering, such as CSS classes.
59
+ # @return [String] an HTML-safe string with the rendered text.
60
+ def text_renderer(properties, options = {})
61
+ properties.map do |rich_text|
62
+ classes = annotation_to_css_class(rich_text['annotations'])
63
+ if rich_text['href']
64
+ link_to(
65
+ rich_text['plain_text'],
66
+ rich_text['href'],
67
+ class: "link #{classes} #{options[:class]}"
68
+ )
69
+ elsif classes.present?
70
+ content_tag(:span, rich_text['plain_text'], class: "#{classes} #{options[:class]}")
71
+ else
72
+ tag.span(rich_text['plain_text'], class: options[:class])
73
+ end
74
+ end.join('').html_safe
75
+ end
76
+
77
+ # Renders a bulleted list item.
78
+ #
79
+ # @param rich_text_array [Array] the rich text array containing the content of the list item.
80
+ # @param _siblings [Array] sibling list items.
81
+ # @param children [Array] child list items.
82
+ # @param options [Hash] additional options for rendering.
83
+ # @return [String] an HTML-safe string with the rendered list item.
84
+ def render_bulleted_list_item(rich_text_array, siblings, children, parent_index, options = {})
85
+ content_tag(:ul, **options, class: css_class_for(:bulleted_list_item, options)) do
86
+ render_list_items(:bulleted_list_item, rich_text_array, siblings, children, parent_index, options)
87
+ end
88
+ end
89
+
90
+ # Renders a callout block.
91
+ #
92
+ # @param rich_text_array [Array] the rich text array containing the content of the callout.
93
+ # @param icon [String] the icon to display in the callout.
94
+ # @param options [Hash] additional options for rendering.
95
+ # @return [String] an HTML-safe string with the rendered callout.
96
+ def render_callout(rich_text_array, icon, options = {})
97
+ content_tag(:div, **options, class: css_class_for(:callout, options)) do
98
+ content = tag.span(icon, class: 'mr-4')
99
+ content += tag.div do
100
+ text_renderer(rich_text_array)
101
+ end
102
+ content
103
+ end
104
+ end
105
+
106
+ # Renders a code block.
107
+ #
108
+ # @param rich_text_array [Array] the rich text array containing the code content.
109
+ # @param options [Hash] additional options for rendering, including the programming language.
110
+ # @return [String] an HTML-safe string with the rendered code block.
111
+ def render_code(rich_text_array, options = {})
112
+ # TODO: render captions
113
+ content_tag(:div, data: { controller: 'highlight' }) do
114
+ content_tag(:div, data: { highlight_target: 'source' }) do
115
+ content_tag(:pre, **options, class: "#{css_class_for(:code, options)} language-#{options[:language]}") do
116
+ text_renderer(rich_text_array, options)
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ # Renders a date block.
123
+ #
124
+ # @param date [Date] the date to be rendered.
125
+ # @param options [Hash] additional options for rendering.
126
+ # @return [String] an HTML-safe string with the rendered date.
127
+ def render_date(date, options = {})
128
+ # TODO: handle end and time zone
129
+ # date=end=, start=2023-07-13, time_zone=, id=%5BsvU, type=date
130
+ tag.p(date.to_date.to_fs(:long), class: css_class_for(:date, options))
131
+ end
132
+
133
+ # Renders a heading 1 block.
134
+ #
135
+ # @param rich_text_array [Array] the rich text array containing the content of the heading.
136
+ # @param options [Hash] additional options for rendering.
137
+ # @return [String] an HTML-safe string with the rendered heading 1.
138
+ def render_heading_1(rich_text_array, options = {})
139
+ content_tag(:h1, **options, class: css_class_for(:heading_1, options)) do
140
+ text_renderer(rich_text_array)
141
+ end
142
+ end
143
+
144
+ # Renders a heading 2 block.
145
+ #
146
+ # @param rich_text_array [Array] the rich text array containing the content of the heading.
147
+ # @param options [Hash] additional options for rendering.
148
+ # @return [String] an HTML-safe string with the rendered heading 2.
149
+ def render_heading_2(rich_text_array, options = {})
150
+ content_tag(:h2, **options, class: css_class_for(:heading_2, options)) do
151
+ text_renderer(rich_text_array)
152
+ end
153
+ end
154
+
155
+ # Renders a heading 3 block.
156
+ #
157
+ # @param rich_text_array [Array] the rich text array containing the content of the heading.
158
+ # @param options [Hash] additional options for rendering.
159
+ # @return [String] an HTML-safe string with the rendered heading 3.
160
+ def render_heading_3(rich_text_array, options = {})
161
+ content_tag(:h3, **options, class: css_class_for(:heading_3, options)) do
162
+ text_renderer(rich_text_array)
163
+ end
164
+ end
165
+
166
+ # Renders an image block.
167
+ #
168
+ # @param src [String] the source URL of the image.
169
+ # @param _expiry_time [Time] the expiration time of the image.
170
+ # @param caption [Array] the caption text array for the image.
171
+ # @param _type [String] the type of image (e.g., 'external', 'file').
172
+ # @param options [Hash] additional options for rendering.
173
+ # @return [String] an HTML-safe string with the rendered image.
174
+ def render_image(src, _expiry_time, caption, _type, options = {})
175
+ content_tag(:figure, **options, class: css_class_for(:image, options)) do
176
+ content = tag.img(src: src, alt: '')
177
+ content += tag.figcaption(text_renderer(caption))
178
+ content
179
+ end
180
+ end
181
+
182
+ # Renders a numbered list item.
183
+ #
184
+ # @param rich_text_array [Array] the rich text array containing the content of the list item.
185
+ # @param siblings [Array] sibling list items.
186
+ # @param children [Array] child list items.
187
+ # @param options [Hash] additional options for rendering.
188
+ # @return [String] an HTML-safe string with the rendered list item.
189
+ def render_numbered_list_item(rich_text_array, siblings, children, parent_index, options = {})
190
+ content_tag(:ol, **options, class: css_class_for(:numbered_list_item, options)) do
191
+ render_list_items(:numbered_list_item, rich_text_array, siblings, children, parent_index, options)
192
+ end
193
+ end
194
+
195
+ # Renders a paragraph block.
196
+ #
197
+ # @param rich_text_array [Array] the rich text array containing the content of the paragraph.
198
+ # @param options [Hash] additional options for rendering.
199
+ # @return [String] an HTML-safe string with the rendered paragraph.
200
+ def render_paragraph(rich_text_array, options = {})
201
+ content_tag(:p, **options, class: css_class_for(:paragraph, options)) do
202
+ text_renderer(rich_text_array)
203
+ end
204
+ end
205
+
206
+ # Renders a quote block.
207
+ #
208
+ # @param rich_text_array [Array] the rich text array containing the content of the quote.
209
+ # @param options [Hash] additional options for rendering.
210
+ # @return [String] an HTML-safe string with the rendered quote.
211
+ def render_quote(rich_text_array, options = {})
212
+ content_tag(:div, options) do
213
+ content_tag(:cite) do
214
+ content_tag(:p, **options, class: css_class_for(:quote, options)) do
215
+ text_renderer(rich_text_array)
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ # Renders a table of contents block.
222
+ #
223
+ # @param options [Hash] additional options for rendering.
224
+ # @return [String] an HTML-safe string with the rendered table of contents.
225
+ def render_table_of_contents(options = {})
226
+ content_tag(:p, 'Table of Contents', class: css_class_for(:table_of_contents, options))
227
+ end
228
+
229
+ # Renders a video block.
230
+ #
231
+ # @param src [String] the source URL of the video.
232
+ # @param _expiry_time [Time] the expiration time of the video.
233
+ # @param caption [Array] the caption text array for the video.
234
+ # @param options [Hash] additional options for rendering.
235
+ # @return [String] an HTML-safe string with the rendered video.
236
+ def render_video(src, _expiry_time, caption, type, options = {})
237
+ content_tag(:figure, **options, class: css_class_for(:video, options)) do
238
+ content = if type == 'file'
239
+ video_tag(src, controls: true, **options, class: css_class_for(:video, options))
240
+ elsif type == 'external'
241
+ options[:class] = "#{options[:class]} aspect-video"
242
+ tag.iframe(src: src, allowfullscreen: true, **options, class: css_class_for(:video, options))
243
+ end
244
+ content += tag.figcaption(text_renderer(caption))
245
+ content
246
+ end
247
+ end
248
+
249
+ private
250
+
251
+ # Determines the CSS class for a given block type.
252
+ #
253
+ # @param type [Symbol] the block type (e.g., :paragraph, :heading_1, etc.).
254
+ # @param options [Hash] additional options for rendering.
255
+ # @return [String] the CSS class for the block.
256
+ def css_class_for(type, options)
257
+ if options[:override_class]
258
+ options[:class]
259
+ else
260
+ "#{DEFAULT_CSS_CLASSES[type]} #{options[:class]}".strip
261
+ end
262
+ end
263
+
264
+ # Renders list items (bulleted or numbered).
265
+ #
266
+ # @param list_type [Symbol] the type of list (:bulleted_list_item or :numbered_list_item).
267
+ # @param rich_text_array [Array] the rich text array containing the content of the list item.
268
+ # @param siblings [Array] sibling list items.
269
+ # @param children [Array] child list items.
270
+ # @param options [Hash] additional options for rendering.
271
+ # @return [String] an HTML-safe string with the rendered list items.
272
+ def render_list_items(type, rich_text_array, siblings, children, parent_index, options = {})
273
+ content = content_tag(:li, class: "#{options[:class]} ml-#{parent_index.to_i * 2}".strip) do
274
+ text_renderer(rich_text_array)
275
+ end
276
+ if children.present?
277
+ res = children.map do |child|
278
+ send("render_#{type}".to_sym, child.rich_text, child.siblings, child.children, parent_index.to_i + 1, options)
279
+ end
280
+ content += res.join('').html_safe
281
+ end
282
+ if siblings.present?
283
+ content += siblings.map do |sibling|
284
+ render_list_items(type, sibling.rich_text, sibling.siblings, sibling.children, parent_index.to_i, options)
285
+ end.join('').html_safe
286
+ end
287
+ content.html_safe
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'notion-ruby-client'
4
+
5
+ # The NotionToHtml::Service module is responsible for managing interactions with the Notion API, specifically for retrieving and processing pages and blocks.
6
+ # This module provides a set of methods that handle queries, sorting, and filtering based on tags, slugs, and other properties.
7
+ # It also manages the retrieval of page content, including associated blocks, and ensures that images are refreshed if their expiry time has passed.
8
+
9
+ module NotionToHtml
10
+ class Service
11
+ class << self
12
+ # Generates the default query for fetching pages
13
+ # @param tag [String, nil] The tag to filter pages by, or nil to include all tags
14
+ # @param slug [String, nil] The slug to filter pages by, or nil to include all slugs
15
+ # @return [Array<Hash>] The default query to be used in the database query
16
+ def default_query(tag: nil, slug: nil)
17
+ query = [
18
+ {
19
+ property: 'public',
20
+ checkbox: {
21
+ equals: true
22
+ }
23
+ }
24
+ ]
25
+
26
+ if slug
27
+ query.push({
28
+ property: 'slug',
29
+ rich_text: {
30
+ equals: slug
31
+ }
32
+ })
33
+ end
34
+
35
+ if tag
36
+ query.push({
37
+ property: 'tags',
38
+ multi_select: {
39
+ contains: tag
40
+ }
41
+ })
42
+ end
43
+
44
+ query
45
+ end
46
+
47
+ # Provides the default sorting order for fetching pages
48
+ # @return [Hash] The default sorting criteria for database queries
49
+ def default_sorting
50
+ {
51
+ property: 'published',
52
+ direction: 'descending'
53
+ }
54
+ end
55
+
56
+ # Fetches a list of pages from Notion based on provided filters
57
+ # @param tag [String, nil] The tag to filter pages by, or nil to include all tags
58
+ # @param slug [String, nil] The slug to filter pages by, or nil to include all slugs
59
+ # @param page_size [Integer] The number of pages to fetch per page
60
+ # @return [Array<NotionToHtml::BasePage>] The list of pages as BasePage objects
61
+ def get_pages(tag: nil, slug: nil, page_size: 10)
62
+ __get_pages(tag: tag, slug: slug, page_size: page_size)['results'].map do |page|
63
+ NotionToHtml::BasePage.new(page)
64
+ end
65
+ end
66
+
67
+ # Fetches a single page by its ID
68
+ # @param id [String] The ID of the page to fetch
69
+ # @return [NotionToHtml::Page] The page as a NotionToHtml::Page object
70
+ def get_page(id)
71
+ base_page = NotionToHtml::BasePage.new(__get_page(id))
72
+ base_blocks = get_blocks(id)
73
+ NotionToHtml::Page.new(base_page, base_blocks)
74
+ end
75
+
76
+ # Fetches blocks associated with a given page ID
77
+ # @param id [String] The ID of the page whose blocks are to be fetched
78
+ # @return [Array<NotionToHtml::BaseBlock>] The list of blocks as BaseBlock objects
79
+ def get_blocks(id)
80
+ blocks = __get_blocks(id)
81
+ parent_list_block_index = nil
82
+ results = []
83
+ blocks['results'].each_with_index do |block, index|
84
+ block = refresh_block(block['id']) if refresh_image?(block)
85
+ base_block = NotionToHtml::BaseBlock.new(block)
86
+ base_block.children = get_blocks(base_block.id) if base_block.has_children
87
+ if %w[numbered_list_item].include? base_block.type
88
+ siblings = !parent_list_block_index.nil? &&
89
+ index != parent_list_block_index &&
90
+ base_block.type == results[parent_list_block_index]&.type &&
91
+ base_block.parent == results[parent_list_block_index]&.parent
92
+ if siblings
93
+ results[parent_list_block_index].siblings << base_block
94
+ next
95
+ else
96
+ parent_list_block_index = results.length
97
+ end
98
+ else
99
+ parent_list_block_index = nil
100
+ end
101
+ results << base_block
102
+ end
103
+ results
104
+ end
105
+
106
+ # Determines if an image block needs to be refreshed based on its expiry time
107
+ # @param data [Hash] The data of the image block
108
+ # @return [Boolean] True if the image needs to be refreshed, false otherwise
109
+ def refresh_image?(data)
110
+ return false unless data['type'] == 'image'
111
+ return false unless data.dig('image', 'type') == 'file'
112
+
113
+ expiry_time = data.dig('image', 'file', 'expiry_time')
114
+ expiry_time.to_datetime.past?
115
+ end
116
+
117
+ private
118
+
119
+ # Accessor for the client
120
+ # @return [Notion::Client] The client instance used to interact with the Notion API
121
+ def client
122
+ @client ||= Notion::Client.new(token: NotionToHtml.config.notion_api_token)
123
+ end
124
+
125
+ # Retrieves pages from Notion using the client
126
+ # @param tag [String, nil] The tag to filter pages by
127
+ # @param slug [String, nil] The slug to filter pages by
128
+ # @param page_size [Integer] The number of pages to fetch per page
129
+ # @return [Hash] The response from the Notion API containing pages
130
+ def __get_pages(tag: nil, slug: nil, page_size: 10)
131
+ client.database_query(
132
+ database_id: NotionToHtml.config.notion_database_id,
133
+ sorts: [
134
+ default_sorting
135
+ ],
136
+ filter: {
137
+ 'and': default_query(tag: tag, slug: slug)
138
+ },
139
+ page_size: page_size
140
+ )
141
+ end
142
+
143
+ # Retrieves a single page by its ID from Notion
144
+ # @param id [String] The ID of the page to fetch
145
+ # @return [Hash] The response from the Notion API containing the page
146
+ def __get_page(id)
147
+ client.page(page_id: id)
148
+ end
149
+
150
+ # Retrieves blocks associated with a given ID from Notion, using cache if available
151
+ # @param id [String] The ID of the block to fetch
152
+ # @return [Hash] The response from the Notion API containing the blocks
153
+ def __get_blocks(id)
154
+ NotionToHtml.config.cache_store.fetch(id) { client.block_children(block_id: id) }
155
+ end
156
+
157
+ # Retrieves a single block by its ID from Notion
158
+ # @param id [String] The ID of the block to fetch
159
+ # @return [Hash] The response from the Notion API containing the block
160
+ def __get_block(id)
161
+ client.block(block_id: id)
162
+ end
163
+
164
+ # Refreshes a block by retrieving it again from Notion
165
+ # @param id [String] The ID of the block to refresh
166
+ # @return [Hash] The response from the Notion API containing the refreshed block
167
+ def refresh_block(id)
168
+ __get_block(id)
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionToHtml
4
+ # The current version of the NotionToHtml gem.
5
+ VERSION = '1.0.0'
6
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+ require 'notion_to_html/renderers'
5
+ require 'notion_to_html/base_block'
6
+ require 'notion_to_html/base_page'
7
+ require 'notion_to_html/page'
8
+ require 'notion_to_html/service'
9
+ require 'dry-configurable'
10
+
11
+ module NotionToHtml
12
+ extend Dry::Configurable
13
+
14
+ # @!attribute [rw] notion_api_token
15
+ # @return [String] The API token used to authenticate requests to the Notion API.
16
+ setting :notion_api_token
17
+
18
+ # @!attribute [rw] notion_database_id
19
+ # @return [String] The database ID in Notion that the module will interact with.
20
+ setting :notion_database_id
21
+
22
+ # @!attribute [rw] cache_store
23
+ # @return [ActiveSupport::Cache::Store] The cache store used to cache responses from the Notion API.
24
+ # @default ActiveSupport::Cache::MemoryStore.new
25
+ setting :cache_store, default: ActiveSupport::Cache::MemoryStore.new
26
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: notion_to_html
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Guillermo Aguirre
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: actionview
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 7.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '7'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 7.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '7'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 7.0.0
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '7'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 7.0.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: dry-configurable
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.2'
60
+ type: :runtime
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '1.2'
67
+ - !ruby/object:Gem::Dependency
68
+ name: notion-ruby-client
69
+ requirement: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: 1.2.2
74
+ type: :runtime
75
+ prerelease: false
76
+ version_requirements: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 1.2.2
81
+ description: Simple gem to render Notion blocks as HTML using Ruby
82
+ email:
83
+ - guillermoaguirre@hey.com
84
+ executables: []
85
+ extensions: []
86
+ extra_rdoc_files: []
87
+ files:
88
+ - LICENSE.txt
89
+ - README.md
90
+ - lib/notion_to_html.rb
91
+ - lib/notion_to_html/base_block.rb
92
+ - lib/notion_to_html/base_page.rb
93
+ - lib/notion_to_html/page.rb
94
+ - lib/notion_to_html/renderers.rb
95
+ - lib/notion_to_html/service.rb
96
+ - lib/notion_to_html/version.rb
97
+ homepage: https://github.com/guillermoap/notion_to_html
98
+ licenses:
99
+ - MIT
100
+ metadata:
101
+ homepage_uri: https://github.com/guillermoap/notion_to_html
102
+ source_code_uri: https://github.com/guillermoap/notion_to_html
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '3.1'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ requirements: []
118
+ rubygems_version: 3.5.11
119
+ signing_key:
120
+ specification_version: 4
121
+ summary: Notion HTML renderer for Ruby
122
+ test_files: []