@moxn/kb-migrate 0.4.2 → 0.4.3

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.
@@ -429,12 +429,9 @@ export function richTextToMarkdown(richText) {
429
429
  text = '*' + text + '*';
430
430
  if (rt.annotations.strikethrough)
431
431
  text = '~~' + text + '~~';
432
- // Apply link
433
- if (rt.href) {
434
- text = `[${text}](${rt.href})`;
435
- }
436
- else if (rt.type === 'mention' && rt.mention) {
437
- // Handle mention types
432
+ // Handle mentions BEFORE href — Notion sets href on mentions too
433
+ // (as a web URL), but we want notion:// protocol for resolution
434
+ if (rt.type === 'mention' && rt.mention) {
438
435
  if (rt.mention.type === 'user' && rt.mention.user?.name) {
439
436
  text = `@${rt.mention.user.name}`;
440
437
  }
@@ -450,6 +447,9 @@ export function richTextToMarkdown(richText) {
450
447
  text += ` → ${rt.mention.date.end}`;
451
448
  }
452
449
  }
450
+ else if (rt.href) {
451
+ text = `[${text}](${rt.href})`;
452
+ }
453
453
  else if (rt.type === 'equation' && rt.equation) {
454
454
  text = `$${rt.equation.expression}$`;
455
455
  }
@@ -45,8 +45,8 @@ describe('richTextToMarkdown', () => {
45
45
  const result = richTextToMarkdown([rt]);
46
46
  expect(result).toBe('[My Database](notion://abc123de-f456-abc1-23de-f456abc123de)');
47
47
  });
48
- it('renders database mention with href using the href (href takes precedence)', () => {
49
- // When href is set on the rich text, line 641 handles it BEFORE the mention check
48
+ it('renders database mention with href using notion:// (mention takes precedence over href)', () => {
49
+ // Mentions should always use notion:// protocol, even when href is set
50
50
  const rt = {
51
51
  type: 'mention',
52
52
  plain_text: 'My Database',
@@ -65,8 +65,7 @@ describe('richTextToMarkdown', () => {
65
65
  },
66
66
  };
67
67
  const result = richTextToMarkdown([rt]);
68
- // href takes precedence — produces a notion.so link (resolved in post-processing)
69
- expect(result).toBe('[My Database](https://www.notion.so/workspace/abc123de-f456-abc1-23de-f456abc123de)');
68
+ expect(result).toBe('[My Database](notion://abc123de-f456-abc1-23de-f456abc123de)');
70
69
  });
71
70
  });
72
71
  describe('page mentions', () => {
@@ -91,7 +90,7 @@ describe('richTextToMarkdown', () => {
91
90
  const result = richTextToMarkdown([rt]);
92
91
  expect(result).toBe('[My Page](notion://abc123de-f456-abc1-23de-f456abc123de)');
93
92
  });
94
- it('renders page mention with href using the href', () => {
93
+ it('renders page mention with href using notion:// (mention takes precedence over href)', () => {
95
94
  const rt = {
96
95
  type: 'mention',
97
96
  plain_text: 'My Page',
@@ -110,7 +109,7 @@ describe('richTextToMarkdown', () => {
110
109
  },
111
110
  };
112
111
  const result = richTextToMarkdown([rt]);
113
- expect(result).toBe('[My Page](https://www.notion.so/workspace/abc123de-f456-abc1-23de-f456abc123de)');
112
+ expect(result).toBe('[My Page](notion://abc123de-f456-abc1-23de-f456abc123de)');
114
113
  });
115
114
  });
116
115
  describe('other mention types preserved', () => {
@@ -18,6 +18,9 @@ const NOTION_PROTOCOL_RE = /\[([^\]]*)\]\(notion:\/\/([a-f0-9-]{32,36})\)/g;
18
18
  const NOTION_WEB_URL_RE = /\[([^\]]*)\]\(https?:\/\/(?:www\.)?notion\.so\/(?:[a-zA-Z0-9_-]+\/)*([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})(?:[?#][^)]*)?(?:\))/g;
19
19
  // Fallback for IDs without dashes embedded in slug URLs (e.g., ...Page-Title-abc123def456...)
20
20
  const NOTION_WEB_URL_SLUG_RE = /\[([^\]]*)\]\(https?:\/\/(?:www\.)?notion\.so\/(?:[a-zA-Z0-9_-]+\/)*[a-zA-Z0-9-]+-([a-f0-9]{32})(?:[?#][^)]*)?\)/g;
21
+ // Bare 32-char hex on notion.so (e.g., https://www.notion.so/3004bf42cfeb818ebe8fe2f4befdad52)
22
+ // Catches mention hrefs that leak through as web URLs
23
+ const NOTION_WEB_URL_BARE_RE = /\[([^\]]*)\]\(https?:\/\/(?:www\.)?notion\.so\/(?:[a-zA-Z0-9_-]+\/)*([a-f0-9]{32})(?:[?#][^)]*)?\)/g;
21
24
  // 3. Unresolved placeholders from link_to_page conversion
22
25
  const LINK_PLACEHOLDER_RE = /\*\(Link to Notion page: ([a-f0-9-]{32,36})\)\*/g;
23
26
  // 4. Relation property markers
@@ -87,6 +90,21 @@ export function resolveNotionReferences(sections, mapping) {
87
90
  }
88
91
  return `[${displayText}](notion://${nid})`;
89
92
  });
93
+ // Pass 3b: Bare 32-char hex Notion URLs (safety net for mention hrefs)
94
+ text = text.replace(NOTION_WEB_URL_BARE_RE, (_match, displayText, rawId) => {
95
+ const nid = normalizeId(rawId);
96
+ const kbPath = mapping.notionIdToKbPath.get(nid);
97
+ if (kbPath) {
98
+ references.push({
99
+ sectionIndex,
100
+ targetNotionId: nid,
101
+ targetKbPath: kbPath,
102
+ displayText: displayText || kbPath,
103
+ });
104
+ return `[${displayText || kbPath}](${kbPath})`;
105
+ }
106
+ return `[${displayText}](notion://${nid})`;
107
+ });
90
108
  // Pass 4: Unresolved link_to_page placeholders
91
109
  text = text.replace(LINK_PLACEHOLDER_RE, (_match, rawId) => {
92
110
  const nid = normalizeId(rawId);
@@ -120,6 +120,24 @@ describe('resolveNotionReferences', () => {
120
120
  const { sections: resolved } = resolveNotionReferences(sections, m);
121
121
  expect(getText(resolved[0])).toBe('See [link](/docs/my-page)');
122
122
  });
123
+ it('resolves bare 32-char hex Notion URLs (no slug prefix)', () => {
124
+ const sections = [
125
+ textSection('Intro', 'See [Dev Setup](https://www.notion.so/3004bf42cfeb818ebe8fe2f4befdad52)'),
126
+ ];
127
+ const m = mapping({ '3004bf42cfeb818ebe8fe2f4befdad52': '/docs/dev-setup' });
128
+ const { sections: resolved, references } = resolveNotionReferences(sections, m);
129
+ expect(getText(resolved[0])).toBe('See [Dev Setup](/docs/dev-setup)');
130
+ expect(references).toHaveLength(1);
131
+ expect(references[0].targetNotionId).toBe('3004bf42cfeb818ebe8fe2f4befdad52');
132
+ });
133
+ it('normalizes unresolved bare hex Notion URLs to notion://', () => {
134
+ const sections = [
135
+ textSection('Intro', 'See [page](https://www.notion.so/aabbccdd11223344aabbccdd11223344)'),
136
+ ];
137
+ const m = mapping({});
138
+ const { sections: resolved } = resolveNotionReferences(sections, m);
139
+ expect(getText(resolved[0])).toBe('See [page](notion://aabbccdd11223344aabbccdd11223344)');
140
+ });
123
141
  });
124
142
  describe('link_to_page placeholders', () => {
125
143
  it('resolves placeholders to KB path links', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxn/kb-migrate",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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",