notion_to_md 1.2.2 → 2.1.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: 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