notion_to_md 0.0.1 → 0.2.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: e6f3d20369829f0cf5f2245eec32fcfc98a01b12edbf143451234922421ba2e1
4
- data.tar.gz: eb3465f0295b9514fcf120efc1330d432d378c449ac5e1e279b19f0e6b8801f4
3
+ metadata.gz: d326983d99843871779f5331bab84d734bc01c21aa90b4da0cc76f1dd092c95e
4
+ data.tar.gz: 4b6a34aebbe07df627d956e9918606385e5d3952c56522ee66f3e331f8522d24
5
5
  SHA512:
6
- metadata.gz: 927753e91887ac4d10b6a325f77f862e55676c607936c2b7a193eb6a5dd9bddf8dcc5824c7d02e714c90037a119921d114cec116e8989d1c789ff63993a2791d
7
- data.tar.gz: b3a457117754849fff34035373385811260c0bf25093208747f3dda6800a0107acf7d1ae46328bb25583428975e8155960f03cb84a53f070afc4bd15d7836949
6
+ metadata.gz: 5e4990d98adf9eaff1dafc48285430b788de0bf4088b95ecd5f6c970fc850e015e9f148c79f0603647902b96b26465b01f88e9d86133472cc154a581c6f2e907
7
+ data.tar.gz: bb63c2a623265a153e7300ca9389bf1e75773373f8221eed41e4b7a11b33f801c26d8d033f3038ac93ead0586257e7874773c9a490de436a30ea79c4e5d5ca08
data/README.md CHANGED
@@ -38,4 +38,62 @@ notion_converter = NotionToMd::Converter.new(page_id: 'b91d5...')
38
38
  md = notion_converter.convert
39
39
  ```
40
40
 
41
- And that's all. The `md` is a string variable containing the notion page formatted in markdown.
41
+ And that's all. The `md` is a string variable containing the notion page formatted in markdown.
42
+
43
+
44
+ ### Front matter
45
+
46
+ From version 0.2.0, notion_to_md supports front matter in markdown files.
47
+
48
+ By default, the front matter section is not included to the document. To do so, provide the `:frontmatter` option set to `true` to `convert` method.
49
+
50
+ ```ruby
51
+ NotionToMd::Converter.new(page_id: 'b91d5...').convert(frontmatter: tue)
52
+ ```
53
+
54
+ Default notion properties are page `id`, `title`, `created_time`, `last_edited_time`, `icon`, `archived` and `cover`.
55
+
56
+ ```yml
57
+ ---
58
+ id: e42383cd-4975-4897-b967-ce453760499f
59
+ title: An amazing post
60
+ cover: https://img.bank.sh/an_image.jpg
61
+ created_time: 2022-01-23T12:31:00.000Z
62
+ last_edited_time: 2022-01-23T12:31:00.000Z
63
+ icon: 💥
64
+ archived: false
65
+ ---
66
+ ```
67
+
68
+ In addition to default properties, custom properties are also supported.
69
+ Custom properties name is [parameterized](https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize) and [underscorized](https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-underscore) before added to front matter.
70
+ For example, two properties named `Multiple Options` and `Tags` will be transformed to `multiple_options` and `tags`, respectively.
71
+
72
+ ```yml
73
+ ---
74
+ tags: tag1, tag2, tag3
75
+ multiple_options: option1, option2
76
+ ---
77
+ ```
78
+
79
+ The supported property types are:
80
+
81
+ * `number`
82
+ * `select`
83
+ * `multi_select`
84
+ * `date`
85
+ * `people`
86
+ * `files`
87
+ * `checkbox`
88
+ * `url`
89
+ * `email`
90
+ * `phone_number`
91
+
92
+ `rich_text` as advanced types like `formula`, `relation` and `rollup` are not supported.
93
+
94
+ Check notion documentation about [property values](https://developers.notion.com/reference/property-value-object#all-property-values) to know more.
95
+
96
+ ## Test
97
+ ```bash
98
+ rspec
99
+ ```
@@ -0,0 +1,119 @@
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
+ # TODO: numbered_list_item
37
+ def numbered_list_item(block)
38
+ Logger.info('numbered_list_item type not supported. Shown as bulleted_list_item.')
39
+ bulleted_list_item(block)
40
+ end
41
+
42
+ def to_do(block)
43
+ checked = block[:checked]
44
+ text = convert_text(block)
45
+
46
+ "- #{checked ? '[x]' : '[ ]'} #{text}"
47
+ end
48
+
49
+ def code(block)
50
+ language = block[:language]
51
+ text = convert_text(block)
52
+
53
+ "```#{language}\n\t#{text}\n```"
54
+ end
55
+
56
+ def embed(block)
57
+ url = block[:url]
58
+
59
+ "[#{url}](#{url})"
60
+ end
61
+
62
+ def image(block)
63
+ type = block[:type].to_sym
64
+ url = block.dig(type, :url)
65
+ caption = convert_caption(block)
66
+
67
+ "![](#{url})\n\n#{caption}"
68
+ end
69
+
70
+ def bookmark(block)
71
+ url = block[:url]
72
+ "[#{url}](#{url})"
73
+ end
74
+
75
+ def divider(_block)
76
+ '---'
77
+ end
78
+
79
+ def blank
80
+ '<br />'
81
+ end
82
+
83
+ def convert_text(block)
84
+ block[:text].map do |text|
85
+ content = Text.send(text[:type], text)
86
+ enrich_text_content(text, content)
87
+ end.join
88
+ end
89
+
90
+ def convert_caption(block)
91
+ convert_text(text: block[:caption])
92
+ end
93
+
94
+ def get_icon(block)
95
+ type = block[:type].to_sym
96
+ block[type]
97
+ end
98
+
99
+ def enrich_text_content(text, content)
100
+ enriched_content = add_link(text, content)
101
+ add_annotations(text, enriched_content)
102
+ end
103
+
104
+ def add_link(text, content)
105
+ href = text[:href]
106
+ return content if href.nil?
107
+
108
+ "[#{content}](#{href})"
109
+ end
110
+
111
+ def add_annotations(text, content)
112
+ annotations = text[:annotations].select { |_key, value| !!value }
113
+ annotations.keys.inject(content) do |enriched_content, annotation|
114
+ TextAnnotation.send(annotation.to_sym, enriched_content)
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'notion'
4
- require_relative './logger'
5
- require_relative './text_annotation'
6
-
7
3
  module NotionToMd
8
4
  class Converter
9
5
  attr_reader :page_id
@@ -13,139 +9,48 @@ module NotionToMd
13
9
  @page_id = page_id
14
10
  end
15
11
 
16
- def convert
17
- md = page_blocks[:results].map do |block|
18
- next blank if block[:type] == 'paragraph' && block.dig(:paragraph, :text).empty?
19
-
20
- block_type = block[:type].to_sym
21
- send(block_type, block[block_type])
22
- end
23
- Logger.info("Notion page #{page_id} converted to markdown")
24
- md.join("\n\n")
12
+ def convert(frontmatter: false)
13
+ <<~MD
14
+ #{parse_frontmatter if frontmatter}
15
+ #{parse_content}
16
+ MD
25
17
  end
26
18
 
27
19
  private
28
20
 
29
- def page_blocks
30
- @page_blocks ||= @notion.block_children(id: page_id)
31
- end
32
-
33
-
34
- def paragraph(block)
35
- convert_text(block)
36
- end
37
-
38
- def heading_1(block)
39
- "# #{convert_text(block)}"
40
- end
41
-
42
- def heading_2(block)
43
- "## #{convert_text(block)}"
44
- end
45
-
46
- def heading_3(block)
47
- "### #{convert_text(block)}"
48
- end
49
-
50
- def callout(block)
51
- icon = get_icon(block[:icon])
52
- text = convert_text(block)
53
- "#{icon} #{text}"
54
- end
55
-
56
- def quote(block)
57
- "> #{convert_text(block)}"
58
- end
59
-
60
- def bulleted_list_item(block)
61
- "- #{convert_text(block)}"
62
- end
63
-
64
- # TODO: numbered_list_item
65
- def numbered_list_item(block)
66
- Logger.info('numbered_list_item type not supported. Shown as bulleted_list_item.')
67
- bulleted_list_item(block)
68
- end
69
-
70
- def to_do(block)
71
- checked = block[:checked]
72
- text = convert_text(block)
73
-
74
- "- #{checked ? '[x]' : '[ ]'} #{text}"
75
- end
76
-
77
- def code(block)
78
- language = block[:language]
79
- text = convert_text(block)
80
-
81
- "```#{language}\n\t#{text}\n```"
82
- end
83
-
84
- def embed(block)
85
- url = block[:url]
86
-
87
- "[#{url}](#{url})"
88
- end
89
-
90
- def image(block)
91
- type = block[:type].to_sym
92
- url = block.dig(type, :url)
93
- caption = convert_caption(block)
94
-
95
- "![](#{url})\n\n#{caption}"
96
- end
97
-
98
- def bookmark(block)
99
- url = block[:url]
100
- "[#{url}](#{url})"
101
- end
102
-
103
- def divider(_block)
104
- '---'
105
- end
106
-
107
- def equation(block)
108
- equ = convert_text(block)
109
- "$$ #{equ} $$"
110
- end
111
-
112
- def blank
113
- '<br />'
114
- end
115
-
116
- def convert_text(block)
117
- block[:text].map do |text|
118
- content = text[:plain_text]
119
- enrich_text_content(text, content)
120
- end.join
121
- end
21
+ def parse_content
22
+ md = page_blocks[:results].map do |block|
23
+ next Block.blank if block[:type] == 'paragraph' && block.dig(:paragraph, :text).empty?
122
24
 
123
- def convert_caption(block)
124
- convert_text(text: block[:caption])
125
- end
25
+ block_type = block[:type].to_sym
126
26
 
127
- def get_icon(block)
128
- type = block[:type].to_sym
129
- block[type]
27
+ begin
28
+ Block.send(block_type, block[block_type])
29
+ rescue StandardError
30
+ Logger.info("Unsupported block type: #{block_type}")
31
+ next nil
32
+ end
33
+ end
34
+ Logger.info("Notion page #{page_id} converted to markdown")
35
+ md.compact.join("\n\n")
130
36
  end
131
37
 
132
- def enrich_text_content(text, content)
133
- enriched_content = add_link(text, content)
134
- add_annotations(text, enriched_content)
38
+ def parse_frontmatter
39
+ notion_page = Page.new(page: page)
40
+ frontmatter = notion_page.props.to_a.map { |k, v| "#{k}: #{v}" }.join("\n")
41
+ <<~CONTENT
42
+ ---
43
+ #{frontmatter}
44
+ ---
45
+ CONTENT
135
46
  end
136
47
 
137
- def add_link(text, content)
138
- href = text[:href]
139
- return content if href.nil?
140
-
141
- "[#{content}](#{href})"
48
+ def page
49
+ @page ||= @notion.page(id: page_id)
142
50
  end
143
51
 
144
- def add_annotations(text, content)
145
- annotations = text[:annotations].select { |key, value| !!value }
146
- annotations.keys.inject(content) do |enriched_content, annotation|
147
- TextAnnotation.send(annotation.to_sym, enriched_content)
148
- end
52
+ def page_blocks
53
+ @page_blocks ||= @notion.block_children(id: page_id)
149
54
  end
150
55
  end
151
56
  end
@@ -1,16 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'logger'
4
-
5
3
  module NotionToMd
6
4
  class Logger
7
- @logger = ::Logger.new(STDOUT)
5
+ @logger = ::Logger.new($stdout)
8
6
 
9
7
  class << self
10
8
  extend Forwardable
11
- def_delegators :@logger, :debug, :info, :warn, :error, :fatal
9
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal, :level, :level=
12
10
  end
13
11
  end
14
12
  end
15
-
16
-
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NotionToMd
4
+ class Page
5
+ attr_reader :page
6
+
7
+ def initialize(page:)
8
+ @page = page
9
+ end
10
+
11
+ def title
12
+ page.dig(:properties, :Name, :title).inject('') do |acc, slug|
13
+ acc + slug[:plain_text]
14
+ end
15
+ end
16
+
17
+ def cover
18
+ page.dig(:cover, :external, :url)
19
+ end
20
+
21
+ def icon
22
+ page.dig(:icon, :emoji)
23
+ end
24
+
25
+ def id
26
+ page[:id]
27
+ end
28
+
29
+ def created_time
30
+ DateTime.parse(page['created_time'])
31
+ end
32
+
33
+ def last_edited_time
34
+ DateTime.parse(page['last_edited_time'])
35
+ end
36
+
37
+ def url
38
+ page[:url]
39
+ end
40
+
41
+ def archived
42
+ page[:archived]
43
+ end
44
+
45
+ def props
46
+ @props ||= custom_props.deep_merge(default_props)
47
+ end
48
+
49
+ def custom_props
50
+ @custom_props ||= page.properties.each_with_object({}) do |prop, memo|
51
+ name = prop.first
52
+ value = prop.last # Notion::Messages::Message
53
+ type = value.type
54
+
55
+ next memo unless CustomProperty.respond_to?(type.to_sym)
56
+
57
+ memo[name.parameterize.underscore] = CustomProperty.send(type, value)
58
+ end.reject { |_k, v| v.presence.nil? }
59
+ end
60
+
61
+ def default_props
62
+ @default_props ||= {
63
+ 'id' => id,
64
+ 'title' => title,
65
+ 'created_time' => created_time,
66
+ 'cover' => cover,
67
+ 'icon' => icon,
68
+ 'last_edited_time' => last_edited_time,
69
+ 'archived' => archived
70
+ }
71
+ end
72
+
73
+ class CustomProperty
74
+ class << self
75
+ def multi_select(prop)
76
+ multi_select = prop.multi_select.map(&:name).join(', ')
77
+ "[#{multi_select}]"
78
+ end
79
+
80
+ def select(prop)
81
+ prop['select'].name
82
+ end
83
+
84
+ def people(prop)
85
+ people = prop.people.map(&:name).join(', ')
86
+ "[#{people}]"
87
+ end
88
+
89
+ def files(prop)
90
+ files = prop.files.map { |f| "\"#{f.file.url}\"" }.join(', ')
91
+ "[#{files}]"
92
+ end
93
+
94
+ def phone_number(prop)
95
+ prop.phone_number
96
+ end
97
+
98
+ def number(prop)
99
+ prop.number
100
+ end
101
+
102
+ def email(prop)
103
+ prop.email
104
+ end
105
+
106
+ def checkbox(prop)
107
+ prop.checkbox.to_s
108
+ end
109
+
110
+ # date type properties not supported:
111
+ # - end
112
+ # - time_zone
113
+ def date(prop)
114
+ prop.date.start
115
+ end
116
+
117
+ def url(prop)
118
+ prop.url
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,13 @@
1
+ module NotionToMd
2
+ class Text
3
+ class << self
4
+ def text(text)
5
+ text[:plain_text]
6
+ end
7
+
8
+ def equation(text)
9
+ "$$ #{text[:plain_text]} $$"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NotionToMd
2
4
  # Append the text type:
3
5
  # * italic: boolean,
@@ -9,19 +11,19 @@ module NotionToMd
9
11
  class TextAnnotation
10
12
  class << self
11
13
  def italic(text)
12
- "_#{text}_"
14
+ "*#{text}*"
13
15
  end
14
16
 
15
17
  def bold(text)
16
18
  "**#{text}**"
17
19
  end
18
20
 
19
- def striketrough(text)
21
+ def strikethrough(text)
20
22
  "~~#{text}~~"
21
23
  end
22
24
 
23
25
  def underline(text)
24
- text
26
+ "<u>#{text}</u>"
25
27
  end
26
28
 
27
29
  def code(text)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NotionToMd
4
- VERSION = '0.0.1'
4
+ VERSION = '0.2.0'
5
5
  end
data/lib/notion_to_md.rb CHANGED
@@ -1,4 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'notion'
4
+ require 'logger'
5
+ require 'active_support/inflector'
6
+ require 'active_support/core_ext/object/blank'
3
7
  require_relative './notion_to_md/version'
4
8
  require_relative './notion_to_md/converter'
9
+ require_relative './notion_to_md/logger'
10
+ require_relative './notion_to_md/page'
11
+ require_relative './notion_to_md/block'
12
+ require_relative './notion_to_md/text'
13
+ require_relative './notion_to_md/text_annotation'
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: notion_to_md
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.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: 2021-12-29 00:00:00.000000000 Z
11
+ date: 2022-02-15 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '6'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '6'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: notion-ruby-client
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +38,48 @@ dependencies:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
40
  version: 0.0.8
41
+ - !ruby/object:Gem::Dependency
42
+ name: hashie
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
27
83
  description: Notion Markdown Exporter in Ruby
28
84
  email: emoriarty81@gmail.com
29
85
  executables: []
@@ -32,8 +88,11 @@ extra_rdoc_files: []
32
88
  files:
33
89
  - README.md
34
90
  - lib/notion_to_md.rb
91
+ - lib/notion_to_md/block.rb
35
92
  - lib/notion_to_md/converter.rb
36
93
  - lib/notion_to_md/logger.rb
94
+ - lib/notion_to_md/page.rb
95
+ - lib/notion_to_md/text.rb
37
96
  - lib/notion_to_md/text_annotation.rb
38
97
  - lib/notion_to_md/version.rb
39
98
  homepage: https://github.com/emoriarty/notion_to_md