@moxn/kb-migrate 0.4.3 → 0.4.6
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/dist/sources/notion-blocks.js +128 -52
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ export async function blocksToSections(blocks, client, pagePathMap, options) {
|
|
|
23
23
|
if (block.type === 'heading_2') {
|
|
24
24
|
// Flush current section if it has content
|
|
25
25
|
if (currentBlocks.length > 0) {
|
|
26
|
-
sections.push({ name: currentSectionName, content: currentBlocks });
|
|
26
|
+
sections.push({ name: currentSectionName, content: mergeConsecutiveListBlocks(currentBlocks) });
|
|
27
27
|
}
|
|
28
28
|
const h2 = block;
|
|
29
29
|
currentSectionName = richTextToPlain(h2.heading_2.rich_text) || 'Untitled';
|
|
@@ -36,7 +36,7 @@ export async function blocksToSections(blocks, client, pagePathMap, options) {
|
|
|
36
36
|
}
|
|
37
37
|
// Flush last section
|
|
38
38
|
if (currentBlocks.length > 0) {
|
|
39
|
-
sections.push({ name: currentSectionName, content: currentBlocks });
|
|
39
|
+
sections.push({ name: currentSectionName, content: mergeConsecutiveListBlocks(currentBlocks) });
|
|
40
40
|
}
|
|
41
41
|
// If no sections were created at all (empty page), return empty array
|
|
42
42
|
return sections;
|
|
@@ -74,10 +74,10 @@ async function convertBlock(block, client, pagePathMap, visitedSyncedBlocks) {
|
|
|
74
74
|
results.push(...convertToDo(block));
|
|
75
75
|
break;
|
|
76
76
|
case 'quote':
|
|
77
|
-
results.push(...convertQuote(block));
|
|
77
|
+
results.push(...(await convertQuote(block, client, pagePathMap, visitedSyncedBlocks)));
|
|
78
78
|
break;
|
|
79
79
|
case 'callout':
|
|
80
|
-
results.push(...convertCallout(block));
|
|
80
|
+
results.push(...(await convertCallout(block, client, pagePathMap, visitedSyncedBlocks)));
|
|
81
81
|
break;
|
|
82
82
|
case 'divider':
|
|
83
83
|
results.push(textBlock('---'));
|
|
@@ -140,25 +140,15 @@ async function convertBlock(block, client, pagePathMap, visitedSyncedBlocks) {
|
|
|
140
140
|
console.warn(` Skipping unsupported Notion block type: ${block.type}`);
|
|
141
141
|
break;
|
|
142
142
|
}
|
|
143
|
-
// If block has children (except
|
|
143
|
+
// If block has children (except types that handle their own)
|
|
144
144
|
if (block.has_children &&
|
|
145
|
-
!['table', 'toggle', 'synced_block', 'column_list', 'column'].includes(block.type)) {
|
|
145
|
+
!['table', 'toggle', 'synced_block', 'column_list', 'column', 'quote', 'callout'].includes(block.type)) {
|
|
146
|
+
const indent = ['bulleted_list_item', 'numbered_list_item', 'to_do'].includes(block.type)
|
|
147
|
+
? ' '
|
|
148
|
+
: undefined;
|
|
146
149
|
const children = await client.getBlockChildren(block.id);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Indent child content for list items
|
|
150
|
-
if (['bulleted_list_item', 'numbered_list_item'].includes(block.type)) {
|
|
151
|
-
for (const cb of childBlocks) {
|
|
152
|
-
if (cb.blockType === 'text' && cb.text) {
|
|
153
|
-
cb.text = cb.text
|
|
154
|
-
.split('\n')
|
|
155
|
-
.map((line) => ' ' + line)
|
|
156
|
-
.join('\n');
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
results.push(...childBlocks);
|
|
161
|
-
}
|
|
150
|
+
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, indent);
|
|
151
|
+
results.push(...childBlocks);
|
|
162
152
|
}
|
|
163
153
|
return results;
|
|
164
154
|
}
|
|
@@ -203,18 +193,26 @@ function convertToDo(block) {
|
|
|
203
193
|
const text = richTextToMarkdown(td.to_do.rich_text);
|
|
204
194
|
return [textBlock(`${checkbox} ${text}`)];
|
|
205
195
|
}
|
|
206
|
-
function convertQuote(block) {
|
|
196
|
+
async function convertQuote(block, client, pagePathMap, visitedSyncedBlocks) {
|
|
207
197
|
const q = block;
|
|
208
198
|
const text = richTextToMarkdown(q.quote.rich_text);
|
|
209
|
-
if (!text)
|
|
199
|
+
if (!text && !block.has_children)
|
|
210
200
|
return [];
|
|
211
201
|
const quoted = text
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
202
|
+
? text
|
|
203
|
+
.split('\n')
|
|
204
|
+
.map((line) => '> ' + line)
|
|
205
|
+
.join('\n')
|
|
206
|
+
: '';
|
|
207
|
+
const results = quoted ? [textBlock(quoted)] : [];
|
|
208
|
+
if (block.has_children) {
|
|
209
|
+
const children = await client.getBlockChildren(block.id);
|
|
210
|
+
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, '> ');
|
|
211
|
+
results.push(...childBlocks);
|
|
212
|
+
}
|
|
213
|
+
return results;
|
|
216
214
|
}
|
|
217
|
-
function convertCallout(block) {
|
|
215
|
+
async function convertCallout(block, client, pagePathMap, visitedSyncedBlocks) {
|
|
218
216
|
const c = block;
|
|
219
217
|
const text = richTextToMarkdown(c.callout.rich_text);
|
|
220
218
|
const emoji = c.callout.icon?.emoji ?? '';
|
|
@@ -223,7 +221,13 @@ function convertCallout(block) {
|
|
|
223
221
|
.split('\n')
|
|
224
222
|
.map((line) => '> ' + line)
|
|
225
223
|
.join('\n');
|
|
226
|
-
|
|
224
|
+
const results = [textBlock(quoted)];
|
|
225
|
+
if (block.has_children) {
|
|
226
|
+
const children = await client.getBlockChildren(block.id);
|
|
227
|
+
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, '> ');
|
|
228
|
+
results.push(...childBlocks);
|
|
229
|
+
}
|
|
230
|
+
return results;
|
|
227
231
|
}
|
|
228
232
|
async function convertTable(block, client) {
|
|
229
233
|
const tableBlock = block;
|
|
@@ -255,19 +259,8 @@ async function convertToggle(block, client, pagePathMap, visitedSyncedBlocks) {
|
|
|
255
259
|
const results = [textBlock(`**${header}**`)];
|
|
256
260
|
if (block.has_children) {
|
|
257
261
|
const children = await client.getBlockChildren(block.id);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
// Indent toggle content
|
|
261
|
-
for (const cb of converted) {
|
|
262
|
-
if (cb.blockType === 'text' && cb.text) {
|
|
263
|
-
cb.text = cb.text
|
|
264
|
-
.split('\n')
|
|
265
|
-
.map((line) => '> ' + line)
|
|
266
|
-
.join('\n');
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
results.push(...converted);
|
|
270
|
-
}
|
|
262
|
+
const childBlocks = await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, '> ');
|
|
263
|
+
results.push(...childBlocks);
|
|
271
264
|
}
|
|
272
265
|
return results;
|
|
273
266
|
}
|
|
@@ -372,12 +365,7 @@ async function convertSyncedBlock(block, client, pagePathMap, visitedSyncedBlock
|
|
|
372
365
|
visitedSyncedBlocks.add(sourceId);
|
|
373
366
|
try {
|
|
374
367
|
const children = await client.getBlockChildren(sourceId);
|
|
375
|
-
|
|
376
|
-
for (const child of children) {
|
|
377
|
-
const converted = await convertBlock(child, client, pagePathMap, visitedSyncedBlocks);
|
|
378
|
-
results.push(...converted);
|
|
379
|
-
}
|
|
380
|
-
return results;
|
|
368
|
+
return await convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks);
|
|
381
369
|
}
|
|
382
370
|
finally {
|
|
383
371
|
visitedSyncedBlocks.delete(sourceId);
|
|
@@ -389,10 +377,8 @@ async function convertColumnList(block, client, pagePathMap, visitedSyncedBlocks
|
|
|
389
377
|
for (const column of children) {
|
|
390
378
|
if (column.type === 'column' && column.has_children) {
|
|
391
379
|
const columnChildren = await client.getBlockChildren(column.id);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
results.push(...converted);
|
|
395
|
-
}
|
|
380
|
+
const converted = await convertAndMergeChildren(columnChildren, client, pagePathMap, visitedSyncedBlocks);
|
|
381
|
+
results.push(...converted);
|
|
396
382
|
}
|
|
397
383
|
}
|
|
398
384
|
return results;
|
|
@@ -469,6 +455,96 @@ export function richTextToPlain(richText) {
|
|
|
469
455
|
function textBlock(text) {
|
|
470
456
|
return { blockType: 'text', text };
|
|
471
457
|
}
|
|
458
|
+
function detectListType(text) {
|
|
459
|
+
const firstLine = text.split('\n')[0];
|
|
460
|
+
if (/^\d+\.\s/.test(firstLine))
|
|
461
|
+
return 'ordered';
|
|
462
|
+
if (/^- \[[ x]\]\s/.test(firstLine))
|
|
463
|
+
return 'todo';
|
|
464
|
+
if (/^- /.test(firstLine))
|
|
465
|
+
return 'bullet';
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Merge consecutive list-item text blocks into single text blocks.
|
|
470
|
+
* This ensures the markdown→TipTap parser sees them as one list
|
|
471
|
+
* instead of creating multiple single-item lists.
|
|
472
|
+
*/
|
|
473
|
+
function mergeConsecutiveListBlocks(blocks) {
|
|
474
|
+
const result = [];
|
|
475
|
+
let accumulator = [];
|
|
476
|
+
let currentListType = null;
|
|
477
|
+
function flush() {
|
|
478
|
+
if (accumulator.length === 0)
|
|
479
|
+
return;
|
|
480
|
+
let merged = accumulator.join('\n');
|
|
481
|
+
if (currentListType === 'ordered') {
|
|
482
|
+
// Fix numbering: replace all leading `1.` with sequential numbers
|
|
483
|
+
let counter = 0;
|
|
484
|
+
merged = merged
|
|
485
|
+
.split('\n')
|
|
486
|
+
.map((line) => {
|
|
487
|
+
if (/^\d+\.\s/.test(line)) {
|
|
488
|
+
counter++;
|
|
489
|
+
return line.replace(/^\d+\./, `${counter}.`);
|
|
490
|
+
}
|
|
491
|
+
return line;
|
|
492
|
+
})
|
|
493
|
+
.join('\n');
|
|
494
|
+
}
|
|
495
|
+
result.push(textBlock(merged));
|
|
496
|
+
accumulator = [];
|
|
497
|
+
currentListType = null;
|
|
498
|
+
}
|
|
499
|
+
for (const block of blocks) {
|
|
500
|
+
if (block.blockType !== 'text' || !block.text) {
|
|
501
|
+
flush();
|
|
502
|
+
result.push(block);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const type = detectListType(block.text);
|
|
506
|
+
// Indented child content (starts with spaces) continues current list group
|
|
507
|
+
if (currentListType &&
|
|
508
|
+
block.text.split('\n').every((line) => line.startsWith(' '))) {
|
|
509
|
+
accumulator.push(block.text);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (type === null) {
|
|
513
|
+
flush();
|
|
514
|
+
result.push(block);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (type !== currentListType) {
|
|
518
|
+
flush();
|
|
519
|
+
currentListType = type;
|
|
520
|
+
}
|
|
521
|
+
accumulator.push(block.text);
|
|
522
|
+
}
|
|
523
|
+
flush();
|
|
524
|
+
return result;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Convert children blocks, merge consecutive list items, and optionally indent.
|
|
528
|
+
*/
|
|
529
|
+
async function convertAndMergeChildren(children, client, pagePathMap, visitedSyncedBlocks, indent) {
|
|
530
|
+
const blocks = [];
|
|
531
|
+
for (const child of children) {
|
|
532
|
+
const converted = await convertBlock(child, client, pagePathMap, visitedSyncedBlocks);
|
|
533
|
+
blocks.push(...converted);
|
|
534
|
+
}
|
|
535
|
+
const merged = mergeConsecutiveListBlocks(blocks);
|
|
536
|
+
if (indent) {
|
|
537
|
+
for (const block of merged) {
|
|
538
|
+
if (block.blockType === 'text' && block.text) {
|
|
539
|
+
block.text = block.text
|
|
540
|
+
.split('\n')
|
|
541
|
+
.map((line) => indent + line)
|
|
542
|
+
.join('\n');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return merged;
|
|
547
|
+
}
|
|
472
548
|
function guessImageMediaType(url) {
|
|
473
549
|
const lower = url.toLowerCase();
|
|
474
550
|
if (lower.includes('.jpg') || lower.includes('.jpeg'))
|
package/package.json
CHANGED