notion_to_md 0.0.1 → 0.2.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: 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