roam-research-mcp 0.2.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 +329 -0
- package/build/config/environment.js +44 -0
- package/build/index.js +4 -0
- package/build/markdown-utils.js +205 -0
- package/build/server/roam-server.js +108 -0
- package/build/test-addMarkdownText.js +87 -0
- package/build/test-queries.js +116 -0
- package/build/tools/handlers.js +706 -0
- package/build/tools/schemas.js +147 -0
- package/build/types/roam.js +1 -0
- package/build/utils/helpers.js +19 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Ian Shen / 2B3 PRODUCTIONS LLC
|
|
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,329 @@
|
|
|
1
|
+
# Roam Research MCP Server
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/roam-research-mcp)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/2b3pro/roam-research-mcp/blob/main/LICENSE)
|
|
6
|
+
|
|
7
|
+
A Model Context Protocol (MCP) server that provides comprehensive access to Roam Research's API functionality. This server enables AI assistants like Claude to interact with your Roam Research graph through a standardized interface.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
You can install the package globally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g roam-research-mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or clone the repository and build from source:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/2b3pro/roam-research-mcp.git
|
|
21
|
+
cd roam-research-mcp
|
|
22
|
+
npm install
|
|
23
|
+
npm run build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
The server provides six powerful tools for interacting with Roam Research:
|
|
29
|
+
|
|
30
|
+
1. `roam_fetch_page_by_title`: Fetch and read a page's content by title, recursively resolving block references up to 4 levels deep
|
|
31
|
+
2. `roam_create_page`: Create new pages with optional content
|
|
32
|
+
3. `roam_create_block`: Create new blocks in a page (defaults to today's daily page)
|
|
33
|
+
4. `roam_import_markdown`: Import nested markdown content under specific blocks
|
|
34
|
+
5. `roam_add_todo`: Add multiple todo items to today's daily page with checkbox syntax
|
|
35
|
+
6. `roam_create_outline`: Create hierarchical outlines with proper nesting and structure
|
|
36
|
+
|
|
37
|
+
## Setup
|
|
38
|
+
|
|
39
|
+
1. Create a Roam Research API token:
|
|
40
|
+
|
|
41
|
+
- Go to your graph settings
|
|
42
|
+
- Navigate to the "API tokens" section
|
|
43
|
+
- Create a new token
|
|
44
|
+
|
|
45
|
+
2. Configure the environment variables:
|
|
46
|
+
You have two options for configuring the required environment variables:
|
|
47
|
+
|
|
48
|
+
Option 1: Using a .env file (Recommended for development)
|
|
49
|
+
Create a `.env` file in the roam-research directory:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
ROAM_API_TOKEN=your-api-token
|
|
53
|
+
ROAM_GRAPH_NAME=your-graph-name
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Option 2: Using MCP settings (Alternative method)
|
|
57
|
+
Add the configuration to your MCP settings file:
|
|
58
|
+
|
|
59
|
+
- For Cline (`~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`):
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"roam-research": {
|
|
65
|
+
"command": "node",
|
|
66
|
+
"args": ["/path/to/roam-research/build/index.js"],
|
|
67
|
+
"env": {
|
|
68
|
+
"ROAM_API_TOKEN": "your-api-token",
|
|
69
|
+
"ROAM_GRAPH_NAME": "your-graph-name"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- For Claude desktop app (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"roam-research": {
|
|
82
|
+
"command": "node",
|
|
83
|
+
"args": ["/path/to/roam-research/build/index.js"],
|
|
84
|
+
"env": {
|
|
85
|
+
"ROAM_API_TOKEN": "your-api-token",
|
|
86
|
+
"ROAM_GRAPH_NAME": "your-graph-name"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Note: The server will first try to load from .env file, then fall back to environment variables from MCP settings.
|
|
94
|
+
|
|
95
|
+
3. Build the server:
|
|
96
|
+
```bash
|
|
97
|
+
cd roam-research
|
|
98
|
+
npm install
|
|
99
|
+
npm run build
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Usage
|
|
103
|
+
|
|
104
|
+
### Fetch Page By Title
|
|
105
|
+
|
|
106
|
+
Fetch and read a page's content with resolved block references:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
use_mcp_tool roam-research roam_fetch_page_by_title {
|
|
110
|
+
"title": "Example Page"
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Returns the page content as markdown with:
|
|
115
|
+
|
|
116
|
+
- Complete hierarchical structure
|
|
117
|
+
- Block references recursively resolved (up to 4 levels deep)
|
|
118
|
+
- Proper indentation for nesting levels
|
|
119
|
+
- Full markdown formatting
|
|
120
|
+
|
|
121
|
+
### Create Page
|
|
122
|
+
|
|
123
|
+
Create a new page with optional content:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
use_mcp_tool roam-research roam_create_page {
|
|
127
|
+
"title": "New Page",
|
|
128
|
+
"content": "Initial content for the page"
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Returns the created page's UID on success.
|
|
133
|
+
|
|
134
|
+
### Create Block
|
|
135
|
+
|
|
136
|
+
Add a new block to a page (defaults to today's daily page if neither page_uid nor title provided):
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
use_mcp_tool roam-research roam_create_block {
|
|
140
|
+
"content": "Block content",
|
|
141
|
+
"page_uid": "optional-target-page-uid",
|
|
142
|
+
"title": "optional-target-page-title"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
You can specify either:
|
|
147
|
+
|
|
148
|
+
- `page_uid`: Direct reference to target page
|
|
149
|
+
- `title`: Name of target page (will be created if it doesn't exist)
|
|
150
|
+
- Neither: Block will be added to today's daily page
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"success": true,
|
|
157
|
+
"block_uid": "created-block-uid",
|
|
158
|
+
"parent_uid": "parent-page-uid"
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Create Outline
|
|
163
|
+
|
|
164
|
+
Create a hierarchical outline with proper nesting and structure:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
use_mcp_tool roam-research roam_create_outline {
|
|
168
|
+
"outline": [
|
|
169
|
+
{
|
|
170
|
+
"text": "I. Top Level",
|
|
171
|
+
"level": 1
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"text": "A. Second Level",
|
|
175
|
+
"level": 2
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"text": "1. Third Level",
|
|
179
|
+
"level": 3
|
|
180
|
+
}
|
|
181
|
+
],
|
|
182
|
+
"page_title_uid": "optional-target-page",
|
|
183
|
+
"block_text_uid": "optional-header-text"
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Features:
|
|
188
|
+
|
|
189
|
+
- Create complex outlines with up to 10 levels of nesting
|
|
190
|
+
- Validate outline structure and content
|
|
191
|
+
- Maintain proper parent-child relationships
|
|
192
|
+
- Optional header block for the outline
|
|
193
|
+
- Defaults to today's daily page if no page specified
|
|
194
|
+
- Efficient batch operations for creating blocks
|
|
195
|
+
|
|
196
|
+
Parameters:
|
|
197
|
+
|
|
198
|
+
- `outline`: Array of outline items, each with:
|
|
199
|
+
- `text`: Content of the outline item (required)
|
|
200
|
+
- `level`: Nesting level (1-10, required)
|
|
201
|
+
- `page_title_uid`: Target page title or UID (optional, defaults to today's page)
|
|
202
|
+
- `block_text_uid`: Header text for the outline (optional)
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
|
|
206
|
+
```json
|
|
207
|
+
{
|
|
208
|
+
"success": true,
|
|
209
|
+
"page_uid": "target-page-uid",
|
|
210
|
+
"parent_uid": "header-block-uid",
|
|
211
|
+
"created_uids": ["uid1", "uid2", ...]
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Add Todo Items
|
|
216
|
+
|
|
217
|
+
Add one or more todo items to today's daily page:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
use_mcp_tool roam-research roam_add_todo {
|
|
221
|
+
"todos": [
|
|
222
|
+
"First todo item",
|
|
223
|
+
"Second todo item",
|
|
224
|
+
"Third todo item"
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Features:
|
|
230
|
+
|
|
231
|
+
- Adds todos with Roam checkbox syntax (`{{TODO}} todo text`)
|
|
232
|
+
- Supports adding multiple todos in a single operation
|
|
233
|
+
- Uses batch actions for efficiency when adding >10 todos
|
|
234
|
+
- Automatically creates today's page if it doesn't exist
|
|
235
|
+
- Adds todos as top-level blocks in sequential order
|
|
236
|
+
|
|
237
|
+
### Import Nested Markdown
|
|
238
|
+
|
|
239
|
+
Import nested markdown content under a specific block:
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
use_mcp_tool roam-research roam_import_markdown {
|
|
243
|
+
"content": "- Item 1\n - Subitem A\n - Subitem B\n- Item 2",
|
|
244
|
+
"page_uid": "optional-page-uid",
|
|
245
|
+
"page_title": "optional-page-title",
|
|
246
|
+
"parent_uid": "optional-parent-block-uid",
|
|
247
|
+
"parent_string": "optional-exact-block-content",
|
|
248
|
+
"order": "first"
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Features:
|
|
253
|
+
|
|
254
|
+
- Import content under specific blocks:
|
|
255
|
+
- Find parent block by UID or exact string match
|
|
256
|
+
- Locate blocks within specific pages by title or UID
|
|
257
|
+
- Defaults to today's page if no page specified
|
|
258
|
+
- Control content placement:
|
|
259
|
+
- Add as first or last child of parent block
|
|
260
|
+
- Preserve hierarchical structure
|
|
261
|
+
- Efficient batch operations for nested content
|
|
262
|
+
- Comprehensive return value:
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"success": true,
|
|
266
|
+
"page_uid": "target-page-uid",
|
|
267
|
+
"parent_uid": "parent-block-uid",
|
|
268
|
+
"created_uids": ["uid1", "uid2", ...]
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Parameters:
|
|
273
|
+
|
|
274
|
+
- `content`: Nested markdown content to import
|
|
275
|
+
- `page_uid`: UID of the page containing the parent block
|
|
276
|
+
- `page_title`: Title of the page containing the parent block (ignored if page_uid provided)
|
|
277
|
+
- `parent_uid`: UID of the parent block to add content under
|
|
278
|
+
- `parent_string`: Exact string content of the parent block (must provide either page_uid or page_title)
|
|
279
|
+
- `order`: Where to add the content ("first" or "last", defaults to "first")
|
|
280
|
+
|
|
281
|
+
## Error Handling
|
|
282
|
+
|
|
283
|
+
The server provides comprehensive error handling for common scenarios:
|
|
284
|
+
|
|
285
|
+
- Configuration errors:
|
|
286
|
+
- Missing API token or graph name
|
|
287
|
+
- Invalid environment variables
|
|
288
|
+
- API errors:
|
|
289
|
+
- Authentication failures
|
|
290
|
+
- Invalid requests
|
|
291
|
+
- Failed operations
|
|
292
|
+
- Tool-specific errors:
|
|
293
|
+
- Page not found (with case-insensitive search)
|
|
294
|
+
- Block not found by string match
|
|
295
|
+
- Invalid markdown format
|
|
296
|
+
- Missing required parameters
|
|
297
|
+
- Invalid outline structure or content
|
|
298
|
+
|
|
299
|
+
Each error response includes:
|
|
300
|
+
|
|
301
|
+
- Standard MCP error code
|
|
302
|
+
- Detailed error message
|
|
303
|
+
- Suggestions for resolution when applicable
|
|
304
|
+
|
|
305
|
+
## Development
|
|
306
|
+
|
|
307
|
+
The server is built with TypeScript and includes:
|
|
308
|
+
|
|
309
|
+
- Environment variable handling with .env support
|
|
310
|
+
- Comprehensive input validation
|
|
311
|
+
- Case-insensitive page title matching
|
|
312
|
+
- Recursive block reference resolution
|
|
313
|
+
- Markdown parsing and conversion
|
|
314
|
+
- Daily page integration
|
|
315
|
+
- Detailed debug logging
|
|
316
|
+
- Efficient batch operations
|
|
317
|
+
- Hierarchical outline creation
|
|
318
|
+
|
|
319
|
+
To modify or extend the server:
|
|
320
|
+
|
|
321
|
+
1. Clone the repository
|
|
322
|
+
2. Install dependencies with `npm install`
|
|
323
|
+
3. Make changes to the source files
|
|
324
|
+
4. Build with `npm run build`
|
|
325
|
+
5. Test locally by configuring environment variables
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
MIT License
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as dotenv from 'dotenv';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
// Get the project root from the script path
|
|
5
|
+
const scriptPath = process.argv[1]; // Full path to the running script
|
|
6
|
+
const projectRoot = dirname(dirname(scriptPath)); // Go up two levels from build/index.js
|
|
7
|
+
// Try to load .env from project root
|
|
8
|
+
const envPath = join(projectRoot, '.env');
|
|
9
|
+
if (existsSync(envPath)) {
|
|
10
|
+
dotenv.config({ path: envPath });
|
|
11
|
+
}
|
|
12
|
+
// Required environment variables
|
|
13
|
+
const API_TOKEN = process.env.ROAM_API_TOKEN;
|
|
14
|
+
const GRAPH_NAME = process.env.ROAM_GRAPH_NAME;
|
|
15
|
+
// Validate environment variables
|
|
16
|
+
if (!API_TOKEN || !GRAPH_NAME) {
|
|
17
|
+
const missingVars = [];
|
|
18
|
+
if (!API_TOKEN)
|
|
19
|
+
missingVars.push('ROAM_API_TOKEN');
|
|
20
|
+
if (!GRAPH_NAME)
|
|
21
|
+
missingVars.push('ROAM_GRAPH_NAME');
|
|
22
|
+
throw new Error(`Missing required environment variables: ${missingVars.join(', ')}\n\n` +
|
|
23
|
+
'Please configure these variables either:\n' +
|
|
24
|
+
'1. In your MCP settings file:\n' +
|
|
25
|
+
' - For Cline: ~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json\n' +
|
|
26
|
+
' - For Claude: ~/Library/Application Support/Claude/claude_desktop_config.json\n\n' +
|
|
27
|
+
' Example configuration:\n' +
|
|
28
|
+
' {\n' +
|
|
29
|
+
' "mcpServers": {\n' +
|
|
30
|
+
' "roam-research": {\n' +
|
|
31
|
+
' "command": "node",\n' +
|
|
32
|
+
' "args": ["/path/to/roam-research/build/index.js"],\n' +
|
|
33
|
+
' "env": {\n' +
|
|
34
|
+
' "ROAM_API_TOKEN": "your-api-token",\n' +
|
|
35
|
+
' "ROAM_GRAPH_NAME": "your-graph-name"\n' +
|
|
36
|
+
' }\n' +
|
|
37
|
+
' }\n' +
|
|
38
|
+
' }\n' +
|
|
39
|
+
' }\n\n' +
|
|
40
|
+
'2. Or in a .env file in the roam-research directory:\n' +
|
|
41
|
+
' ROAM_API_TOKEN=your-api-token\n' +
|
|
42
|
+
' ROAM_GRAPH_NAME=your-graph-name');
|
|
43
|
+
}
|
|
44
|
+
export { API_TOKEN, GRAPH_NAME };
|
package/build/index.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if text has a traditional markdown table
|
|
3
|
+
*/
|
|
4
|
+
function hasMarkdownTable(text) {
|
|
5
|
+
return /^\|([^|]+\|)+\s*$\n\|(\s*:?-+:?\s*\|)+\s*$\n(\|([^|]+\|)+\s*$\n*)+$/.test(text);
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Converts a markdown table to Roam format
|
|
9
|
+
*/
|
|
10
|
+
function convertTableToRoamFormat(text) {
|
|
11
|
+
const lines = text.split('\n')
|
|
12
|
+
.map(line => line.trim())
|
|
13
|
+
.filter(line => line.length > 0);
|
|
14
|
+
const tableRegex = /^\|([^|]+\|)+\s*$\n\|(\s*:?-+:?\s*\|)+\s*$\n(\|([^|]+\|)+\s*$\n*)+/m;
|
|
15
|
+
if (!tableRegex.test(text)) {
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
const rows = lines
|
|
19
|
+
.filter((_, index) => index !== 1)
|
|
20
|
+
.map(line => line.trim()
|
|
21
|
+
.replace(/^\||\|$/g, '')
|
|
22
|
+
.split('|')
|
|
23
|
+
.map(cell => cell.trim()));
|
|
24
|
+
let roamTable = '{{table}}\n';
|
|
25
|
+
// First row becomes column headers
|
|
26
|
+
const headers = rows[0];
|
|
27
|
+
for (let i = 0; i < headers.length; i++) {
|
|
28
|
+
roamTable += `${' '.repeat(i + 1)}- ${headers[i]}\n`;
|
|
29
|
+
}
|
|
30
|
+
// Remaining rows become nested under each column
|
|
31
|
+
for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
|
|
32
|
+
const row = rows[rowIndex];
|
|
33
|
+
for (let colIndex = 0; colIndex < row.length; colIndex++) {
|
|
34
|
+
roamTable += `${' '.repeat(colIndex + 1)}- ${row[colIndex]}\n`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return roamTable.trim();
|
|
38
|
+
}
|
|
39
|
+
function convertAllTables(text) {
|
|
40
|
+
return text.replaceAll(/(^\|([^|]+\|)+\s*$\n\|(\s*:?-+:?\s*\|)+\s*$\n(\|([^|]+\|)+\s*$\n*)+)/gm, (match) => {
|
|
41
|
+
return '\n' + convertTableToRoamFormat(match) + '\n';
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function convertToRoamMarkdown(text) {
|
|
45
|
+
// First handle double asterisks/underscores (bold)
|
|
46
|
+
text = text.replace(/\*\*(.+?)\*\*/g, '**$1**'); // Preserve double asterisks
|
|
47
|
+
// Then handle single asterisks/underscores (italic)
|
|
48
|
+
text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '__$1__'); // Single asterisk to double underscore
|
|
49
|
+
text = text.replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, '__$1__'); // Single underscore to double underscore
|
|
50
|
+
// Handle highlights
|
|
51
|
+
text = text.replace(/==(.+?)==/g, '^^$1^^');
|
|
52
|
+
// Convert tables
|
|
53
|
+
text = convertAllTables(text);
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
function parseMarkdown(markdown) {
|
|
57
|
+
const lines = markdown.split('\n');
|
|
58
|
+
const rootNodes = [];
|
|
59
|
+
const stack = [];
|
|
60
|
+
for (let i = 0; i < lines.length; i++) {
|
|
61
|
+
const line = lines[i];
|
|
62
|
+
const trimmedLine = line.trimEnd();
|
|
63
|
+
// Skip truly empty lines (no spaces)
|
|
64
|
+
if (trimmedLine === '') {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Calculate indentation level (2 spaces = 1 level)
|
|
68
|
+
const indentation = line.match(/^\s*/)?.[0].length ?? 0;
|
|
69
|
+
let level = Math.floor(indentation / 2);
|
|
70
|
+
// Extract content after bullet point or heading
|
|
71
|
+
let content = trimmedLine;
|
|
72
|
+
content = trimmedLine.replace(/^\s*[-*+]\s+/, '');
|
|
73
|
+
if (trimmedLine.startsWith('#') || trimmedLine.includes('{{table}}') || (trimmedLine.startsWith('**') && trimmedLine.endsWith('**'))) {
|
|
74
|
+
// Remove bullet point if it precedes a table marker
|
|
75
|
+
// content = trimmedLine.replace(/^\s*[-*+]\s+/, '');
|
|
76
|
+
level = 0;
|
|
77
|
+
// Reset stack but keep heading/table as parent
|
|
78
|
+
stack.length = 1; // Keep only the heading/table
|
|
79
|
+
}
|
|
80
|
+
else if (stack[0]?.content.startsWith('#') || stack[0]?.content.includes('{{table}}') || (stack[0]?.content.startsWith('**') && stack[0]?.content.endsWith('**'))) {
|
|
81
|
+
// If previous node was a heading or table marker or wrapped in double-asterisks, increase level by 1
|
|
82
|
+
level = Math.max(level, 1);
|
|
83
|
+
// Remove bullet point
|
|
84
|
+
// content = trimmedLine.replace(/^\s*[-*+]\s+/, '');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Remove bullet point
|
|
88
|
+
content = trimmedLine.replace(/^\s*[-*+]\s+/, '');
|
|
89
|
+
}
|
|
90
|
+
// Create new node
|
|
91
|
+
const node = {
|
|
92
|
+
content,
|
|
93
|
+
level,
|
|
94
|
+
children: []
|
|
95
|
+
};
|
|
96
|
+
// Find the appropriate parent for this node based on level
|
|
97
|
+
if (level === 0) {
|
|
98
|
+
rootNodes.push(node);
|
|
99
|
+
stack[0] = node;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// Pop stack until we find the parent level
|
|
103
|
+
while (stack.length > level) {
|
|
104
|
+
stack.pop();
|
|
105
|
+
}
|
|
106
|
+
// Add as child to parent
|
|
107
|
+
if (stack[level - 1]) {
|
|
108
|
+
stack[level - 1].children.push(node);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// If no parent found, treat as root node
|
|
112
|
+
rootNodes.push(node);
|
|
113
|
+
}
|
|
114
|
+
stack[level] = node;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return rootNodes;
|
|
118
|
+
}
|
|
119
|
+
function parseTableRows(lines) {
|
|
120
|
+
const tableNodes = [];
|
|
121
|
+
let currentLevel = -1;
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const trimmedLine = line.trimEnd();
|
|
124
|
+
if (!trimmedLine)
|
|
125
|
+
continue;
|
|
126
|
+
// Calculate indentation level
|
|
127
|
+
const indentation = line.match(/^\s*/)?.[0].length ?? 0;
|
|
128
|
+
const level = Math.floor(indentation / 2);
|
|
129
|
+
// Extract content after bullet point
|
|
130
|
+
const content = trimmedLine.replace(/^\s*[-*+]\s*/, '');
|
|
131
|
+
// Create node for this cell
|
|
132
|
+
const node = {
|
|
133
|
+
content,
|
|
134
|
+
level,
|
|
135
|
+
children: []
|
|
136
|
+
};
|
|
137
|
+
// Track the first level we see to maintain relative nesting
|
|
138
|
+
if (currentLevel === -1) {
|
|
139
|
+
currentLevel = level;
|
|
140
|
+
}
|
|
141
|
+
// Add node to appropriate parent based on level
|
|
142
|
+
if (level === currentLevel) {
|
|
143
|
+
tableNodes.push(node);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
// Find parent by walking back through nodes
|
|
147
|
+
let parent = tableNodes[tableNodes.length - 1];
|
|
148
|
+
while (parent && parent.level < level - 1) {
|
|
149
|
+
parent = parent.children[parent.children.length - 1];
|
|
150
|
+
}
|
|
151
|
+
if (parent) {
|
|
152
|
+
parent.children.push(node);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return tableNodes;
|
|
157
|
+
}
|
|
158
|
+
function generateBlockUid() {
|
|
159
|
+
// Generate a random string of 9 characters (Roam's format)
|
|
160
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_';
|
|
161
|
+
let uid = '';
|
|
162
|
+
for (let i = 0; i < 9; i++) {
|
|
163
|
+
uid += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
164
|
+
}
|
|
165
|
+
return uid;
|
|
166
|
+
}
|
|
167
|
+
function convertNodesToBlocks(nodes) {
|
|
168
|
+
return nodes.map(node => ({
|
|
169
|
+
uid: generateBlockUid(),
|
|
170
|
+
content: node.content,
|
|
171
|
+
children: convertNodesToBlocks(node.children)
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
function convertToRoamActions(nodes, parentUid, order = 'last') {
|
|
175
|
+
// First convert nodes to blocks with UIDs
|
|
176
|
+
const blocks = convertNodesToBlocks(nodes);
|
|
177
|
+
const actions = [];
|
|
178
|
+
// Helper function to recursively create actions
|
|
179
|
+
function createBlockActions(blocks, parentUid, order) {
|
|
180
|
+
for (const block of blocks) {
|
|
181
|
+
// Create the current block
|
|
182
|
+
const action = {
|
|
183
|
+
action: 'create-block',
|
|
184
|
+
location: {
|
|
185
|
+
'parent-uid': parentUid,
|
|
186
|
+
order
|
|
187
|
+
},
|
|
188
|
+
block: {
|
|
189
|
+
uid: block.uid,
|
|
190
|
+
string: block.content
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
actions.push(action);
|
|
194
|
+
// Create child blocks if any
|
|
195
|
+
if (block.children.length > 0) {
|
|
196
|
+
createBlockActions(block.children, block.uid, 'last');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Create all block actions
|
|
201
|
+
createBlockActions(blocks, parentUid, order);
|
|
202
|
+
return actions;
|
|
203
|
+
}
|
|
204
|
+
// Export public functions and types
|
|
205
|
+
export { parseMarkdown, convertToRoamActions, hasMarkdownTable, convertAllTables, convertToRoamMarkdown };
|