notion_rails 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd982bd457f6bd05562e1e957c146e7680f51665498ae33b5127c68a3a34f157
4
- data.tar.gz: 79a47c55cd34192ec52def80c4bc2a3f81f65476087b1941344cb2075f9072a1
3
+ metadata.gz: 95a025ff228890fafa2fd982d108512fa0980fbae091232c6fbfac6ae85667b8
4
+ data.tar.gz: 8bc99ae3b897b5116ed5e4fe91847a8160db7436490f76360d637c10128df487
5
5
  SHA512:
6
- metadata.gz: e894d60b980e651fd7c9a91ea48c03852d2f4859a88ea058b5e8926f1fd3b7286f17147db8dcde5057d655af22684cb7ed9bcbcc9edf281765305b8d2f3f0afd
7
- data.tar.gz: 22b978b22ec0d744c99a4323528e8d908526f6213dbf9338869945af25fc8b1c1a9eee22189f7715fbc2e2819dff1c51d12870e2128e90431a15f1b2cef95454
6
+ metadata.gz: bdd4412954a606cccf29b805a4db0c9f4ffcec32b9d1baf64feb7a7724fe48a2a72974e1e5650254bc86d0b5eb467961492c5e49284d1d3f9f7a62f31d072b0a
7
+ data.tar.gz: 426dee990473e7a39722e81e32f282cd3f3909156884e2a9dd46f64fff0ef4893a235ab8eff6921621603c445a6117932116961d370c423cb393fc88a145c171
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  TODO: Delete this and the text below, and describe your gem
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/notion/rails`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/notion_rails`. To experiment with that code, run `bin/console` for an interactive prompt.
6
6
 
7
7
  ## Installation
8
8
 
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionRails
4
+ class BaseBlock
5
+ include NotionRails::Renderers
6
+
7
+ # TODO: validate object type is block
8
+
9
+ attr_reader :id,
10
+ :created_time,
11
+ :last_edited_time,
12
+ :created_by,
13
+ :last_edited_by,
14
+ :parent,
15
+ :archived,
16
+ :has_children,
17
+ :children,
18
+ :siblings,
19
+ :type,
20
+ :properties
21
+
22
+ attr_accessor :children, :siblings
23
+
24
+ BLOCK_TYPES = %w[
25
+ paragraph
26
+ heading_1
27
+ heading_2
28
+ heading_3
29
+ bulleted_list_item
30
+ numbered_list_item
31
+ quote
32
+ callout
33
+ code
34
+ image
35
+ video
36
+ table_of_contents
37
+ ].freeze
38
+
39
+ def initialize(data)
40
+ @id = data['id']
41
+ @created_time = data['created_time']
42
+ @last_edited_time = data['last_edited_time']
43
+ # TODO: handle user object
44
+ @created_by = data['created_by']
45
+ @last_edited_by = data['last_edited_by']
46
+ # TODO: handle page_id type
47
+ @parent = data['parent']
48
+ @archived = data['archived']
49
+ @has_children = data['has_children']
50
+ @children = []
51
+ @siblings = []
52
+ @type = data['type']
53
+ @properties = data[@type]
54
+ end
55
+
56
+ def render(options = {})
57
+ case @type
58
+ when 'paragraph' then render_paragraph(rich_text, class: options[:paragraph])
59
+ when 'heading_1' then render_heading_1(rich_text, class: options[:heading_1])
60
+ when 'heading_2' then render_heading_2(rich_text, class: options[:heading_2])
61
+ when 'heading_3' then render_heading_3(rich_text, class: options[:heading_3])
62
+ when 'table_of_contents' then render_table_of_contents
63
+ when 'bulleted_list_item'
64
+ render_bulleted_list_item(rich_text, @siblings, @children, class: options[:bulleted_list_item])
65
+ when 'numbered_list_item'
66
+ render_numbered_list_item(rich_text, @siblings, @children, class: options[:numbered_list_item])
67
+ when 'quote' then render_quote(rich_text, class: options[:quote])
68
+ when 'callout' then render_callout(rich_text, icon, class: options[:callout])
69
+ when 'code' then render_code(rich_text, class: "#{options[:code]} language-#{@properties["language"]}")
70
+ when 'image' then render_image(*multi_media)
71
+ when 'video' then render_video(*multi_media)
72
+ else
73
+ 'Error'
74
+ end
75
+ end
76
+
77
+ def rich_text
78
+ @properties['rich_text'] || []
79
+ end
80
+
81
+ def icon
82
+ icon = @properties['icon']
83
+ @properties['icon'][icon['type']] || []
84
+ end
85
+
86
+ def multi_media
87
+ case @properties['type']
88
+ when 'file'
89
+ [@properties.dig('file', 'url'), @properties.dig('file', 'expiry_time'), @properties['caption'], 'file']
90
+ when 'external'
91
+ [@properties.dig('external', 'url'), nil, @properties['caption'], 'external']
92
+ else
93
+ [nil, nil, @properties['caption'], nil]
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def render_table_of_contents; end
100
+ end
101
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionRails
4
+ class BasePage
5
+ include NotionRails::Renderers
6
+
7
+ # TODO: validate object type is page
8
+ attr_reader :id,
9
+ :created_time,
10
+ :last_edited_time,
11
+ :created_by,
12
+ :last_edited_by,
13
+ :cover,
14
+ :icon,
15
+ :parent,
16
+ :archived,
17
+ :properties,
18
+ :published_at,
19
+ :tags,
20
+ :title,
21
+ :slug,
22
+ :description,
23
+ :url
24
+
25
+ def initialize(data)
26
+ @id = data['id']
27
+ @created_time = data['created_time']
28
+ @last_edited_time = data['last_edited_time']
29
+ # TODO: handle user object
30
+ @created_by = data['created_by']
31
+ @last_edited_by = data['last_edited_by']
32
+ # TODO: handle external type
33
+ @cover = data['cover']
34
+ # TODO: handle emoji type
35
+ @icon = data['icon']
36
+ # TODO: handle database_id type
37
+ @parent = data['parent']
38
+ @archived = data['archived']
39
+ # TODO: handle properties object
40
+ @properties = data['properties']
41
+ process_properties
42
+ @url = data['url']
43
+ end
44
+
45
+ def formatted_title(options = {})
46
+ render_title(@title, options)
47
+ end
48
+
49
+ def formatted_description(options = {})
50
+ render_paragraph(@description, options)
51
+ end
52
+
53
+ def formatted_published_at(options = {})
54
+ render_date(@published_at, options)
55
+ end
56
+
57
+ private
58
+
59
+ def process_properties
60
+ @tags = @properties['tags']
61
+ @title = @properties.dig('name', 'title')
62
+ @slug = @properties['slug']
63
+ @published_at = @properties.dig('published', 'date', 'start')
64
+ @description = @properties.dig('description', 'rich_text')
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionRails
4
+ class Page
5
+ include NotionRails::Renderers
6
+
7
+ attr_reader :metadata, :blocks
8
+
9
+ delegate :formatted_title, to: :metadata
10
+ delegate :formatted_description, to: :metadata
11
+ delegate :formatted_published_at, to: :metadata
12
+
13
+ def initialize(base_page, base_blocks)
14
+ @metadata = base_page
15
+ @blocks = base_blocks
16
+ end
17
+
18
+ def formatted_blocks(options = {})
19
+ @blocks.map { |block| block.render(options) }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_view'
4
+
5
+ module NotionRails
6
+ module Renderers
7
+ include ActionView::Helpers::AssetTagHelper
8
+ include ActionView::Helpers::TagHelper
9
+ include ActionView::Helpers::UrlHelper
10
+ include ActionView::Context
11
+
12
+ def annotation_to_css_class(annotations)
13
+ classes = annotations.keys.map do |key|
14
+ case key
15
+ when 'strikethrough'
16
+ 'line-through' if annotations[key]
17
+ when 'bold'
18
+ 'font-bold' if annotations[key]
19
+ when 'code'
20
+ 'inline-code' if annotations[key]
21
+ when 'color'
22
+ "text-#{annotations["color"]}-600" if annotations[key] != 'default'
23
+ else
24
+ annotations[key] ? key : nil
25
+ end
26
+ end
27
+ classes.compact.join(' ')
28
+ end
29
+
30
+ def text_renderer(properties, options = {})
31
+ properties.map do |rich_text|
32
+ classes = annotation_to_css_class(rich_text['annotations'])
33
+ if rich_text['href']
34
+ link_to(
35
+ rich_text['plain_text'],
36
+ rich_text['href'],
37
+ class: "link #{classes} #{options[:class]}"
38
+ )
39
+ elsif classes.present?
40
+ content_tag(:span, rich_text['plain_text'], class: "#{classes} #{options[:class]}")
41
+ else
42
+ tag.span(rich_text['plain_text'], class: options[:class])
43
+ end
44
+ end.join('').html_safe
45
+ end
46
+
47
+ def render_title(title, options = {})
48
+ render_heading_1(title, options)
49
+ end
50
+
51
+ def render_date(date, options = {})
52
+ # TODO: handle end and time zone
53
+ # date=end=, start=2023-07-13, time_zone=, id=%5BsvU, type=date
54
+ tag.p(date.to_date.to_fs(:long), class: options[:class])
55
+ end
56
+
57
+ def render_paragraph(rich_text_array, options = {})
58
+ content_tag(:p, options) do
59
+ text_renderer(rich_text_array)
60
+ end
61
+ end
62
+
63
+ def render_heading_1(rich_text_array, options = {})
64
+ content_tag(:h1, class: 'mb-4 mt-6 text-3xl font-semibold', **options) do
65
+ text_renderer(rich_text_array)
66
+ end
67
+ end
68
+
69
+ def render_heading_2(rich_text_array, options = {})
70
+ content_tag(:h2, class: 'mb-4 mt-6 text-2xl font-semibold', **options) do
71
+ text_renderer(rich_text_array)
72
+ end
73
+ end
74
+
75
+ def render_heading_3(rich_text_array, options = {})
76
+ content_tag(:h3, class: 'mb-2 mt-6 text-xl font-semibold', **options) do
77
+ text_renderer(rich_text_array)
78
+ end
79
+ end
80
+
81
+ def render_code(rich_text_array, options = {})
82
+ # TODO: render captions
83
+ pre_options = options
84
+ pre_options[:class] = "border-2 p-6 rounded #{pre_options[:class]}"
85
+ content_tag(:div, class: 'mt-4', data: { controller: 'highlight' }) do
86
+ content_tag(:div, data: { highlight_target: 'source' }) do
87
+ content_tag(:pre, pre_options) do
88
+ text_renderer(rich_text_array, options)
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ def render_bulleted_list_item(rich_text_array, siblings, children, options = {})
95
+ pre_options = options
96
+ pre_options[:class] = "list-disc break-words #{pre_options[:class]}"
97
+ content_tag(:ul, pre_options) do
98
+ content = content_tag(:li, options) do
99
+ text_renderer(rich_text_array)
100
+ end
101
+ if children.present?
102
+ res = children.map do |child|
103
+ render_bulleted_list_item(child.rich_text, child.siblings, child.children, options)
104
+ end
105
+ content += res.join('').html_safe
106
+ end
107
+ content.html_safe
108
+ end
109
+ end
110
+
111
+ def render_numbered_list_item(rich_text_array, siblings, children, options = {})
112
+ pre_options = options
113
+ pre_options[:class] = "list-decimal #{pre_options[:class]}"
114
+ content_tag(:ol, pre_options) do
115
+ render_list_items(:numbered_list_item, rich_text_array, siblings, children, options)
116
+ end
117
+ end
118
+
119
+ def render_list_items(type, rich_text_array, siblings, children, options = {})
120
+ content = content_tag(:li, options) do
121
+ text_renderer(rich_text_array)
122
+ end
123
+ if children.present?
124
+ res = children.map do |child|
125
+ render_numbered_list_item(child.rich_text, child.siblings, child.children)
126
+ end
127
+ content += res.join('').html_safe
128
+ end
129
+ if siblings.present?
130
+ content += siblings.map do |sibling|
131
+ render_list_items(type, sibling.rich_text, sibling.siblings, sibling.children, options)
132
+ end.join('').html_safe
133
+ end
134
+ content.html_safe
135
+ end
136
+
137
+ def render_quote(rich_text_array, options = {})
138
+ div_options = options.dup
139
+ pre_options = options.dup
140
+ div_options[:class] = "mt-4 #{options[:class]}"
141
+ content_tag(:div, div_options) do
142
+ pre_options[:class] = "border-l-4 border-black px-5 py-1 #{options[:class]}"
143
+ content_tag(:cite) do
144
+ content_tag(:p, pre_options) do
145
+ text_renderer(rich_text_array)
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ def render_callout(rich_text_array, icon, options = {})
152
+ pre_options = options
153
+ pre_options[:class] = "p-4 rounded bg-neutral-200 mt-4 #{pre_options[:class]}"
154
+ content_tag(:div, pre_options) do
155
+ content = tag.span(icon, class: 'pr-2')
156
+ content += text_renderer(rich_text_array)
157
+ content
158
+ end
159
+ end
160
+
161
+ def render_image(src, expiry_time, caption, type, options = {})
162
+ content_tag(:figure, options) do
163
+ content = tag.img(src: src, alt: '')
164
+ content += tag.figcaption(text_renderer(caption))
165
+ content
166
+ end
167
+ end
168
+
169
+ def render_video(src, expiry_time, caption, type, options = {})
170
+ content_tag(:figure, options) do
171
+ content = if type == 'file'
172
+ video_tag(src, controls: true)
173
+ elsif type == 'external'
174
+ tag.iframe(src: src, allowfullscreen: true, class: 'w-full aspect-video')
175
+ end
176
+ content += tag.figcaption(text_renderer(caption))
177
+ content
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionRails
4
+ class Service
5
+ def initialize
6
+ @client = Notion::Client.new(token: NotionRails.config.notion_api_token)
7
+ end
8
+
9
+ def default_query(tag: nil, slug: nil)
10
+ query = [
11
+ {
12
+ property: 'public',
13
+ checkbox: {
14
+ equals: true
15
+ }
16
+ }
17
+ ]
18
+
19
+ if slug
20
+ query.push({
21
+ property: 'slug',
22
+ rich_text: {
23
+ equals: slug
24
+ }
25
+ })
26
+ end
27
+
28
+ if tag
29
+ query.push({
30
+ property: 'tags',
31
+ multi_select: {
32
+ contains: tag
33
+ }
34
+ })
35
+ end
36
+
37
+ query
38
+ end
39
+
40
+ def default_sorting
41
+ {
42
+ property: 'published',
43
+ direction: 'descending'
44
+ }
45
+ end
46
+
47
+ def get_articles(tag: nil, slug: nil, page_size: 10)
48
+ __get_articles(tag: tag, slug: slug, page_size: page_size)['results'].map do |page|
49
+ NotionRails::BasePage.new(page)
50
+ end
51
+ end
52
+
53
+ def get_article(id)
54
+ base_page = NotionRails::BasePage.new(__get_page(id))
55
+ base_blocks = NotionRails.config.cache_store.fetch(id) { get_blocks(id) }
56
+ NotionRails::Page.new(base_page, base_blocks)
57
+ end
58
+
59
+ def get_blocks(id)
60
+ blocks = __get_blocks(id)
61
+ parent_list_block_index = nil
62
+ results = []
63
+ blocks['results'].each_with_index do |block, index|
64
+ base_block = NotionRails::BaseBlock.new(block)
65
+ base_block.children = get_blocks(base_block.id) if base_block.has_children
66
+ # Notion returns same list items as different blocks so we have to do some processing to have them be related
67
+ # TODO: Separate this into a function, add support for bulleted items.
68
+ # Currently bulleted items render fine, but they do it in separate ul blocks
69
+ # Make them appear in the same ul block as numbered_items appear in the same ol block
70
+ if %w[numbered_list_item].include? base_block.type
71
+ siblings = !parent_list_block_index.nil? &&
72
+ index != parent_list_block_index &&
73
+ base_block.type == results[parent_list_block_index]&.type &&
74
+ base_block.parent == results[parent_list_block_index]&.parent
75
+ if siblings
76
+ results[parent_list_block_index].siblings << base_block
77
+ next
78
+ else
79
+ parent_list_block_index = results.length
80
+ end
81
+ else
82
+ parent_list_block_index = nil
83
+ end
84
+ results << base_block
85
+ end
86
+ results
87
+ end
88
+
89
+ private
90
+
91
+ def __get_articles(tag: nil, slug: nil, page_size: 10)
92
+ @client.database_query(
93
+ database_id: NotionRails.config.notion_database_id,
94
+ sorts: [
95
+ default_sorting
96
+ ],
97
+ filter: {
98
+ 'and': default_query(tag: tag, slug: slug)
99
+ },
100
+ page_size: page_size
101
+ )
102
+ end
103
+
104
+ def __get_page(id)
105
+ @client.page(page_id: id)
106
+ end
107
+
108
+ def __get_blocks(id)
109
+ @client.block_children(block_id: id)
110
+ end
111
+ end
112
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionRails
4
- VERSION = "0.1.0"
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/notion_rails.rb CHANGED
@@ -1,3 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "notion_rails/version"
3
+ require 'notion_rails/renderers'
4
+ require 'notion_rails/base_block'
5
+ require 'notion_rails/base_page'
6
+ require 'notion_rails/page'
7
+ require 'notion_rails/service'
8
+ require 'dry-configurable'
9
+
10
+ module NotionRails
11
+ extend Dry::Configurable
12
+
13
+ setting :notion_api_token
14
+ setting :notion_database_id
15
+ setting :cache_store, default: ActiveSupport::Cache::MemoryStore.new
16
+ end
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notion_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Guillermo Aguirre
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-09 00:00:00.000000000 Z
11
+ date: 2024-08-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rails
14
+ name: actionview
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
@@ -24,9 +24,51 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 7.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 7.0.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 7.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-configurable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: notion-ruby-client
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 1.2.2
27
69
  description: Simple gem to render Notion blocks as HTML using Rails
28
70
  email:
29
- - guillermoaguirre1@gmail.com
71
+ - guillermoaguirre@hey.com
30
72
  executables: []
31
73
  extensions: []
32
74
  extra_rdoc_files: []
@@ -34,6 +76,11 @@ files:
34
76
  - LICENSE.txt
35
77
  - README.md
36
78
  - lib/notion_rails.rb
79
+ - lib/notion_rails/base_block.rb
80
+ - lib/notion_rails/base_page.rb
81
+ - lib/notion_rails/page.rb
82
+ - lib/notion_rails/renderers.rb
83
+ - lib/notion_rails/service.rb
37
84
  - lib/notion_rails/version.rb
38
85
  homepage: https://github.com/guillermoap/notion-rails
39
86
  licenses:
@@ -41,7 +88,7 @@ licenses:
41
88
  metadata:
42
89
  homepage_uri: https://github.com/guillermoap/notion-rails
43
90
  source_code_uri: https://github.com/guillermoap/notion-rails
44
- post_install_message:
91
+ post_install_message:
45
92
  rdoc_options: []
46
93
  require_paths:
47
94
  - lib
@@ -49,15 +96,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
49
96
  requirements:
50
97
  - - ">="
51
98
  - !ruby/object:Gem::Version
52
- version: 2.6.0
99
+ version: '3.0'
53
100
  required_rubygems_version: !ruby/object:Gem::Requirement
54
101
  requirements:
55
102
  - - ">="
56
103
  - !ruby/object:Gem::Version
57
104
  version: '0'
58
105
  requirements: []
59
- rubygems_version: 3.4.10
60
- signing_key:
106
+ rubygems_version: 3.5.11
107
+ signing_key:
61
108
  specification_version: 4
62
109
  summary: Notion HTML renderer for Rails
63
110
  test_files: []