notion_to_md 2.5.0 → 3.0.0.beta1
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 -82
- data/lib/notion_to_md/blocks/block.rb +47 -15
- data/lib/notion_to_md/blocks/factory.rb +51 -14
- data/lib/notion_to_md/blocks/normalizer.rb +97 -16
- data/lib/notion_to_md/blocks/numbered_list_item_block.rb +1 -3
- data/lib/notion_to_md/blocks/{types.rb → renderer.rb} +21 -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 +85 -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 +68 -93
- data/lib/notion_to_md/support/frontmatter.rb +119 -0
- data/lib/notion_to_md/support/metadata_properties.rb +107 -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 +30 -1
- 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 -64
- data/lib/notion_to_md/blocks.rb +0 -32
- data/lib/notion_to_md/converter.rb +0 -72
- data/lib/notion_to_md/helpers/yaml_sanitizer.rb +0 -17
- data/lib/notion_to_md/helpers.rb +0 -1
- 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: 2671f75f28373209f1846e06ed528f7adcc9d857da3fde4d1b0a5e43144c5850
|
4
|
+
data.tar.gz: 39c65f58ff9432277667c0c5c2597561d1b28d9252a7868ec2b8885d0e94e07a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c1571b5b93eb39bccb66032db2296d84b1d02313a6098e3d0e0f03022bfbea5eb0d46b54ae20b7bed6a9a5d6e79721922623491b5b1b829060e08b09cb67114
|
7
|
+
data.tar.gz: 5eea9bf71539d0d6c66880c89a39950c42da4c67767d651b1e218ebb380539b27c5b3ed5f00c18c77a13f547db9edc21f3645bb66e4c3e6204d08b59085da0fa
|
data/README.md
CHANGED
@@ -1,113 +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`
|
79
|
+
If your token is stored in `NOTION_TOKEN`, you don’t need to pass it explicitly:
|
88
80
|
|
89
|
-
|
81
|
+
```bash
|
82
|
+
export NOTION_TOKEN=<secret_...>
|
83
|
+
```
|
90
84
|
|
91
|
-
|
85
|
+
```ruby
|
86
|
+
md = NotionToMd.call(:page, id: 'b91d5...')
|
87
|
+
```
|
88
|
+
|
89
|
+
## Supported Blocks
|
92
90
|
|
93
|
-
|
94
|
-
* `bulleted_list_item`
|
95
|
-
* `numbered_list_item`
|
96
|
-
* `to_do`
|
91
|
+
Everything in Notion is a [block object](https://developers.notion.com/reference/block#block-object-keys).
|
97
92
|
|
98
|
-
|
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 | ❌ |
|
99
108
|
|
100
|
-
|
109
|
+
## Front Matter
|
101
110
|
|
102
|
-
By default,
|
111
|
+
By default, front matter is **disabled**. Enable it with:
|
103
112
|
|
104
113
|
```ruby
|
105
|
-
NotionToMd
|
106
|
-
# or
|
107
|
-
NotionToMd.convert(page_id: 'b91d5...', frontmatter: true) # Since v2.3
|
114
|
+
NotionToMd.call(:page, id: 'b91d5...', frontmatter: true)
|
108
115
|
```
|
109
116
|
|
110
|
-
|
117
|
+
### Examples
|
118
|
+
|
119
|
+
**Default properties:**
|
111
120
|
|
112
121
|
```yml
|
113
122
|
---
|
@@ -125,9 +134,7 @@ last_edited_by_object: user
|
|
125
134
|
---
|
126
135
|
```
|
127
136
|
|
128
|
-
|
129
|
-
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.
|
130
|
-
For example, two properties named `Multiple Options` and `Tags` will be transformed to `multiple_options` and `tags`, respectively.
|
137
|
+
**Custom properties:**
|
131
138
|
|
132
139
|
```yml
|
133
140
|
---
|
@@ -136,25 +143,30 @@ multiple_options: option1, option2
|
|
136
143
|
---
|
137
144
|
```
|
138
145
|
|
139
|
-
|
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)
|
140
161
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
* `date`
|
145
|
-
* `people`
|
146
|
-
* `files`
|
147
|
-
* `checkbox`
|
148
|
-
* `url`
|
149
|
-
* `email`
|
150
|
-
* `phone_number`
|
151
|
-
* `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).
|
152
165
|
|
153
|
-
|
166
|
+
## Development
|
154
167
|
|
155
|
-
|
168
|
+
Run the test suite with:
|
156
169
|
|
157
|
-
## Test
|
158
170
|
```bash
|
159
171
|
rspec
|
160
172
|
```
|
@@ -4,37 +4,55 @@ require 'forwardable'
|
|
4
4
|
|
5
5
|
class NotionToMd
|
6
6
|
module Blocks
|
7
|
+
# === NotionToMd::Blocks::Block
|
8
|
+
#
|
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
|
7
20
|
class Block
|
8
21
|
extend Forwardable
|
9
22
|
|
10
|
-
|
23
|
+
# @return [Notion::Messages::Message] The Notion API block data.
|
24
|
+
attr_reader :block
|
11
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`.
|
12
30
|
def_delegators :block, :type
|
13
31
|
|
14
|
-
#
|
15
|
-
# block::
|
16
|
-
# A {Notion::Messages::Message}[https://github.com/orbit-love/notion-ruby-client/blob/main/lib/notion/messages/message.rb] object.
|
17
|
-
# children::
|
18
|
-
# An array of NotionToMd::Block::Block objects.
|
19
|
-
#
|
20
|
-
# === Returns
|
21
|
-
# A Block object.
|
32
|
+
# Initialize a new block wrapper.
|
22
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.
|
23
38
|
def initialize(block:, children: [])
|
24
39
|
@block = block
|
25
40
|
@children = children
|
26
41
|
end
|
27
42
|
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
43
|
+
# Convert the block (and its children) into Markdown.
|
44
|
+
#
|
45
|
+
# Uses {NotionToMd::Blocks::Renderer} to render
|
46
|
+
# the Markdown representation of the block.
|
47
|
+
#
|
48
|
+
# @param tab_width [Integer] The number of tabs used to indent nested blocks. Defaults to 0.
|
31
49
|
#
|
32
|
-
#
|
33
|
-
# The current block (and its children) converted to a markdown string.
|
50
|
+
# @return [String, nil] The Markdown string, or `nil` if the block type is unsupported.
|
34
51
|
#
|
52
|
+
# @see NotionToMd::Blocks::Renderer
|
35
53
|
def to_md(tab_width: 0)
|
36
54
|
block_type = block.type.to_sym
|
37
|
-
md =
|
55
|
+
md = Renderer.send(block_type, block[block_type]) + newline
|
38
56
|
md + build_nested_blocks(tab_width + 1)
|
39
57
|
rescue NoMethodError
|
40
58
|
Logger.info("Unsupported block type: #{block_type}")
|
@@ -43,21 +61,35 @@ class NotionToMd
|
|
43
61
|
|
44
62
|
protected
|
45
63
|
|
64
|
+
# @return [String] Two newlines separating blocks.
|
46
65
|
def newline
|
47
66
|
"\n\n"
|
48
67
|
end
|
49
68
|
|
69
|
+
# Build the Markdown for all nested blocks.
|
70
|
+
#
|
71
|
+
# @param tab_width [Integer] The indentation level.
|
72
|
+
# @return [String]
|
50
73
|
def build_nested_blocks(tab_width)
|
51
74
|
mds = markdownify_children(tab_width).compact
|
52
75
|
indent_children(mds, tab_width).join
|
53
76
|
end
|
54
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).
|
55
82
|
def markdownify_children(tab_width)
|
56
83
|
children.map do |nested_block|
|
57
84
|
nested_block.to_md(tab_width: tab_width)
|
58
85
|
end
|
59
86
|
end
|
60
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.
|
61
93
|
def indent_children(mds, tab_width)
|
62
94
|
mds.map do |md|
|
63
95
|
"#{"\t" * tab_width}#{md}"
|
@@ -2,22 +2,59 @@
|
|
2
2
|
|
3
3
|
class NotionToMd
|
4
4
|
module Blocks
|
5
|
+
# === NotionToMd::Blocks::Factory
|
6
|
+
#
|
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
|
5
20
|
class Factory
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
20
55
|
end
|
56
|
+
|
57
|
+
alias build call
|
21
58
|
end
|
22
59
|
end
|
23
60
|
end
|
@@ -2,62 +2,143 @@
|
|
2
2
|
|
3
3
|
class NotionToMd
|
4
4
|
module Blocks
|
5
|
+
# === NotionToMd::Blocks::Normalizer
|
6
|
+
#
|
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
|
5
26
|
class Normalizer
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
12
38
|
end
|
13
39
|
|
40
|
+
# @return [Array<NotionToMd::Blocks::Block>]
|
41
|
+
# The mutable, normalized block list.
|
14
42
|
attr_reader :normalized_blocks
|
15
43
|
|
44
|
+
# Create a new normalizer.
|
45
|
+
#
|
46
|
+
# @param blocks [Array<NotionToMd::Blocks::Block>]
|
47
|
+
# Raw blocks to normalize.
|
16
48
|
def initialize(blocks:)
|
17
49
|
@normalized_blocks = blocks.dup
|
18
50
|
end
|
19
51
|
|
20
|
-
|
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
|
21
61
|
normalize_for :bulleted_list_item
|
22
62
|
normalize_for :numbered_list_item
|
23
63
|
normalize_for :to_do
|
64
|
+
normalized_blocks
|
24
65
|
end
|
25
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.
|
26
73
|
def normalize_for(type)
|
27
74
|
new_blocks = []
|
28
75
|
|
29
76
|
normalized_blocks.each do |block|
|
30
|
-
if block
|
77
|
+
if block_of_type?(block, type)
|
78
|
+
# Accumulate consecutive blocks of the target type
|
31
79
|
blocks_to_normalize << block
|
32
80
|
else
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
|
37
|
-
new_blocks << new_block_and_reset_blocks_to_normalize(type) unless blocks_to_normalize.empty?
|
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
|
84
|
+
flush_blocks_to_normalize_into(new_blocks, type)
|
38
85
|
new_blocks << block
|
39
86
|
end
|
40
87
|
end
|
41
88
|
|
42
|
-
# If the
|
43
|
-
#
|
44
|
-
new_blocks
|
89
|
+
# If the sequence ended with a run of the target type, it hasn’t been
|
90
|
+
# emitted yet—flush it now.
|
91
|
+
flush_blocks_to_normalize_into(new_blocks, type)
|
45
92
|
|
93
|
+
# Replace the working set with the newly normalized list
|
46
94
|
normalized_blocks.replace(new_blocks)
|
47
95
|
end
|
48
96
|
|
49
97
|
private
|
50
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.
|
103
|
+
def block_of_type?(block, type)
|
104
|
+
block.type.to_sym == type
|
105
|
+
end
|
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]
|
113
|
+
def flush_blocks_to_normalize_into(new_blocks, type)
|
114
|
+
return if blocks_to_normalize.empty?
|
115
|
+
|
116
|
+
new_blocks << new_block_and_reset_blocks_to_normalize(type)
|
117
|
+
end
|
118
|
+
|
119
|
+
# @api private
|
120
|
+
# Create a grouped block and reset buffer.
|
121
|
+
#
|
122
|
+
# @param type [Symbol]
|
123
|
+
# @return [NotionToMd::Blocks::Block]
|
51
124
|
def new_block_and_reset_blocks_to_normalize(type)
|
52
125
|
new_block = new_block_for(type, blocks_to_normalize)
|
53
126
|
@blocks_to_normalize = []
|
54
127
|
new_block
|
55
128
|
end
|
56
129
|
|
130
|
+
# @api private
|
131
|
+
# @return [Array<NotionToMd::Blocks::Block>]
|
57
132
|
def blocks_to_normalize
|
58
133
|
@blocks_to_normalize ||= []
|
59
134
|
end
|
60
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]
|
61
142
|
def new_block_for(type, children)
|
62
143
|
case type
|
63
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?
|
@@ -111,6 +127,10 @@ class NotionToMd
|
|
111
127
|
file(block)
|
112
128
|
end
|
113
129
|
|
130
|
+
def equation(block)
|
131
|
+
"$$#{block['expression']}$$"
|
132
|
+
end
|
133
|
+
|
114
134
|
private
|
115
135
|
|
116
136
|
def convert_table_row(cells)
|