notion_to_md 1.2.0 → 2.0.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: 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