@moxn/kb-migrate 0.4.16 → 0.4.18

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.
@@ -187,19 +187,54 @@ async function uploadFileToNotion(client, url, filename, contentType) {
187
187
  /**
188
188
  * Post-process martian output to reconstruct callout blocks.
189
189
  *
190
- * Callouts are stored as blockquotes with an emoji prefix (e.g., `> 💡 tip text`).
191
- * Martian converts these to Notion `quote` blocks. This function detects the emoji
192
- * prefix pattern and converts matching quotes back to native Notion `callout` blocks.
190
+ * Callouts may arrive as either:
191
+ * 1. `quote` blocks (when stored with `> ` prefix) text in children[0].paragraph.rich_text
192
+ * 2. `paragraph` blocks (when stored as plain text) text in paragraph.rich_text
193
+ *
194
+ * In both cases, if the first text segment starts with an emoji followed by a space,
195
+ * the block is converted to a native Notion `callout` with that emoji as the icon.
193
196
  */
194
197
  function reconstructCallouts(blocks) {
195
198
  const emojiPrefixRe = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F?)\s/u;
199
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
196
200
  return blocks.map((block) => {
201
+ // Case 1: paragraph with emoji prefix
202
+ if (block.type === 'paragraph') {
203
+ const richText = block.paragraph?.rich_text;
204
+ if (!richText?.length)
205
+ return block;
206
+ const firstSegment = richText[0];
207
+ if (firstSegment.type !== 'text' || !firstSegment.text?.content)
208
+ return block;
209
+ const match = firstSegment.text.content.match(emojiPrefixRe);
210
+ if (!match)
211
+ return block;
212
+ const emoji = match[1];
213
+ const strippedContent = firstSegment.text.content.slice(match[0].length);
214
+ const calloutRichText = [
215
+ { ...firstSegment, text: { ...firstSegment.text, content: strippedContent } },
216
+ ...richText.slice(1),
217
+ ].filter((rt) => rt.text?.content);
218
+ return {
219
+ object: 'block',
220
+ type: 'callout',
221
+ callout: {
222
+ rich_text: calloutRichText,
223
+ icon: { type: 'emoji', emoji },
224
+ color: 'default',
225
+ },
226
+ };
227
+ }
228
+ // Case 2: quote with emoji prefix in first child paragraph
197
229
  if (block.type !== 'quote')
198
230
  return block;
199
- const richText = block.quote?.rich_text;
200
- if (!richText?.length)
231
+ const children = block.quote?.children;
232
+ if (!children?.length)
233
+ return block;
234
+ const firstChild = children[0];
235
+ if (firstChild.type !== 'paragraph' || !firstChild.paragraph?.rich_text?.length)
201
236
  return block;
202
- const firstSegment = richText[0];
237
+ const firstSegment = firstChild.paragraph.rich_text[0];
203
238
  if (firstSegment.type !== 'text' || !firstSegment.text?.content)
204
239
  return block;
205
240
  const match = firstSegment.text.content.match(emojiPrefixRe);
@@ -207,22 +242,19 @@ function reconstructCallouts(blocks) {
207
242
  return block;
208
243
  const emoji = match[1];
209
244
  const strippedContent = firstSegment.text.content.slice(match[0].length);
210
- const updatedRichText = [
211
- { ...firstSegment, text: { ...firstSegment.text, content: strippedContent }, plain_text: strippedContent },
212
- ...richText.slice(1),
213
- ].filter((rt) => {
214
- const seg = rt;
215
- return seg.text?.content || seg.plain_text;
216
- });
217
- const quoteBlock = block;
245
+ const calloutRichText = [
246
+ { ...firstSegment, text: { ...firstSegment.text, content: strippedContent } },
247
+ ...firstChild.paragraph.rich_text.slice(1),
248
+ ].filter((rt) => rt.text?.content);
249
+ const remainingChildren = children.slice(1);
218
250
  return {
219
251
  object: 'block',
220
252
  type: 'callout',
221
253
  callout: {
222
- rich_text: updatedRichText,
254
+ rich_text: calloutRichText,
223
255
  icon: { type: 'emoji', emoji },
224
256
  color: 'default',
225
- ...(quoteBlock.quote?.children ? { children: quoteBlock.quote.children } : {}),
257
+ ...(remainingChildren.length > 0 ? { children: remainingChildren } : {}),
226
258
  },
227
259
  };
228
260
  });
@@ -265,14 +297,22 @@ async function sectionsToNotionBlocks(sections, options) {
265
297
  allReferences.push(...references);
266
298
  }
267
299
  text = stripInvalidLinks(text);
268
- // Split on standalone --- dividers and emit native Notion divider blocks
269
- const parts = text.split(/\n---\n/);
300
+ // Split on standalone --- dividers and emit native Notion divider blocks.
301
+ // Pad with newlines so --- at start/end of text is also caught.
302
+ const padded = '\n' + text + '\n';
303
+ const rawParts = padded.split('\n---\n');
304
+ const parts = rawParts.map((p, idx) => {
305
+ if (idx === 0)
306
+ p = p.slice(1); // remove leading \n pad
307
+ if (idx === rawParts.length - 1)
308
+ p = p.slice(0, -1); // remove trailing \n pad
309
+ return p;
310
+ });
270
311
  for (let i = 0; i < parts.length; i++) {
271
312
  if (i > 0) {
272
313
  flushText();
273
314
  allBlocks.push({ object: 'block', type: 'divider', divider: {} });
274
315
  }
275
- // Handle --- at start (empty first part) or end (empty last part)
276
316
  const part = parts[i];
277
317
  if (part.trim()) {
278
318
  pendingMarkdown.push(part);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
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",