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 +4 -4
- data/README.md +2 -0
- data/lib/notion_to_md/blocks/block.rb +62 -0
- data/lib/notion_to_md/blocks/types.rb +126 -0
- data/lib/notion_to_md/blocks.rb +46 -0
- data/lib/notion_to_md/converter.rb +34 -1
- data/lib/notion_to_md/page.rb +2 -13
- data/lib/notion_to_md/text_annotation.rb +2 -0
- data/lib/notion_to_md/version.rb +1 -1
- data/lib/notion_to_md.rb +1 -1
- metadata +19 -3
- data/lib/notion_to_md/block.rb +0 -118
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7b301a14c22e1790633fc88c179d3882521f9c9632655b6fd9fc883cf128e9dc
|
4
|
+
data.tar.gz: 528414ba605c3e03bb3599ea70b7e998768cba3ba56429aaa23523f203b8de19
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
+
"\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 ||=
|
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
|
data/lib/notion_to_md/page.rb
CHANGED
@@ -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
|
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)
|
data/lib/notion_to_md/version.rb
CHANGED
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/
|
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:
|
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-
|
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/
|
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
|
data/lib/notion_to_md/block.rb
DELETED
@@ -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
|
-
"\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
|