notion_rails 0.1.0 → 0.2.0

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