notion_to_md 1.2.0 → 2.0.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: 4e623e752d6bbe53643139220d1f47f17ca411298a985bb2889a69d6dc9070d3
4
- data.tar.gz: 03631f5fb4b9d8a5e3faa4cce66d14bd89e65bc42bf62b22b90f267456b40f14
3
+ metadata.gz: 7b301a14c22e1790633fc88c179d3882521f9c9632655b6fd9fc883cf128e9dc
4
+ data.tar.gz: 528414ba605c3e03bb3599ea70b7e998768cba3ba56429aaa23523f203b8de19
5
5
  SHA512:
6
- metadata.gz: cd44a229e6e53a802008bde4e7791087c9fe92172d7ce30f575e1cc31d3a268c1f2b17ffb2502a2436ec815eea5e55bfecebb8dcf25dc9d03d783818a899e411
7
- data.tar.gz: a7e89786e1e35a23d946c5ae877051259f9496997fd6ea8fe8b1bf32b6cefe0ce10e197c38d5dabc21951d6b3a54b249eeac63c9223c484939290dcde950736c
6
+ metadata.gz: df30fc42ab07586e39a73f6ca8ddd6742e8672c5701c78c67eb4059515403090f71466fe550c3f54617edd110b395843a366f647492da687a43cc3aedf61b27c
7
+ data.tar.gz: c102997be2946fab86cd2ba837119ec34cb51da23e9104662f8cb4006930ad9aa916067fa02a26c9de066c7ceb34dc33878c01e64f800c0160aeffb172efe5f2
data/README.md CHANGED
@@ -57,6 +57,8 @@ Everything in a notion page body is a [block object](https://developers.notion.c
57
57
  * `quote`
58
58
  * `divider`
59
59
 
60
+ **Note: children nested blocks are not supported.**
61
+
60
62
  ## Front matter
61
63
 
62
64
  From version 0.2.0, notion_to_md supports front matter in markdown files.
@@ -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,126 @@
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
+ private
88
+
89
+ def convert_text(block)
90
+ block[:rich_text].map do |text|
91
+ content = Text.send(text[:type], text)
92
+ enrich_text_content(text, content)
93
+ end.join
94
+ end
95
+
96
+ def convert_caption(block)
97
+ convert_text(rich_text: block[:caption])
98
+ end
99
+
100
+ def get_icon(block)
101
+ type = block[:type].to_sym
102
+ block[type]
103
+ end
104
+
105
+ def enrich_text_content(text, content)
106
+ enriched_content = add_link(text, content)
107
+ add_annotations(text, enriched_content)
108
+ end
109
+
110
+ def add_link(text, content)
111
+ href = text[:href]
112
+ return content if href.nil?
113
+
114
+ "[#{content}](#{href})"
115
+ end
116
+
117
+ def add_annotations(text, content)
118
+ annotations = text[:annotations].select { |_key, value| !!value }
119
+ annotations.keys.inject(content) do |enriched_content, annotation|
120
+ TextAnnotation.send(annotation.to_sym, enriched_content)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './blocks/block'
4
+ require_relative './blocks/types'
5
+
6
+ module NotionToMd
7
+ module Blocks
8
+ ##
9
+ # Array containing the block types allowed to have nested blocks (children).
10
+ PERMITTED_CHILDREN = [
11
+ Types.method(:bulleted_list_item).name,
12
+ Types.method(:numbered_list_item).name
13
+ ].freeze
14
+
15
+ # === Parameters
16
+ # block::
17
+ # A {Notion::Messages::Message}[https://github.com/orbit-love/notion-ruby-client/blob/main/lib/notion/messages/message.rb] object.
18
+ #
19
+ # === Returns
20
+ # A boolean indicating if the blocked passed in
21
+ # is permitted to have children based on its type.
22
+ #
23
+ def self.permitted_children?(block:)
24
+ PERMITTED_CHILDREN.include?(block.type.to_sym) && block.has_children
25
+ end
26
+
27
+ # === Parameters
28
+ # block_id::
29
+ # A string representing a notion block id .
30
+ #
31
+ # === Returns
32
+ # An array of NotionToMd::Blocks::Block.
33
+ #
34
+ def self.build(block_id:, &fetch_blocks)
35
+ blocks = fetch_blocks.call(block_id)
36
+ blocks.results.map do |block|
37
+ children = if permitted_children?(block: block)
38
+ build(block_id: block.id, &fetch_blocks)
39
+ else
40
+ []
41
+ end
42
+ Blocks::Block.new(block: block, children: children)
43
+ end
44
+ end
45
+ end
46
+ 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
@@ -20,7 +20,7 @@ module NotionToMd
20
20
  end
21
21
 
22
22
  def icon
23
- page.dig(:icon, :emoji)
23
+ page.dig(:icon, :file, :url) || page.dig(:icon, :emoji)
24
24
  end
25
25
 
26
26
  def id
@@ -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.0'
4
+ VERSION = '2.0.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.0
4
+ version: 2.0.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-04-04 00:00:00.000000000 Z
11
+ date: 2022-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
83
97
  description: Notion Markdown Exporter in Ruby
84
98
  email: emoriarty81@gmail.com
85
99
  executables: []
@@ -88,7 +102,9 @@ extra_rdoc_files: []
88
102
  files:
89
103
  - README.md
90
104
  - lib/notion_to_md.rb
91
- - 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/types.rb
92
108
  - lib/notion_to_md/converter.rb
93
109
  - lib/notion_to_md/logger.rb
94
110
  - lib/notion_to_md/page.rb
@@ -1,118 +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}\n#{text}\n```"
53
- end
54
-
55
- def embed(block)
56
- url = block[:url]
57
-
58
- "[#{url}](#{url})"
59
- end
60
-
61
- def image(block)
62
- type = block[:type].to_sym
63
- url = block.dig(type, :url)
64
- caption = convert_caption(block)
65
-
66
- "![](#{url})\n\n#{caption}"
67
- end
68
-
69
- def bookmark(block)
70
- url = block[:url]
71
- "[#{url}](#{url})"
72
- end
73
-
74
- def divider(_block)
75
- '---'
76
- end
77
-
78
- def blank
79
- '<br />'
80
- end
81
-
82
- def convert_text(block)
83
- block[:rich_text].map do |text|
84
- content = Text.send(text[:type], text)
85
- enrich_text_content(text, content)
86
- end.join
87
- end
88
-
89
- def convert_caption(block)
90
- convert_text(rich_text: block[:caption])
91
- end
92
-
93
- def get_icon(block)
94
- type = block[:type].to_sym
95
- block[type]
96
- end
97
-
98
- def enrich_text_content(text, content)
99
- enriched_content = add_link(text, content)
100
- add_annotations(text, enriched_content)
101
- end
102
-
103
- def add_link(text, content)
104
- href = text[:href]
105
- return content if href.nil?
106
-
107
- "[#{content}](#{href})"
108
- end
109
-
110
- def add_annotations(text, content)
111
- annotations = text[:annotations].select { |_key, value| !!value }
112
- annotations.keys.inject(content) do |enriched_content, annotation|
113
- TextAnnotation.send(annotation.to_sym, enriched_content)
114
- end
115
- end
116
- end
117
- end
118
- end