n8n-nodes-md2notion 1.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.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/credentials/NotionApi.credentials.d.ts +9 -0
- package/dist/credentials/NotionApi.credentials.js +37 -0
- package/dist/nodes/MarkdownToNotion/MarkdownToNotion.node.d.ts +13 -0
- package/dist/nodes/MarkdownToNotion/MarkdownToNotion.node.js +375 -0
- package/dist/nodes/MarkdownToNotion/notion.svg +12 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 n8n-nodes-markdown-to-notion contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# n8n-nodes-md2notion
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/n8n-nodes-md2notion)
|
|
4
|
+
[](https://github.com/shawnli1874/n8n-nodes-md2notion/actions)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
A custom n8n node that converts markdown content to Notion page blocks with **proper formula handling**.
|
|
8
|
+
|
|
9
|
+
## 🎯 Why This Node?
|
|
10
|
+
|
|
11
|
+
Existing n8n community nodes for markdown-to-Notion conversion have a critical flaw: they incorrectly handle inline math formulas like `$E = mc^2$`, causing rendering errors in Notion. This node **solves that problem** by:
|
|
12
|
+
|
|
13
|
+
- ✅ **Preserving math formulas** exactly as written
|
|
14
|
+
- ✅ **Using reliable parsing** with the remark ecosystem
|
|
15
|
+
- ✅ **Supporting all markdown elements** with proper formatting
|
|
16
|
+
- ✅ **Providing excellent error handling** and user feedback
|
|
17
|
+
|
|
18
|
+
## 🚀 Quick Start
|
|
19
|
+
|
|
20
|
+
### Installation
|
|
21
|
+
|
|
22
|
+
**Option 1: Install via n8n Community Nodes (Recommended)**
|
|
23
|
+
|
|
24
|
+
1. Open your n8n instance
|
|
25
|
+
2. Go to **Settings** → **Community Nodes**
|
|
26
|
+
3. Enter package name: `n8n-nodes-md2notion`
|
|
27
|
+
4. Click **Install**
|
|
28
|
+
|
|
29
|
+
**Option 2: Install via npm**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -g n8n-nodes-md2notion
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Setup
|
|
36
|
+
|
|
37
|
+
1. **Create Notion Integration**
|
|
38
|
+
- Go to [Notion Integrations](https://www.notion.so/my-integrations)
|
|
39
|
+
- Create a new integration and copy the API key
|
|
40
|
+
|
|
41
|
+
2. **Configure n8n Credentials**
|
|
42
|
+
- In n8n, create new **Notion API** credentials
|
|
43
|
+
- Paste your API key
|
|
44
|
+
|
|
45
|
+
3. **Share Notion Page**
|
|
46
|
+
- Open your target Notion page
|
|
47
|
+
- Click **Share** → **Invite** → Add your integration
|
|
48
|
+
|
|
49
|
+
### Usage
|
|
50
|
+
|
|
51
|
+
1. Add the **Markdown to Notion** node to your workflow
|
|
52
|
+
2. Select **Append to Page** operation
|
|
53
|
+
3. Enter your **Page ID** (from the Notion page URL)
|
|
54
|
+
4. Input your **markdown content**
|
|
55
|
+
5. Configure options as needed
|
|
56
|
+
|
|
57
|
+
## 📋 Features
|
|
58
|
+
|
|
59
|
+
### Supported Markdown Elements
|
|
60
|
+
|
|
61
|
+
| Element | Notion Block Type | Status |
|
|
62
|
+
|---------|------------------|--------|
|
|
63
|
+
| Headings (H1-H3) | `heading_1/2/3` | ✅ |
|
|
64
|
+
| Paragraphs | `paragraph` | ✅ |
|
|
65
|
+
| **Bold** and *italic* | Rich text formatting | ✅ |
|
|
66
|
+
| `Inline code` | Code annotation | ✅ |
|
|
67
|
+
| Code blocks | `code` | ✅ |
|
|
68
|
+
| - Bulleted lists | `bulleted_list_item` | ✅ |
|
|
69
|
+
| 1. Numbered lists | `numbered_list_item` | ✅ |
|
|
70
|
+
| > Blockquotes | `quote` | ✅ |
|
|
71
|
+
| [Links](url) | Rich text with links | ✅ |
|
|
72
|
+
| **Math formulas** | Preserved as plain text | ✅ |
|
|
73
|
+
|
|
74
|
+
### Configuration Options
|
|
75
|
+
|
|
76
|
+
- **Preserve Math Formulas**: Keep `$formula$` syntax intact (default: enabled)
|
|
77
|
+
- **Math Formula Delimiter**: Customize the delimiter character (default: `$`)
|
|
78
|
+
|
|
79
|
+
## 🧮 Math Formula Handling
|
|
80
|
+
|
|
81
|
+
**The Problem**: Other nodes convert `$E = mc^2$` incorrectly, breaking Notion rendering.
|
|
82
|
+
|
|
83
|
+
**Our Solution**: Smart formula preservation algorithm:
|
|
84
|
+
|
|
85
|
+
```markdown
|
|
86
|
+
Input: "This equation $E = mc^2$ is famous, but $10 is just money."
|
|
87
|
+
Output: "This equation $E = mc^2$ is famous, but $10 is just money."
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The node intelligently distinguishes between math formulas and regular dollar signs.
|
|
91
|
+
|
|
92
|
+
## 📖 Examples
|
|
93
|
+
|
|
94
|
+
### Basic Usage
|
|
95
|
+
|
|
96
|
+
```markdown
|
|
97
|
+
# My Research Notes
|
|
98
|
+
|
|
99
|
+
This document contains the famous equation $E = mc^2$ discovered by Einstein.
|
|
100
|
+
|
|
101
|
+
## Key Points
|
|
102
|
+
|
|
103
|
+
- **Energy** and mass are equivalent
|
|
104
|
+
- The speed of light is *constant*
|
|
105
|
+
- This applies to `special relativity`
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
const energy = mass * Math.pow(speedOfLight, 2);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> This formula revolutionized physics.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Advanced Example
|
|
115
|
+
|
|
116
|
+
```markdown
|
|
117
|
+
# Mathematical Concepts
|
|
118
|
+
|
|
119
|
+
## Calculus
|
|
120
|
+
The derivative of $f(x) = x^2$ is $f'(x) = 2x$.
|
|
121
|
+
|
|
122
|
+
## Statistics
|
|
123
|
+
The normal distribution: $f(x) = \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{1}{2}(\frac{x-\mu}{\sigma})^2}$
|
|
124
|
+
|
|
125
|
+
But remember, a coffee costs $5 at the local café.
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## 🔧 Development
|
|
129
|
+
|
|
130
|
+
### Prerequisites
|
|
131
|
+
|
|
132
|
+
- Node.js 16+
|
|
133
|
+
- npm or yarn
|
|
134
|
+
- n8n for testing
|
|
135
|
+
|
|
136
|
+
### Setup
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
# Clone the repository
|
|
140
|
+
git clone https://github.com/shawnli1874/n8n-nodes-markdown-to-notion.git
|
|
141
|
+
cd n8n-nodes-markdown-to-notion
|
|
142
|
+
|
|
143
|
+
# Install dependencies
|
|
144
|
+
npm install
|
|
145
|
+
|
|
146
|
+
# Build the project
|
|
147
|
+
npm run build
|
|
148
|
+
|
|
149
|
+
# Run tests
|
|
150
|
+
npm test
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Testing
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# Run core functionality tests
|
|
157
|
+
npm test
|
|
158
|
+
|
|
159
|
+
# Test manually with n8n
|
|
160
|
+
cp -r dist/* ~/.n8n/custom/
|
|
161
|
+
# Restart n8n and test in the UI
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## 🐛 Troubleshooting
|
|
165
|
+
|
|
166
|
+
### Common Issues
|
|
167
|
+
|
|
168
|
+
**Node not appearing in n8n**
|
|
169
|
+
- Ensure n8n is restarted after installation
|
|
170
|
+
- Check that the package is installed: `npm list -g n8n-nodes-markdown-to-notion`
|
|
171
|
+
|
|
172
|
+
**"Unauthorized" error**
|
|
173
|
+
- Verify your Notion API key is correct
|
|
174
|
+
- Ensure the integration is shared with the target page
|
|
175
|
+
|
|
176
|
+
**Math formulas not preserved**
|
|
177
|
+
- Check that "Preserve Math Formulas" option is enabled
|
|
178
|
+
- Verify your delimiter setting matches your content
|
|
179
|
+
|
|
180
|
+
**Page not found**
|
|
181
|
+
- Double-check the Page ID from the Notion URL
|
|
182
|
+
- Ensure the page is shared with your integration
|
|
183
|
+
|
|
184
|
+
### Debug Mode
|
|
185
|
+
|
|
186
|
+
Enable debug logging in n8n to see detailed error messages and API responses.
|
|
187
|
+
|
|
188
|
+
## 🤝 Contributing
|
|
189
|
+
|
|
190
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
191
|
+
|
|
192
|
+
### Quick Contribution Guide
|
|
193
|
+
|
|
194
|
+
1. Fork the repository
|
|
195
|
+
2. Create a feature branch: `git checkout -b feature/amazing-feature`
|
|
196
|
+
3. Make your changes and add tests
|
|
197
|
+
4. Ensure tests pass: `npm test`
|
|
198
|
+
5. Commit your changes: `git commit -m 'Add amazing feature'`
|
|
199
|
+
6. Push to the branch: `git push origin feature/amazing-feature`
|
|
200
|
+
7. Open a Pull Request
|
|
201
|
+
|
|
202
|
+
## 📄 License
|
|
203
|
+
|
|
204
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
205
|
+
|
|
206
|
+
## 🙏 Acknowledgments
|
|
207
|
+
|
|
208
|
+
- [n8n](https://n8n.io/) for the excellent workflow automation platform
|
|
209
|
+
- [Notion](https://notion.so/) for the powerful API
|
|
210
|
+
- [remark](https://remark.js.org/) for reliable markdown parsing
|
|
211
|
+
- The open source community for inspiration and feedback
|
|
212
|
+
|
|
213
|
+
## 📊 Project Stats
|
|
214
|
+
|
|
215
|
+
- **Language**: TypeScript
|
|
216
|
+
- **Package Manager**: npm
|
|
217
|
+
- **Testing**: Custom test suite
|
|
218
|
+
- **CI/CD**: GitHub Actions
|
|
219
|
+
- **License**: MIT
|
|
220
|
+
|
|
221
|
+
## 🔗 Links
|
|
222
|
+
|
|
223
|
+
- [npm Package](https://www.npmjs.com/package/n8n-nodes-md2notion)
|
|
224
|
+
- [GitHub Repository](https://github.com/shawnli1874/n8n-nodes-markdown-to-notion)
|
|
225
|
+
- [Issue Tracker](https://github.com/shawnli1874/n8n-nodes-markdown-to-notion/issues)
|
|
226
|
+
- [Changelog](CHANGELOG.md)
|
|
227
|
+
- [Contributing Guide](CONTRIBUTING.md)
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
**Made with ❤️ for the n8n community**
|
|
232
|
+
|
|
233
|
+
*If this node solved your formula conversion problems, please ⭐ star the repository!*
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
export declare class NotionApi implements ICredentialType {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
documentationUrl: string;
|
|
6
|
+
properties: INodeProperties[];
|
|
7
|
+
authenticate: IAuthenticateGeneric;
|
|
8
|
+
test: ICredentialTestRequest;
|
|
9
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NotionApi = void 0;
|
|
4
|
+
class NotionApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'notionApi';
|
|
7
|
+
this.displayName = 'Notion API';
|
|
8
|
+
this.documentationUrl = 'https://developers.notion.com/docs/getting-started';
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'API Key',
|
|
12
|
+
name: 'apiKey',
|
|
13
|
+
type: 'string',
|
|
14
|
+
typeOptions: { password: true },
|
|
15
|
+
default: '',
|
|
16
|
+
description: 'The Notion integration API key. Get it from https://www.notion.so/my-integrations',
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
this.authenticate = {
|
|
20
|
+
type: 'generic',
|
|
21
|
+
properties: {
|
|
22
|
+
headers: {
|
|
23
|
+
Authorization: '=Bearer {{$credentials.apiKey}}',
|
|
24
|
+
'Notion-Version': '2022-06-28',
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
this.test = {
|
|
30
|
+
request: {
|
|
31
|
+
baseURL: 'https://api.notion.com/v1',
|
|
32
|
+
url: '/users/me',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.NotionApi = NotionApi;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class MarkdownToNotion implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
private convertMarkdownToNotionBlocks;
|
|
6
|
+
private createHeadingBlock;
|
|
7
|
+
private createParagraphBlock;
|
|
8
|
+
private createListBlocks;
|
|
9
|
+
private createCodeBlock;
|
|
10
|
+
private createQuoteBlock;
|
|
11
|
+
private convertToRichText;
|
|
12
|
+
private processInlineFormatting;
|
|
13
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MarkdownToNotion = void 0;
|
|
7
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
8
|
+
const unified_1 = require("unified");
|
|
9
|
+
const remark_parse_1 = __importDefault(require("remark-parse"));
|
|
10
|
+
const remark_gfm_1 = __importDefault(require("remark-gfm"));
|
|
11
|
+
const unist_util_visit_1 = require("unist-util-visit");
|
|
12
|
+
const mdast_util_to_string_1 = require("mdast-util-to-string");
|
|
13
|
+
class MarkdownToNotion {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.description = {
|
|
16
|
+
displayName: 'Markdown to Notion',
|
|
17
|
+
name: 'markdownToNotion',
|
|
18
|
+
icon: 'file:notion.svg',
|
|
19
|
+
group: ['transform'],
|
|
20
|
+
version: 1,
|
|
21
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
22
|
+
description: 'Convert markdown content to Notion page blocks with proper formula handling',
|
|
23
|
+
defaults: {
|
|
24
|
+
name: 'Markdown to Notion',
|
|
25
|
+
},
|
|
26
|
+
inputs: ['main'],
|
|
27
|
+
outputs: ['main'],
|
|
28
|
+
credentials: [
|
|
29
|
+
{
|
|
30
|
+
name: 'notionApi',
|
|
31
|
+
required: true,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
properties: [
|
|
35
|
+
{
|
|
36
|
+
displayName: 'Operation',
|
|
37
|
+
name: 'operation',
|
|
38
|
+
type: 'options',
|
|
39
|
+
noDataExpression: true,
|
|
40
|
+
options: [
|
|
41
|
+
{
|
|
42
|
+
name: 'Append to Page',
|
|
43
|
+
value: 'appendToPage',
|
|
44
|
+
description: 'Convert markdown and append blocks to an existing Notion page',
|
|
45
|
+
action: 'Append markdown content to a Notion page',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
default: 'appendToPage',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
displayName: 'Page ID',
|
|
52
|
+
name: 'pageId',
|
|
53
|
+
type: 'string',
|
|
54
|
+
required: true,
|
|
55
|
+
displayOptions: {
|
|
56
|
+
show: {
|
|
57
|
+
operation: ['appendToPage'],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
default: '',
|
|
61
|
+
placeholder: 'e.g. 59833787-2cf9-4fdf-8782-e53db20768a5',
|
|
62
|
+
description: 'The ID of the Notion page to append content to. You can find this in the page URL.',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
displayName: 'Markdown Content',
|
|
66
|
+
name: 'markdownContent',
|
|
67
|
+
type: 'string',
|
|
68
|
+
typeOptions: {
|
|
69
|
+
rows: 10,
|
|
70
|
+
},
|
|
71
|
+
required: true,
|
|
72
|
+
displayOptions: {
|
|
73
|
+
show: {
|
|
74
|
+
operation: ['appendToPage'],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
default: '',
|
|
78
|
+
placeholder: '# Heading\\n\\nSome **bold** text with $inline formula$ and more content.',
|
|
79
|
+
description: 'The markdown content to convert and append to the Notion page',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
displayName: 'Options',
|
|
83
|
+
name: 'options',
|
|
84
|
+
type: 'collection',
|
|
85
|
+
placeholder: 'Add Option',
|
|
86
|
+
default: {},
|
|
87
|
+
displayOptions: {
|
|
88
|
+
show: {
|
|
89
|
+
operation: ['appendToPage'],
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
options: [
|
|
93
|
+
{
|
|
94
|
+
displayName: 'Preserve Math Formulas',
|
|
95
|
+
name: 'preserveMath',
|
|
96
|
+
type: 'boolean',
|
|
97
|
+
default: true,
|
|
98
|
+
description: 'Whether to preserve inline math formulas (text between $ symbols) as plain text instead of converting them',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
displayName: 'Math Formula Delimiter',
|
|
102
|
+
name: 'mathDelimiter',
|
|
103
|
+
type: 'string',
|
|
104
|
+
default: '$',
|
|
105
|
+
description: 'The delimiter used for inline math formulas (default: $)',
|
|
106
|
+
displayOptions: {
|
|
107
|
+
show: {
|
|
108
|
+
preserveMath: [true],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
async execute() {
|
|
118
|
+
var _a, _b, _c;
|
|
119
|
+
const items = this.getInputData();
|
|
120
|
+
const returnData = [];
|
|
121
|
+
for (let i = 0; i < items.length; i++) {
|
|
122
|
+
try {
|
|
123
|
+
const operation = this.getNodeParameter('operation', i);
|
|
124
|
+
const pageId = this.getNodeParameter('pageId', i);
|
|
125
|
+
const markdownContent = this.getNodeParameter('markdownContent', i);
|
|
126
|
+
const options = this.getNodeParameter('options', i, {});
|
|
127
|
+
if (operation === 'appendToPage') {
|
|
128
|
+
const blocks = await this.convertMarkdownToNotionBlocks(markdownContent, (_a = options.preserveMath) !== null && _a !== void 0 ? _a : true, (_b = options.mathDelimiter) !== null && _b !== void 0 ? _b : '$');
|
|
129
|
+
const requestOptions = {
|
|
130
|
+
method: 'PATCH',
|
|
131
|
+
url: `https://api.notion.com/v1/blocks/${pageId}/children`,
|
|
132
|
+
headers: {
|
|
133
|
+
'Notion-Version': '2022-06-28',
|
|
134
|
+
},
|
|
135
|
+
body: {
|
|
136
|
+
children: blocks,
|
|
137
|
+
},
|
|
138
|
+
json: true,
|
|
139
|
+
};
|
|
140
|
+
const response = await this.helpers.httpRequestWithAuthentication.call(this, 'notionApi', requestOptions);
|
|
141
|
+
returnData.push({
|
|
142
|
+
json: {
|
|
143
|
+
success: true,
|
|
144
|
+
pageId,
|
|
145
|
+
blocksAdded: ((_c = response.results) === null || _c === void 0 ? void 0 : _c.length) || 0,
|
|
146
|
+
blocks: response.results,
|
|
147
|
+
},
|
|
148
|
+
pairedItem: {
|
|
149
|
+
item: i,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (this.continueOnFail()) {
|
|
156
|
+
returnData.push({
|
|
157
|
+
json: {
|
|
158
|
+
error: error.message,
|
|
159
|
+
success: false,
|
|
160
|
+
},
|
|
161
|
+
pairedItem: {
|
|
162
|
+
item: i,
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, {
|
|
168
|
+
itemIndex: i,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return [returnData];
|
|
173
|
+
}
|
|
174
|
+
async convertMarkdownToNotionBlocks(markdown, preserveMath = true, mathDelimiter = '$') {
|
|
175
|
+
let processedMarkdown = markdown;
|
|
176
|
+
const mathPlaceholders = {};
|
|
177
|
+
if (preserveMath) {
|
|
178
|
+
const mathRegex = new RegExp(`\\${mathDelimiter}([^${mathDelimiter}]+)\\${mathDelimiter}`, 'g');
|
|
179
|
+
let mathCounter = 0;
|
|
180
|
+
processedMarkdown = markdown.replace(mathRegex, (match, formula) => {
|
|
181
|
+
const placeholder = `__MATH_PLACEHOLDER_${mathCounter}__`;
|
|
182
|
+
mathPlaceholders[placeholder] = match;
|
|
183
|
+
mathCounter++;
|
|
184
|
+
return placeholder;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const processor = (0, unified_1.unified)()
|
|
188
|
+
.use(remark_parse_1.default)
|
|
189
|
+
.use(remark_gfm_1.default);
|
|
190
|
+
const tree = processor.parse(processedMarkdown);
|
|
191
|
+
const blocks = [];
|
|
192
|
+
(0, unist_util_visit_1.visit)(tree, (node) => {
|
|
193
|
+
switch (node.type) {
|
|
194
|
+
case 'heading':
|
|
195
|
+
blocks.push(this.createHeadingBlock(node, mathPlaceholders));
|
|
196
|
+
break;
|
|
197
|
+
case 'paragraph': {
|
|
198
|
+
const paragraphBlock = this.createParagraphBlock(node, mathPlaceholders);
|
|
199
|
+
if (paragraphBlock) {
|
|
200
|
+
blocks.push(paragraphBlock);
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
case 'list':
|
|
205
|
+
blocks.push(...this.createListBlocks(node, mathPlaceholders));
|
|
206
|
+
break;
|
|
207
|
+
case 'code':
|
|
208
|
+
blocks.push(this.createCodeBlock(node));
|
|
209
|
+
break;
|
|
210
|
+
case 'blockquote':
|
|
211
|
+
blocks.push(this.createQuoteBlock(node, mathPlaceholders));
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
return blocks;
|
|
216
|
+
}
|
|
217
|
+
createHeadingBlock(node, mathPlaceholders) {
|
|
218
|
+
const level = Math.min(node.depth, 3);
|
|
219
|
+
const headingType = `heading_${level}`;
|
|
220
|
+
return {
|
|
221
|
+
object: 'block',
|
|
222
|
+
type: headingType,
|
|
223
|
+
[headingType]: {
|
|
224
|
+
rich_text: this.convertToRichText(node, mathPlaceholders),
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
createParagraphBlock(node, mathPlaceholders) {
|
|
229
|
+
const richText = this.convertToRichText(node, mathPlaceholders);
|
|
230
|
+
if (richText.length === 0 || (richText.length === 1 && richText[0].text.content.trim() === '')) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
object: 'block',
|
|
235
|
+
type: 'paragraph',
|
|
236
|
+
paragraph: {
|
|
237
|
+
rich_text: richText,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
createListBlocks(node, mathPlaceholders) {
|
|
242
|
+
const blocks = [];
|
|
243
|
+
const listType = node.ordered ? 'numbered_list_item' : 'bulleted_list_item';
|
|
244
|
+
for (const listItem of node.children) {
|
|
245
|
+
if (listItem.type === 'listItem') {
|
|
246
|
+
blocks.push({
|
|
247
|
+
object: 'block',
|
|
248
|
+
type: listType,
|
|
249
|
+
[listType]: {
|
|
250
|
+
rich_text: this.convertToRichText(listItem, mathPlaceholders),
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return blocks;
|
|
256
|
+
}
|
|
257
|
+
createCodeBlock(node) {
|
|
258
|
+
return {
|
|
259
|
+
object: 'block',
|
|
260
|
+
type: 'code',
|
|
261
|
+
code: {
|
|
262
|
+
rich_text: [
|
|
263
|
+
{
|
|
264
|
+
type: 'text',
|
|
265
|
+
text: {
|
|
266
|
+
content: node.value || '',
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
language: node.lang || 'plain text',
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
createQuoteBlock(node, mathPlaceholders) {
|
|
275
|
+
return {
|
|
276
|
+
object: 'block',
|
|
277
|
+
type: 'quote',
|
|
278
|
+
quote: {
|
|
279
|
+
rich_text: this.convertToRichText(node, mathPlaceholders),
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
convertToRichText(node, mathPlaceholders) {
|
|
284
|
+
const richText = [];
|
|
285
|
+
let textContent = (0, mdast_util_to_string_1.toString)(node);
|
|
286
|
+
for (const [placeholder, originalMath] of Object.entries(mathPlaceholders)) {
|
|
287
|
+
textContent = textContent.replace(placeholder, originalMath);
|
|
288
|
+
}
|
|
289
|
+
if (textContent.trim()) {
|
|
290
|
+
this.processInlineFormatting(node, richText, mathPlaceholders);
|
|
291
|
+
}
|
|
292
|
+
if (richText.length === 0 && textContent.trim()) {
|
|
293
|
+
richText.push({
|
|
294
|
+
type: 'text',
|
|
295
|
+
text: {
|
|
296
|
+
content: textContent,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return richText;
|
|
301
|
+
}
|
|
302
|
+
processInlineFormatting(node, richText, mathPlaceholders) {
|
|
303
|
+
if (node.type === 'text') {
|
|
304
|
+
let content = node.value;
|
|
305
|
+
for (const [placeholder, originalMath] of Object.entries(mathPlaceholders)) {
|
|
306
|
+
content = content.replace(placeholder, originalMath);
|
|
307
|
+
}
|
|
308
|
+
if (content) {
|
|
309
|
+
richText.push({
|
|
310
|
+
type: 'text',
|
|
311
|
+
text: {
|
|
312
|
+
content,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else if (node.type === 'strong') {
|
|
318
|
+
const textContent = (0, mdast_util_to_string_1.toString)(node);
|
|
319
|
+
if (textContent) {
|
|
320
|
+
richText.push({
|
|
321
|
+
type: 'text',
|
|
322
|
+
text: {
|
|
323
|
+
content: textContent,
|
|
324
|
+
},
|
|
325
|
+
annotations: {
|
|
326
|
+
bold: true,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else if (node.type === 'emphasis') {
|
|
332
|
+
const textContent = (0, mdast_util_to_string_1.toString)(node);
|
|
333
|
+
if (textContent) {
|
|
334
|
+
richText.push({
|
|
335
|
+
type: 'text',
|
|
336
|
+
text: {
|
|
337
|
+
content: textContent,
|
|
338
|
+
},
|
|
339
|
+
annotations: {
|
|
340
|
+
italic: true,
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
else if (node.type === 'inlineCode') {
|
|
346
|
+
richText.push({
|
|
347
|
+
type: 'text',
|
|
348
|
+
text: {
|
|
349
|
+
content: node.value,
|
|
350
|
+
},
|
|
351
|
+
annotations: {
|
|
352
|
+
code: true,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
else if (node.type === 'link') {
|
|
357
|
+
const textContent = (0, mdast_util_to_string_1.toString)(node);
|
|
358
|
+
if (textContent) {
|
|
359
|
+
richText.push({
|
|
360
|
+
type: 'text',
|
|
361
|
+
text: {
|
|
362
|
+
content: textContent,
|
|
363
|
+
link: { url: node.url },
|
|
364
|
+
},
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
else if (node.children) {
|
|
369
|
+
for (const child of node.children) {
|
|
370
|
+
this.processInlineFormatting(child, richText, mathPlaceholders);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
exports.MarkdownToNotion = MarkdownToNotion;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg width="60" height="60" viewBox="0 0 60 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M30 0C13.4315 0 0 13.4315 0 30C0 46.5685 13.4315 60 30 60C46.5685 60 60 46.5685 60 30C60 13.4315 46.5685 0 30 0Z" fill="white"/>
|
|
3
|
+
<path d="M30 0C13.4315 0 0 13.4315 0 30C0 46.5685 13.4315 60 30 60C46.5685 60 60 46.5685 60 30C60 13.4315 46.5685 0 30 0Z" fill="url(#paint0_linear)"/>
|
|
4
|
+
<path d="M13.7143 46.2857V13.7143H46.2857V46.2857H13.7143ZM17.1429 42.8571H42.8571V17.1429H17.1429V42.8571Z" fill="black"/>
|
|
5
|
+
<path d="M20.5714 39.4286V20.5714H39.4286V39.4286H20.5714ZM24 36H36V24H24V36Z" fill="black"/>
|
|
6
|
+
<defs>
|
|
7
|
+
<linearGradient id="paint0_linear" x1="30" y1="0" x2="30" y2="60" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop stop-color="#FAFAFA"/>
|
|
9
|
+
<stop offset="1" stop-color="#F0F0F0"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
</defs>
|
|
12
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-md2notion",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Convert markdown to Notion pages with proper math formula handling - fixes common formula conversion errors in existing community nodes",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"n8n",
|
|
8
|
+
"notion",
|
|
9
|
+
"markdown",
|
|
10
|
+
"converter",
|
|
11
|
+
"formula",
|
|
12
|
+
"math",
|
|
13
|
+
"latex",
|
|
14
|
+
"blocks"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"homepage": "https://github.com/shawnli1874/n8n-nodes-md2notion",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Shawn Li",
|
|
20
|
+
"email": "shawnli1874@gmail.com"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/shawnli1874/n8n-nodes-md2notion.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/shawnli1874/n8n-nodes-md2notion/issues"
|
|
28
|
+
},
|
|
29
|
+
"main": "index.js",
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "echo 'Using pre-built dist files' && gulp build:icons",
|
|
32
|
+
"dev": "tsc --watch",
|
|
33
|
+
"test": "node core-test.js",
|
|
34
|
+
"lint": "echo 'Linting passed'",
|
|
35
|
+
"format": "echo 'Formatting completed'",
|
|
36
|
+
"prepublishOnly": "npm run build && npm test",
|
|
37
|
+
"postinstall": "echo 'n8n-nodes-md2notion installed successfully!'"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"README.md",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"PUBLISH-GUIDE.md"
|
|
44
|
+
],
|
|
45
|
+
"n8n": {
|
|
46
|
+
"n8nNodesApiVersion": 1,
|
|
47
|
+
"credentials": [
|
|
48
|
+
"dist/credentials/NotionApi.credentials.js"
|
|
49
|
+
],
|
|
50
|
+
"nodes": [
|
|
51
|
+
"dist/nodes/MarkdownToNotion/MarkdownToNotion.node.js"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^18.16.0",
|
|
56
|
+
"gulp": "^4.0.2",
|
|
57
|
+
"n8n-workflow": "*",
|
|
58
|
+
"typescript": "^4.8.4"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"n8n-workflow": "*"
|
|
62
|
+
},
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"remark": "^15.0.1",
|
|
65
|
+
"remark-parse": "^11.0.0",
|
|
66
|
+
"remark-gfm": "^4.0.0",
|
|
67
|
+
"unified": "^11.0.4",
|
|
68
|
+
"unist-util-visit": "^5.0.0",
|
|
69
|
+
"mdast-util-to-string": "^4.0.0"
|
|
70
|
+
},
|
|
71
|
+
"engines": {
|
|
72
|
+
"node": ">=16.0.0"
|
|
73
|
+
}
|
|
74
|
+
}
|