@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.
@@ -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 table, toggle, synced_block, column_list which handle their own)
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
- for (const child of children) {
148
- const childBlocks = await convertBlock(child, client, pagePathMap, visitedSyncedBlocks);
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
- .split('\n')
213
- .map((line) => '> ' + line)
214
- .join('\n');
215
- return [textBlock(quoted)];
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
- return [textBlock(quoted)];
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
- for (const child of children) {
259
- const converted = await convertBlock(child, client, pagePathMap, visitedSyncedBlocks);
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
- const results = [];
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
- for (const child of columnChildren) {
393
- const converted = await convertBlock(child, client, pagePathMap, visitedSyncedBlocks);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.3",
3
+ "version": "0.4.6",
4
4
  "description": "Migration tool for importing documents into Moxn Knowledge Base from local files, Notion, Google Docs, and more",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",