notion_to_md 2.5.1 → 3.0.0.beta2
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 +94 -83
- data/lib/notion_to_md/blocks/block.rb +45 -16
- data/lib/notion_to_md/blocks/factory.rb +49 -17
- data/lib/notion_to_md/blocks/normalizer.rb +82 -14
- data/lib/notion_to_md/blocks/numbered_list_item_block.rb +1 -3
- data/lib/notion_to_md/blocks/{types.rb → renderer.rb} +17 -1
- data/lib/notion_to_md/blocks/to_do_list_item_block.rb +0 -2
- data/lib/notion_to_md/database/builder.rb +107 -0
- data/lib/notion_to_md/database.rb +114 -0
- data/lib/notion_to_md/logger.rb +1 -0
- data/lib/notion_to_md/metadata_type.rb +170 -0
- data/lib/notion_to_md/page/builder.rb +112 -0
- data/lib/notion_to_md/page.rb +80 -106
- data/lib/notion_to_md/support/frontmatter.rb +119 -0
- data/lib/notion_to_md/support/metadata_properties.rb +102 -0
- data/lib/notion_to_md/support/pagination.rb +55 -0
- data/lib/notion_to_md/support/yaml_sanitizer.rb +42 -0
- data/lib/notion_to_md/text.rb +27 -0
- data/lib/notion_to_md/text_annotation.rb +41 -9
- data/lib/notion_to_md/version.rb +1 -1
- data/lib/notion_to_md.rb +65 -38
- metadata +24 -103
- data/lib/notion_to_md/blocks/builder.rb +0 -67
- data/lib/notion_to_md/blocks.rb +0 -35
- data/lib/notion_to_md/converter.rb +0 -72
- data/lib/notion_to_md/helpers/yaml_sanitizer.rb +0 -19
- data/lib/notion_to_md/helpers.rb +0 -3
- data/lib/notion_to_md/page_property.rb +0 -106
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3892953f8ab81cb3c4ff219452e8daf615997b27b41aedcd64984d127fcc2d6e
|
4
|
+
data.tar.gz: d85c49651c090a1fbd47d0fd57584b71ec7e76955147c9a4526b60ffd496c354
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7414f7b60612f7dbf9565b4e5d97d0f6c96c389abd5f0c0d46906711621d99e5e617fcf94e2809e483f244438f9184dacc4ba043bc52d6129909a93ae05229cc
|
7
|
+
data.tar.gz: 6a4520688e495502e140e170f69ff78fa55ead84021e0bb8e2cbc99615fd30d87be3bb613ee18a253a0c41403567b0786f44f78f6654c9c272f690aeddb1c176
|
data/README.md
CHANGED
@@ -1,114 +1,122 @@
|
|
1
|
+
<img src="https://ik.imagekit.io/gxidvqvc9/noiton_to_md_logo_white_bg_-OiZSEkqY.png?updatedAt=1756209770491" width="150">
|
2
|
+
|
1
3
|
# notion_to_md
|
2
|
-
Notion Markdown Exporter in Ruby.
|
3
4
|
|
4
|
-
|
5
|
+
A Ruby library to export [Notion](https://www.notion.so/) pages and databases to Markdown.
|
6
|
+
The output is fully compliant with the [GitHub Flavored Markdown specification](https://github.github.com/gfm/).
|
7
|
+
|
8
|
+
[](https://badge.fury.io/rb/notion_to_md)
|
9
|
+
[](https://github.com/emoriarty/notion_to_md/actions)
|
10
|
+
|
11
|
+
> [!NOTE]
|
12
|
+
> You are reading the documentation for the latest development branch.
|
13
|
+
> For the stable **v2.x.x** documentation, see [the v2.x.x branch](https://github.com/emoriarty/notion_to_md/tree/v2.x.x).
|
5
14
|
|
6
15
|
## Installation
|
7
|
-
|
16
|
+
|
17
|
+
Install via RubyGems:
|
18
|
+
|
8
19
|
```bash
|
9
|
-
|
20
|
+
gem install notion_to_md
|
10
21
|
```
|
11
22
|
|
12
|
-
Or add it to
|
23
|
+
Or add it to your `Gemfile`:
|
24
|
+
|
13
25
|
```ruby
|
14
|
-
# Gemfile
|
15
26
|
gem 'notion_to_md'
|
16
27
|
```
|
17
28
|
|
18
|
-
|
19
|
-
Before using the gem create an integration and generate a secret token. Check [notion getting started guide](https://developers.notion.com/docs/getting-started) to learn more.
|
20
|
-
|
21
|
-
Pass the page id and secret token to the constructor and execute the `convert` method.
|
29
|
+
### Beta version (3.0.0.beta1)
|
22
30
|
|
23
|
-
|
24
|
-
require 'notion_to_md'
|
31
|
+
If you want to try the **beta release**, install with the `--pre` flag:
|
25
32
|
|
26
|
-
|
27
|
-
|
33
|
+
```bash
|
34
|
+
gem install notion_to_md --pre
|
28
35
|
```
|
29
36
|
|
30
|
-
|
37
|
+
Or pin the beta in your Gemfile:
|
31
38
|
|
32
39
|
```ruby
|
33
|
-
|
40
|
+
gem "notion_to_md", "3.0.0.beta1"
|
34
41
|
```
|
35
42
|
|
36
|
-
|
43
|
+
⚠️ This version is under active development. For stable usage, prefer the latest `2.x.x` release.
|
37
44
|
|
38
|
-
|
39
|
-
|
40
|
-
```
|
45
|
+
|
46
|
+
## Quick Start
|
41
47
|
|
42
48
|
```ruby
|
43
|
-
|
44
|
-
md =
|
45
|
-
|
46
|
-
md = NotionToMd.convert(page_id: 'b91d5...')
|
49
|
+
# Convert a Notion page to Markdown
|
50
|
+
md = NotionToMd.call(:page, id: 'b91d5...', token: ENV['NOTION_TOKEN'])
|
51
|
+
File.write("page.md", md)
|
47
52
|
```
|
48
53
|
|
49
|
-
|
54
|
+
## Usage
|
50
55
|
|
51
|
-
|
56
|
+
Before using the gem, create a Notion integration and obtain a secret token.
|
57
|
+
See the [Notion Getting Started Guide](https://developers.notion.com/docs/getting-started) for details.
|
52
58
|
|
53
|
-
|
59
|
+
### Pages
|
54
60
|
|
55
61
|
```ruby
|
56
|
-
md = NotionToMd.call(
|
62
|
+
md = NotionToMd.call(:page, id: 'b91d5...', token: 'secret_...')
|
63
|
+
# or equivalently
|
64
|
+
md = NotionToMd.convert(:page, id: 'b91d5...', token: 'secret_...')
|
57
65
|
```
|
58
66
|
|
59
|
-
|
67
|
+
`md` is a string containing the page content in Markdown format.
|
68
|
+
|
69
|
+
### Databases
|
60
70
|
|
61
71
|
```ruby
|
62
|
-
NotionToMd.call(
|
72
|
+
mds = NotionToMd.call(:database, id: 'b91d5...')
|
63
73
|
```
|
64
74
|
|
65
|
-
|
75
|
+
`mds` is an array of strings, one per page.
|
66
76
|
|
67
|
-
|
77
|
+
### Environment Variables
|
68
78
|
|
69
|
-
|
70
|
-
* `heading_1`
|
71
|
-
* `heading_2`
|
72
|
-
* `heading_3`
|
73
|
-
* `bulleted_list_item`
|
74
|
-
* `numbered_list_item` (supported since v2.3, in previous versions is displayed as `bulleted_list_item`)
|
75
|
-
* `to_do`
|
76
|
-
* `image`
|
77
|
-
* `bookmark`
|
78
|
-
* `callout`
|
79
|
-
* `quote`
|
80
|
-
* `divider`
|
81
|
-
* `table`
|
82
|
-
* `embed`
|
83
|
-
* `code`
|
84
|
-
* `link_preview`
|
85
|
-
* `file`
|
86
|
-
* `pdf`
|
87
|
-
* `video`
|
88
|
-
* `equation`
|
79
|
+
If your token is stored in `NOTION_TOKEN`, you don’t need to pass it explicitly:
|
89
80
|
|
90
|
-
|
81
|
+
```bash
|
82
|
+
export NOTION_TOKEN=<secret_...>
|
83
|
+
```
|
91
84
|
|
92
|
-
|
85
|
+
```ruby
|
86
|
+
md = NotionToMd.call(:page, id: 'b91d5...')
|
87
|
+
```
|
88
|
+
|
89
|
+
## Supported Blocks
|
93
90
|
|
94
|
-
|
95
|
-
* `bulleted_list_item`
|
96
|
-
* `numbered_list_item`
|
97
|
-
* `to_do`
|
91
|
+
Everything in Notion is a [block object](https://developers.notion.com/reference/block#block-object-keys).
|
98
92
|
|
99
|
-
|
93
|
+
| Block type | Nested children supported? |
|
94
|
+
|-----------------------------------|----------------------------|
|
95
|
+
| paragraph | ✅ |
|
96
|
+
| heading_1 / heading_2 / heading_3 | ❌ |
|
97
|
+
| bulleted_list_item | ✅ |
|
98
|
+
| numbered_list_item | ✅ |
|
99
|
+
| to_do | ✅ |
|
100
|
+
| image, file, pdf, video | ❌ |
|
101
|
+
| bookmark, embed, link_preview | ❌ |
|
102
|
+
| callout | ❌ |
|
103
|
+
| quote | ❌ |
|
104
|
+
| divider | ❌ |
|
105
|
+
| table | ❌ |
|
106
|
+
| code | ❌ |
|
107
|
+
| equation | ❌ |
|
100
108
|
|
101
|
-
|
109
|
+
## Front Matter
|
102
110
|
|
103
|
-
By default,
|
111
|
+
By default, front matter is **disabled**. Enable it with:
|
104
112
|
|
105
113
|
```ruby
|
106
|
-
NotionToMd
|
107
|
-
# or
|
108
|
-
NotionToMd.convert(page_id: 'b91d5...', frontmatter: true) # Since v2.3
|
114
|
+
NotionToMd.call(:page, id: 'b91d5...', frontmatter: true)
|
109
115
|
```
|
110
116
|
|
111
|
-
|
117
|
+
### Examples
|
118
|
+
|
119
|
+
**Default properties:**
|
112
120
|
|
113
121
|
```yml
|
114
122
|
---
|
@@ -126,9 +134,7 @@ last_edited_by_object: user
|
|
126
134
|
---
|
127
135
|
```
|
128
136
|
|
129
|
-
|
130
|
-
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.
|
131
|
-
For example, two properties named `Multiple Options` and `Tags` will be transformed to `multiple_options` and `tags`, respectively.
|
137
|
+
**Custom properties:**
|
132
138
|
|
133
139
|
```yml
|
134
140
|
---
|
@@ -137,25 +143,30 @@ multiple_options: option1, option2
|
|
137
143
|
---
|
138
144
|
```
|
139
145
|
|
140
|
-
|
146
|
+
Custom property names are [parameterized](https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-parameterize) and [underscored](https://api.rubyonrails.org/classes/ActiveSupport/Inflector.html#method-i-underscore).
|
147
|
+
|
148
|
+
Supported property types:
|
149
|
+
|
150
|
+
- `number`
|
151
|
+
- `select`
|
152
|
+
- `multi_select`
|
153
|
+
- `date`
|
154
|
+
- `people`
|
155
|
+
- `files`
|
156
|
+
- `checkbox`
|
157
|
+
- `url`
|
158
|
+
- `email`
|
159
|
+
- `phone_number`
|
160
|
+
- `rich_text` (plain text only)
|
141
161
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
* `date`
|
146
|
-
* `people`
|
147
|
-
* `files`
|
148
|
-
* `checkbox`
|
149
|
-
* `url`
|
150
|
-
* `email`
|
151
|
-
* `phone_number`
|
152
|
-
* `rich_text`, supported but in plain text
|
162
|
+
> [!NOTE]
|
163
|
+
> Advanced types such as `formula`, `relation`, and `rollup` are **not supported**.
|
164
|
+
> See the [Notion property value docs](https://developers.notion.com/reference/property-value-object#all-property-values).
|
153
165
|
|
154
|
-
|
166
|
+
## Development
|
155
167
|
|
156
|
-
|
168
|
+
Run the test suite with:
|
157
169
|
|
158
|
-
## Test
|
159
170
|
```bash
|
160
171
|
rspec
|
161
172
|
```
|
@@ -6,38 +6,53 @@ class NotionToMd
|
|
6
6
|
module Blocks
|
7
7
|
# === NotionToMd::Blocks::Block
|
8
8
|
#
|
9
|
-
#
|
9
|
+
# Represents a Notion block and provides functionality
|
10
|
+
# to convert it (and its children) into Markdown.
|
11
|
+
#
|
12
|
+
# A block corresponds to a `Notion::Messages::Message`
|
13
|
+
# returned by the [notion-ruby-client](https://github.com/orbit-love/notion-ruby-client).
|
14
|
+
#
|
15
|
+
# @example Convert a block to Markdown
|
16
|
+
# block = NotionToMd::Blocks::Block.new(block: notion_message, children: [])
|
17
|
+
# puts block.to_md
|
18
|
+
#
|
19
|
+
# @see NotionToMd::Blocks::Renderer
|
10
20
|
class Block
|
11
21
|
extend Forwardable
|
12
22
|
|
13
|
-
|
23
|
+
# @return [Notion::Messages::Message] The Notion API block data.
|
24
|
+
attr_reader :block
|
14
25
|
|
26
|
+
# @return [Array<NotionToMd::Blocks::Block>] The nested child blocks.
|
27
|
+
attr_reader :children
|
28
|
+
|
29
|
+
# Delegate the `#type` method to the underlying `block`.
|
15
30
|
def_delegators :block, :type
|
16
31
|
|
17
|
-
#
|
18
|
-
# block::
|
19
|
-
# A {Notion::Messages::Message}[https://github.com/orbit-love/notion-ruby-client/blob/main/lib/notion/messages/message.rb] object.
|
20
|
-
# children::
|
21
|
-
# An array of NotionToMd::Block::Block objects.
|
22
|
-
#
|
23
|
-
# === Returns
|
24
|
-
# A Block object.
|
32
|
+
# Initialize a new block wrapper.
|
25
33
|
#
|
34
|
+
# @param block [Notion::Messages::Message]
|
35
|
+
# A Notion block message from the Notion API.
|
36
|
+
# @param children [Array<NotionToMd::Blocks::Block>]
|
37
|
+
# Nested blocks belonging to this block.
|
26
38
|
def initialize(block:, children: [])
|
27
39
|
@block = block
|
28
40
|
@children = children
|
29
41
|
end
|
30
42
|
|
31
|
-
#
|
32
|
-
# tab_width::
|
33
|
-
# The number of tabs used to indent the block.
|
43
|
+
# Convert the block (and its children) into Markdown.
|
34
44
|
#
|
35
|
-
#
|
36
|
-
#
|
45
|
+
# Uses {NotionToMd::Blocks::Renderer} to render
|
46
|
+
# the Markdown representation of the block.
|
37
47
|
#
|
48
|
+
# @param tab_width [Integer] The number of tabs used to indent nested blocks. Defaults to 0.
|
49
|
+
#
|
50
|
+
# @return [String, nil] The Markdown string, or `nil` if the block type is unsupported.
|
51
|
+
#
|
52
|
+
# @see NotionToMd::Blocks::Renderer
|
38
53
|
def to_md(tab_width: 0)
|
39
54
|
block_type = block.type.to_sym
|
40
|
-
md =
|
55
|
+
md = Renderer.send(block_type, block[block_type]) + newline
|
41
56
|
md + build_nested_blocks(tab_width + 1)
|
42
57
|
rescue NoMethodError
|
43
58
|
Logger.info("Unsupported block type: #{block_type}")
|
@@ -46,21 +61,35 @@ class NotionToMd
|
|
46
61
|
|
47
62
|
protected
|
48
63
|
|
64
|
+
# @return [String] Two newlines separating blocks.
|
49
65
|
def newline
|
50
66
|
"\n\n"
|
51
67
|
end
|
52
68
|
|
69
|
+
# Build the Markdown for all nested blocks.
|
70
|
+
#
|
71
|
+
# @param tab_width [Integer] The indentation level.
|
72
|
+
# @return [String]
|
53
73
|
def build_nested_blocks(tab_width)
|
54
74
|
mds = markdownify_children(tab_width).compact
|
55
75
|
indent_children(mds, tab_width).join
|
56
76
|
end
|
57
77
|
|
78
|
+
# Render children blocks into Markdown.
|
79
|
+
#
|
80
|
+
# @param tab_width [Integer] The indentation level.
|
81
|
+
# @return [Array<String, nil>] Markdown strings (or nil if unsupported).
|
58
82
|
def markdownify_children(tab_width)
|
59
83
|
children.map do |nested_block|
|
60
84
|
nested_block.to_md(tab_width: tab_width)
|
61
85
|
end
|
62
86
|
end
|
63
87
|
|
88
|
+
# Indent the Markdown output of children by `tab_width`.
|
89
|
+
#
|
90
|
+
# @param mds [Array<String>] Markdown strings.
|
91
|
+
# @param tab_width [Integer] The indentation level.
|
92
|
+
# @return [Array<String>] Indented Markdown strings.
|
64
93
|
def indent_children(mds, tab_width)
|
65
94
|
mds.map do |md|
|
66
95
|
"#{"\t" * tab_width}#{md}"
|
@@ -4,26 +4,58 @@ class NotionToMd
|
|
4
4
|
module Blocks
|
5
5
|
# === NotionToMd::Blocks::Factory
|
6
6
|
#
|
7
|
-
# Factory class
|
7
|
+
# Factory class for instantiating the correct block wrapper class
|
8
|
+
# depending on the Notion block type.
|
9
|
+
#
|
10
|
+
# This is used by {NotionToMd::Page::Builder} when traversing Notion
|
11
|
+
# blocks returned from the API.
|
12
|
+
#
|
13
|
+
# @example Build a block instance from a Notion API message
|
14
|
+
# block = notion_client.block_children(block_id: "xxxx").results.first
|
15
|
+
# instance = NotionToMd::Blocks::Factory.build(block: block)
|
16
|
+
# instance # => NotionToMd::Blocks::Block subclass
|
17
|
+
#
|
18
|
+
# @see NotionToMd::Blocks::Block
|
19
|
+
# @see NotionToMd::Page::Builder
|
8
20
|
class Factory
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
class << self
|
22
|
+
# Build the appropriate block wrapper.
|
23
|
+
#
|
24
|
+
# @param block [Notion::Messages::Message]
|
25
|
+
# A Notion block message from the API.
|
26
|
+
# @param children [Array<NotionToMd::Blocks::Block>]
|
27
|
+
# Optional nested block instances.
|
28
|
+
#
|
29
|
+
# @return [NotionToMd::Blocks::Block]
|
30
|
+
# A block instance of the corresponding subclass:
|
31
|
+
#
|
32
|
+
# * {TableBlock} for `:table`
|
33
|
+
# * {TableRowBlock} for `:table_row`
|
34
|
+
# * {BulletedListItemBlock} for `:bulleted_list_item`
|
35
|
+
# * {NumberedListItemBlock} for `:numbered_list_item`
|
36
|
+
# * {ToDoListItemBlock} for `:to_do`
|
37
|
+
# * {NotionToMd::Blocks::Block} for all others
|
38
|
+
#
|
39
|
+
# @note Custom block classes must implement `#to_md`.
|
40
|
+
def call(block:, children: [])
|
41
|
+
case block.type.to_sym
|
42
|
+
when :table
|
43
|
+
TableBlock.new(block: block, children: children)
|
44
|
+
when :table_row
|
45
|
+
TableRowBlock.new(block: block, children: children)
|
46
|
+
when :bulleted_list_item
|
47
|
+
BulletedListItemBlock.new(block: block, children: children)
|
48
|
+
when :numbered_list_item
|
49
|
+
NumberedListItemBlock.new(block: block, children: children)
|
50
|
+
when :to_do
|
51
|
+
ToDoListItemBlock.new(block: block, children: children)
|
52
|
+
else
|
53
|
+
Blocks::Block.new(block: block, children: children)
|
54
|
+
end
|
24
55
|
end
|
56
|
+
|
57
|
+
alias build call
|
25
58
|
end
|
26
|
-
# rubocop:enable Metrics/MethodLength
|
27
59
|
end
|
28
60
|
end
|
29
61
|
end
|
@@ -4,73 +4,141 @@ class NotionToMd
|
|
4
4
|
module Blocks
|
5
5
|
# === NotionToMd::Blocks::Normalizer
|
6
6
|
#
|
7
|
-
#
|
7
|
+
# Responsible for normalizing sequences of adjacent blocks of the same type
|
8
|
+
# into grouped list blocks (e.g., multiple consecutive `bulleted_list_item`
|
9
|
+
# blocks into a single `BulletedListBlock`).
|
10
|
+
#
|
11
|
+
# This ensures Markdown output has proper list structures instead of
|
12
|
+
# repeated, ungrouped items.
|
13
|
+
#
|
14
|
+
# @example Normalize a sequence of blocks
|
15
|
+
# blocks = [
|
16
|
+
# BulletedListItemBlock.new(block: block1),
|
17
|
+
# BulletedListItemBlock.new(block: block2),
|
18
|
+
# NotionToMd::Blocks::Block.new(block: block3)
|
19
|
+
# ]
|
20
|
+
#
|
21
|
+
# normalized = NotionToMd::Blocks::Normalizer.normalize(blocks: blocks)
|
22
|
+
# # => [BulletedListBlock(children: [..]), Block(..)]
|
23
|
+
#
|
24
|
+
# @see NotionToMd::Blocks::Block
|
25
|
+
# @see NotionToMd::Blocks::Factory
|
8
26
|
class Normalizer
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
27
|
+
class << self
|
28
|
+
# Normalize a list of blocks.
|
29
|
+
#
|
30
|
+
# @param blocks [Array<NotionToMd::Blocks::Block>]
|
31
|
+
# Raw blocks to normalize.
|
32
|
+
#
|
33
|
+
# @return [Array<NotionToMd::Blocks::Block>]
|
34
|
+
# Normalized blocks where consecutive list items are grouped.
|
35
|
+
def call(blocks:)
|
36
|
+
new(blocks: blocks).call
|
37
|
+
end
|
15
38
|
end
|
16
39
|
|
40
|
+
# @return [Array<NotionToMd::Blocks::Block>]
|
41
|
+
# The mutable, normalized block list.
|
17
42
|
attr_reader :normalized_blocks
|
18
43
|
|
44
|
+
# Create a new normalizer.
|
45
|
+
#
|
46
|
+
# @param blocks [Array<NotionToMd::Blocks::Block>]
|
47
|
+
# Raw blocks to normalize.
|
19
48
|
def initialize(blocks:)
|
20
49
|
@normalized_blocks = blocks.dup
|
21
50
|
end
|
22
51
|
|
23
|
-
|
52
|
+
# Run normalization for all supported types:
|
53
|
+
#
|
54
|
+
# * `:bulleted_list_item`
|
55
|
+
# * `:numbered_list_item`
|
56
|
+
# * `:to_do`
|
57
|
+
#
|
58
|
+
# @return [Array<NotionToMd::Blocks::Block>]
|
59
|
+
# The final normalized block array.
|
60
|
+
def call
|
24
61
|
normalize_for :bulleted_list_item
|
25
62
|
normalize_for :numbered_list_item
|
26
63
|
normalize_for :to_do
|
64
|
+
normalized_blocks
|
27
65
|
end
|
28
66
|
|
67
|
+
# Normalize consecutive blocks of the given +type+ into a single grouped block,
|
68
|
+
# while preserving original order for everything else.
|
69
|
+
#
|
70
|
+
# The algorithm scans left-to-right, buffering runs of +type+. When a different
|
71
|
+
# type appears (or we reach the end), the buffered run is flushed as a grouped
|
72
|
+
# block and the non-matching block is appended as-is.
|
29
73
|
def normalize_for(type)
|
30
74
|
new_blocks = []
|
31
75
|
|
32
76
|
normalized_blocks.each do |block|
|
33
77
|
if block_of_type?(block, type)
|
78
|
+
# Accumulate consecutive blocks of the target type
|
34
79
|
blocks_to_normalize << block
|
35
80
|
else
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
# This is to keep the order of the blocks as they are in the original array.
|
81
|
+
# A different type breaks the run:
|
82
|
+
# 1) flush the buffered run as a single grouped block
|
83
|
+
# 2) append the current (non-matching) block
|
40
84
|
flush_blocks_to_normalize_into(new_blocks, type)
|
41
85
|
new_blocks << block
|
42
86
|
end
|
43
87
|
end
|
44
88
|
|
45
|
-
# If the
|
46
|
-
#
|
89
|
+
# If the sequence ended with a run of the target type, it hasn’t been
|
90
|
+
# emitted yet—flush it now.
|
47
91
|
flush_blocks_to_normalize_into(new_blocks, type)
|
48
92
|
|
93
|
+
# Replace the working set with the newly normalized list
|
49
94
|
normalized_blocks.replace(new_blocks)
|
50
95
|
end
|
51
96
|
|
52
97
|
private
|
53
98
|
|
99
|
+
# @api private
|
100
|
+
# @param block [NotionToMd::Blocks::Block]
|
101
|
+
# @param type [Symbol]
|
102
|
+
# @return [Boolean] true if the block matches the given type.
|
54
103
|
def block_of_type?(block, type)
|
55
104
|
block.type.to_sym == type
|
56
105
|
end
|
57
106
|
|
107
|
+
# @api private
|
108
|
+
# Flush accumulated blocks to the new array as a grouped block.
|
109
|
+
#
|
110
|
+
# @param new_blocks [Array<NotionToMd::Blocks::Block>]
|
111
|
+
# @param type [Symbol]
|
112
|
+
# @return [void]
|
58
113
|
def flush_blocks_to_normalize_into(new_blocks, type)
|
59
114
|
return if blocks_to_normalize.empty?
|
60
115
|
|
61
116
|
new_blocks << new_block_and_reset_blocks_to_normalize(type)
|
62
117
|
end
|
63
118
|
|
119
|
+
# @api private
|
120
|
+
# Create a grouped block and reset buffer.
|
121
|
+
#
|
122
|
+
# @param type [Symbol]
|
123
|
+
# @return [NotionToMd::Blocks::Block]
|
64
124
|
def new_block_and_reset_blocks_to_normalize(type)
|
65
125
|
new_block = new_block_for(type, blocks_to_normalize)
|
66
126
|
@blocks_to_normalize = []
|
67
127
|
new_block
|
68
128
|
end
|
69
129
|
|
130
|
+
# @api private
|
131
|
+
# @return [Array<NotionToMd::Blocks::Block>]
|
70
132
|
def blocks_to_normalize
|
71
133
|
@blocks_to_normalize ||= []
|
72
134
|
end
|
73
135
|
|
136
|
+
# @api private
|
137
|
+
# Create a wrapper block (list) depending on type.
|
138
|
+
#
|
139
|
+
# @param type [Symbol]
|
140
|
+
# @param children [Array<NotionToMd::Blocks::Block>]
|
141
|
+
# @return [NotionToMd::Blocks::Block]
|
74
142
|
def new_block_for(type, children)
|
75
143
|
case type
|
76
144
|
when :bulleted_list_item
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative './bulleted_list_item_block'
|
4
|
-
|
5
3
|
class NotionToMd
|
6
4
|
module Blocks
|
7
5
|
class NumberedListItemBlock < BulletedListItemBlock
|
@@ -15,7 +13,7 @@ class NotionToMd
|
|
15
13
|
# The current block (and its children) converted to a markdown string.
|
16
14
|
#
|
17
15
|
def to_md(tab_width: 0, index: nil)
|
18
|
-
md =
|
16
|
+
md = Renderer.numbered_list_item(block[block.type.to_sym], index) + newline
|
19
17
|
md + build_nested_blocks(tab_width + 1)
|
20
18
|
rescue NoMethodError
|
21
19
|
Logger.info("Unsupported block type: #{block.type}")
|
@@ -2,7 +2,23 @@
|
|
2
2
|
|
3
3
|
class NotionToMd
|
4
4
|
module Blocks
|
5
|
-
|
5
|
+
# === NotionToMd::Blocks::Renderer
|
6
|
+
#
|
7
|
+
# Stateless renderer for individual Notion block payloads into
|
8
|
+
# GitHub-Flavored Markdown (GFM). Each public class method expects a
|
9
|
+
# simplified block hash (already focused on the inner block content)
|
10
|
+
# and returns a Markdown string.
|
11
|
+
#
|
12
|
+
# This class is used by {NotionToMd::Blocks::Block#to_md}.
|
13
|
+
#
|
14
|
+
# @example Render a paragraph block
|
15
|
+
# md = NotionToMd::Blocks::Renderer.paragraph({ rich_text: [{ type: :text, plain_text: "Hello" }] })
|
16
|
+
# # => "Hello"
|
17
|
+
#
|
18
|
+
# @see NotionToMd::Blocks::Block
|
19
|
+
# @see NotionToMd::Blocks::Text
|
20
|
+
# @see NotionToMd::Blocks::TextAnnotation
|
21
|
+
class Renderer
|
6
22
|
class << self
|
7
23
|
def paragraph(block)
|
8
24
|
return blank if block[:rich_text].empty?
|