notion_to_md 1.2.2 → 2.1.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: 4876bb92641f8a6199b932404c609cf99a69bafd2f39eaded145f638edd98c39
4
- data.tar.gz: f57fa933a7bfa0fc4e1b916cf022b64255da39142a2c546720e08d72b6915b44
3
+ metadata.gz: a6cff35e8c8b88f99117499abcc24c89fc98ec723aa4d3380b7b36699e8886e3
4
+ data.tar.gz: 280543cfe9aae2d20d74336f992c26ab5e83dabe738a6f1b6f5bf9742fb41e36
5
5
  SHA512:
6
- metadata.gz: 86b7daa8a26780c4d6a091048144e05f9a5b48f28993d5a1fed8b53f10e844d3cb54d9b39bb4ffacb35945a1ef9e0128398dac36e68246476f8e3d3919189b87
7
- data.tar.gz: 3689b48b6fd3a550be20091601a0b888d5656f51d03e0141b37c391934184a5b3e48456996d5bd42b0ed3188204a3be15292a354eaa28f6c4a61fac2de277a32
6
+ metadata.gz: 8808bdd505c6df869f5c06f71d1d763e6f008bc68379528af997b546ff0c6ba1312107b328c3b60b4708949e49a8e03ae8a764acc1e3789fdaf7a5e68b7da2d4
7
+ data.tar.gz: 5bb5daa841ada70a0ded345fdbf3bbda55aac0e078413f6723ced019029f6ea9a990625f0ee22533766f54b7453e77c40a2b6e59e622dc12f9a3180366fa71bc
data/README.md CHANGED
@@ -56,6 +56,11 @@ Everything in a notion page body is a [block object](https://developers.notion.c
56
56
  * `callout`
57
57
  * `quote`
58
58
  * `divider`
59
+ * `tables`
60
+
61
+ ### Nested blocks
62
+
63
+ Starting with v2, nested blocks are supported. For now, only lists are supported, but more elements will be added.
59
64
 
60
65
  ## Front matter
61
66
 
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module NotionToMd
6
+ module Blocks
7
+ class Block
8
+ extend Forwardable
9
+
10
+ attr_reader :block, :children
11
+
12
+ # === Parameters:
13
+ # block::
14
+ # A {Notion::Messages::Message}[https://github.com/orbit-love/notion-ruby-client/blob/main/lib/notion/messages/message.rb] object.
15
+ # children::
16
+ # An array of NotionToMd::Block::Block objects.
17
+ #
18
+ # === Returns
19
+ # A Block object.
20
+ #
21
+ def initialize(block:, children: [])
22
+ @block = block
23
+ @children = children
24
+ end
25
+
26
+ # === Parameters:
27
+ # tab_width::
28
+ # The number of tabs used to indent the block.
29
+ #
30
+ # === Returns
31
+ # The current block (and its children) converted to a markdown string.
32
+ #
33
+ def to_md(tab_width: 0)
34
+ block_type = block.type.to_sym
35
+ md = Types.send(block_type, block[block_type])
36
+ md + build_nested_blocks(tab_width + 1)
37
+ rescue NoMethodError
38
+ Logger.info("Unsupported block type: #{block_type}")
39
+ nil
40
+ end
41
+
42
+ private
43
+
44
+ def build_nested_blocks(tab_width)
45
+ mds = markdownify_children(tab_width).compact
46
+ indent_children(mds, tab_width).join
47
+ end
48
+
49
+ def markdownify_children(tab_width)
50
+ children.map do |nested_block|
51
+ nested_block.to_md(tab_width: tab_width)
52
+ end
53
+ end
54
+
55
+ def indent_children(mds, tab_width)
56
+ mds.map do |md|
57
+ "\n\n#{"\t" * tab_width}#{md}"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionToMd
4
+ module Blocks
5
+ class Factory
6
+ def self.build(block:, children: [])
7
+ case block.type.to_sym
8
+ when :table
9
+ TableBlock.new(block: block, children: children)
10
+ else
11
+ Blocks::Block.new(block: block, children: children)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionToMd
4
+ module Blocks
5
+ class TableBlock < Block
6
+ def to_md
7
+ table = markdownify_children(0)
8
+
9
+ table_header = table[0]
10
+ table_aligment = markdownify_aligment
11
+ table_body = table[1..table.size]
12
+
13
+ [table_header, table_aligment, table_body.join("\n")].compact.join("\n")
14
+ end
15
+
16
+ private
17
+
18
+ def row_size
19
+ @row_size ||= children.first.block.table_row.cells.size
20
+ end
21
+
22
+ def markdownify_aligment
23
+ "|#{row_size.times.map { '---' }.join('|')}|" if block.table.has_column_header
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionToMd
4
+ module Blocks
5
+ class Types
6
+ class << self
7
+ def paragraph(block)
8
+ return blank if block.rich_text.empty?
9
+
10
+ convert_text(block)
11
+ end
12
+
13
+ def heading_1(block)
14
+ "# #{convert_text(block)}"
15
+ end
16
+
17
+ def heading_2(block)
18
+ "## #{convert_text(block)}"
19
+ end
20
+
21
+ def heading_3(block)
22
+ "### #{convert_text(block)}"
23
+ end
24
+
25
+ def callout(block)
26
+ icon = get_icon(block[:icon])
27
+ text = convert_text(block)
28
+ "#{icon} #{text}"
29
+ end
30
+
31
+ def quote(block)
32
+ "> #{convert_text(block)}"
33
+ end
34
+
35
+ def bulleted_list_item(block)
36
+ "- #{convert_text(block)}"
37
+ end
38
+
39
+ def numbered_list_item(block)
40
+ Logger.info('numbered_list_item type not supported. Shown as bulleted_list_item.')
41
+ bulleted_list_item(block)
42
+ end
43
+
44
+ def to_do(block)
45
+ checked = block[:checked]
46
+ text = convert_text(block)
47
+
48
+ "- #{checked ? '[x]' : '[ ]'} #{text}"
49
+ end
50
+
51
+ def code(block)
52
+ language = block[:language]
53
+ text = convert_text(block)
54
+
55
+ language = 'text' if language == 'plain text'
56
+
57
+ "```#{language}\n#{text}\n```"
58
+ end
59
+
60
+ def embed(block)
61
+ url = block[:url]
62
+
63
+ "[#{url}](#{url})"
64
+ end
65
+
66
+ def image(block)
67
+ type = block[:type].to_sym
68
+ url = block.dig(type, :url)
69
+ caption = convert_caption(block)
70
+
71
+ "![](#{url})\n\n#{caption}"
72
+ end
73
+
74
+ def bookmark(block)
75
+ url = block[:url]
76
+ "[#{url}](#{url})"
77
+ end
78
+
79
+ def divider(_block)
80
+ '---'
81
+ end
82
+
83
+ def blank
84
+ '<br />'
85
+ end
86
+
87
+ def table_row(block)
88
+ "|#{block[:cells].map(&method(:convert_table_row)).join('|')}|"
89
+ end
90
+
91
+ private
92
+
93
+ def convert_table_row(cells)
94
+ cells.map(&method(:convert_table_cell))
95
+ end
96
+
97
+ def convert_table_cell(text)
98
+ convert_text({ rich_text: [text] })
99
+ end
100
+
101
+ def convert_text(block)
102
+ block[:rich_text].map do |text|
103
+ content = Text.send(text[:type], text)
104
+ enrich_text_content(text, content)
105
+ end.join
106
+ end
107
+
108
+ def convert_caption(block)
109
+ convert_text(rich_text: block[:caption])
110
+ end
111
+
112
+ def get_icon(block)
113
+ type = block[:type].to_sym
114
+ block[type]
115
+ end
116
+
117
+ def enrich_text_content(text, content)
118
+ enriched_content = add_link(text, content)
119
+ add_annotations(text, enriched_content)
120
+ end
121
+
122
+ def add_link(text, content)
123
+ href = text[:href]
124
+ return content if href.nil?
125
+
126
+ "[#{content}](#{href})"
127
+ end
128
+
129
+ def add_annotations(text, content)
130
+ annotations = text[:annotations].select { |_key, value| !!value }
131
+ annotations.keys.inject(content) do |enriched_content, annotation|
132
+ TextAnnotation.send(annotation.to_sym, enriched_content)
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './blocks/block'
4
+ require_relative './blocks/factory'
5
+ require_relative './blocks/table_block'
6
+ require_relative './blocks/types'
7
+
8
+ module NotionToMd
9
+ module Blocks
10
+ ##
11
+ # Array containing the block types allowed to have nested blocks (children).
12
+ PERMITTED_CHILDREN = [
13
+ Types.method(:bulleted_list_item).name,
14
+ Types.method(:numbered_list_item).name,
15
+ :table
16
+ ].freeze
17
+
18
+ # === Parameters
19
+ # block::
20
+ # A {Notion::Messages::Message}[https://github.com/orbit-love/notion-ruby-client/blob/main/lib/notion/messages/message.rb] object.
21
+ #
22
+ # === Returns
23
+ # A boolean indicating if the blocked passed in
24
+ # is permitted to have children based on its type.
25
+ #
26
+ def self.permitted_children?(block:)
27
+ PERMITTED_CHILDREN.include?(block.type.to_sym) && block.has_children
28
+ end
29
+
30
+ # === Parameters
31
+ # block_id::
32
+ # A string representing a notion block id .
33
+ #
34
+ # === Returns
35
+ # An array of NotionToMd::Blocks::Block.
36
+ #
37
+ def self.build(block_id:, &fetch_blocks)
38
+ blocks = fetch_blocks.call(block_id)
39
+ blocks.results.map do |block|
40
+ children = if permitted_children?(block: block)
41
+ build(block_id: block.id, &fetch_blocks)
42
+ else
43
+ []
44
+ end
45
+ Factory.build(block: block, children: children)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,14 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionToMd
4
+ ##
5
+ # The Converter class allows to transform notion pages to markdown documents.
6
+ # Just create a new Converter instance by providing the page_id:
7
+ # page_converter = NotionToMd::Converter.new(page_id: '9dc17c9c9d2e469dbbf0f9648f3288d3')
8
+ # Then, call for convert to obtain the markdown document:
9
+ # page_converter.convert
10
+
4
11
  class Converter
5
12
  attr_reader :page_id
6
13
 
14
+ # === Parameters
15
+ # page_id::
16
+ # A string representing the notion page id.
17
+ # token::
18
+ # The notion API secret token. The token can replaced by the environment variable NOTION_TOKEN.
19
+ #
20
+ # === Returns
21
+ # A NotionToMd::Converter object.
22
+ #
7
23
  def initialize(page_id:, token: nil)
8
24
  @notion = Notion::Client.new(token: token || ENV['NOTION_TOKEN'])
9
25
  @page_id = page_id
10
26
  end
11
27
 
28
+ # === Parameters
29
+ # frontmatter::
30
+ # A boolean value that indicates whether the front matter block is included in the markdown document.
31
+ #
32
+ # === Returns
33
+ # The string that represent the markdown document.
34
+ #
12
35
  def convert(frontmatter: false)
13
36
  md_page = Page.new(page: page, blocks: page_blocks)
14
37
  <<~MD
@@ -24,7 +47,17 @@ module NotionToMd
24
47
  end
25
48
 
26
49
  def page_blocks
27
- @page_blocks ||= @notion.block_children(block_id: page_id)
50
+ @page_blocks ||= build_blocks(block_id: page_id)
51
+ end
52
+
53
+ def build_blocks(block_id:)
54
+ Blocks.build(block_id: block_id) do |nested_block_id|
55
+ fetch_blocks(block_id: nested_block_id)
56
+ end
57
+ end
58
+
59
+ def fetch_blocks(block_id:)
60
+ @notion.block_children(block_id: block_id)
28
61
  end
29
62
  end
30
63
  end
@@ -44,18 +44,7 @@ module NotionToMd
44
44
  end
45
45
 
46
46
  def body
47
- @body ||= blocks[:results].map do |block|
48
- next Block.blank if block[:type] == 'paragraph' && block.dig(:paragraph, :rich_text).empty?
49
-
50
- block_type = block[:type].to_sym
51
-
52
- begin
53
- Block.send(block_type, block[block_type])
54
- rescue StandardError
55
- Logger.info("Unsupported block type: #{block_type}")
56
- next nil
57
- end
58
- end.compact.join("\n\n")
47
+ @body ||= blocks.map(&:to_md).compact.join("\n\n")
59
48
  end
60
49
 
61
50
  def frontmatter
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionToMd
4
+ ##
4
5
  # Append the text type:
5
6
  # * italic: boolean,
6
7
  # * bold: boolean,
@@ -8,6 +9,7 @@ module NotionToMd
8
9
  # * underline: boolean,
9
10
  # * code: boolean,
10
11
  # * color: string NOT_SUPPORTED
12
+
11
13
  class TextAnnotation
12
14
  class << self
13
15
  def italic(text)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionToMd
4
- VERSION = '1.2.2'
4
+ VERSION = '2.1.0'
5
5
  end
data/lib/notion_to_md.rb CHANGED
@@ -8,6 +8,6 @@ require_relative './notion_to_md/version'
8
8
  require_relative './notion_to_md/converter'
9
9
  require_relative './notion_to_md/logger'
10
10
  require_relative './notion_to_md/page'
11
- require_relative './notion_to_md/block'
11
+ require_relative './notion_to_md/blocks'
12
12
  require_relative './notion_to_md/text'
13
13
  require_relative './notion_to_md/text_annotation'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notion_to_md
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Enrique Arias
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-03 00:00:00.000000000 Z
11
+ date: 2022-09-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -102,7 +102,11 @@ extra_rdoc_files: []
102
102
  files:
103
103
  - README.md
104
104
  - lib/notion_to_md.rb
105
- - lib/notion_to_md/block.rb
105
+ - lib/notion_to_md/blocks.rb
106
+ - lib/notion_to_md/blocks/block.rb
107
+ - lib/notion_to_md/blocks/factory.rb
108
+ - lib/notion_to_md/blocks/table_block.rb
109
+ - lib/notion_to_md/blocks/types.rb
106
110
  - lib/notion_to_md/converter.rb
107
111
  - lib/notion_to_md/logger.rb
108
112
  - lib/notion_to_md/page.rb
@@ -1,120 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module NotionToMd
4
- class Block
5
- class << self
6
- def paragraph(block)
7
- convert_text(block)
8
- end
9
-
10
- def heading_1(block)
11
- "# #{convert_text(block)}"
12
- end
13
-
14
- def heading_2(block)
15
- "## #{convert_text(block)}"
16
- end
17
-
18
- def heading_3(block)
19
- "### #{convert_text(block)}"
20
- end
21
-
22
- def callout(block)
23
- icon = get_icon(block[:icon])
24
- text = convert_text(block)
25
- "#{icon} #{text}"
26
- end
27
-
28
- def quote(block)
29
- "> #{convert_text(block)}"
30
- end
31
-
32
- def bulleted_list_item(block)
33
- "- #{convert_text(block)}"
34
- end
35
-
36
- def numbered_list_item(block)
37
- Logger.info('numbered_list_item type not supported. Shown as bulleted_list_item.')
38
- bulleted_list_item(block)
39
- end
40
-
41
- def to_do(block)
42
- checked = block[:checked]
43
- text = convert_text(block)
44
-
45
- "- #{checked ? '[x]' : '[ ]'} #{text}"
46
- end
47
-
48
- def code(block)
49
- language = block[:language]
50
- text = convert_text(block)
51
-
52
- language = 'text' if language == 'plain text'
53
-
54
- "```#{language}\n#{text}\n```"
55
- end
56
-
57
- def embed(block)
58
- url = block[:url]
59
-
60
- "[#{url}](#{url})"
61
- end
62
-
63
- def image(block)
64
- type = block[:type].to_sym
65
- url = block.dig(type, :url)
66
- caption = convert_caption(block)
67
-
68
- "![](#{url})\n\n#{caption}"
69
- end
70
-
71
- def bookmark(block)
72
- url = block[:url]
73
- "[#{url}](#{url})"
74
- end
75
-
76
- def divider(_block)
77
- '---'
78
- end
79
-
80
- def blank
81
- '<br />'
82
- end
83
-
84
- def convert_text(block)
85
- block[:rich_text].map do |text|
86
- content = Text.send(text[:type], text)
87
- enrich_text_content(text, content)
88
- end.join
89
- end
90
-
91
- def convert_caption(block)
92
- convert_text(rich_text: block[:caption])
93
- end
94
-
95
- def get_icon(block)
96
- type = block[:type].to_sym
97
- block[type]
98
- end
99
-
100
- def enrich_text_content(text, content)
101
- enriched_content = add_link(text, content)
102
- add_annotations(text, enriched_content)
103
- end
104
-
105
- def add_link(text, content)
106
- href = text[:href]
107
- return content if href.nil?
108
-
109
- "[#{content}](#{href})"
110
- end
111
-
112
- def add_annotations(text, content)
113
- annotations = text[:annotations].select { |_key, value| !!value }
114
- annotations.keys.inject(content) do |enriched_content, annotation|
115
- TextAnnotation.send(annotation.to_sym, enriched_content)
116
- end
117
- end
118
- end
119
- end
120
- end