notion_to_html 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []