@im-nobsidian/core 0.1.0 → 0.1.2

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/index.js CHANGED
@@ -366,6 +366,11 @@ var StateDB = class _StateDB {
366
366
  ).run(hash, id);
367
367
  }
368
368
  }
369
+ setNotionLastEdited(id, lastEdited) {
370
+ this.db.prepare(
371
+ "UPDATE sync_state SET notion_last_edited = ?, updated_at = datetime('now') WHERE id = ?"
372
+ ).run(lastEdited, id);
373
+ }
369
374
  delete(id) {
370
375
  this.db.prepare("DELETE FROM sync_state WHERE id = ?").run(id);
371
376
  }
@@ -448,8 +453,13 @@ var StateDB = class _StateDB {
448
453
 
449
454
  // src/notion/property-mapper.ts
450
455
  var DATE_REGEX = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2})?/;
456
+ var WIKILINK_REGEX = /^\[\[(.+?)(?:\|.+?)?\]\]$/;
451
457
  var PropertyMapper = class {
452
458
  schema = /* @__PURE__ */ new Map();
459
+ wikilinkResolver = null;
460
+ setWikilinkResolver(resolver) {
461
+ this.wikilinkResolver = resolver;
462
+ }
453
463
  loadSchema(properties) {
454
464
  this.schema.clear();
455
465
  for (const [name, prop] of Object.entries(properties)) {
@@ -501,9 +511,13 @@ var PropertyMapper = class {
501
511
  case "checkbox":
502
512
  return { checkbox: Boolean(value) };
503
513
  case "date": {
514
+ if (typeof value === "object" && value !== null && "start" in value) {
515
+ const obj = value;
516
+ return { date: { start: obj.start, end: obj.end ?? null } };
517
+ }
504
518
  const dateStr = String(value);
505
519
  if (DATE_REGEX.test(dateStr)) {
506
- return { date: { start: dateStr } };
520
+ return { date: { start: dateStr, end: null } };
507
521
  }
508
522
  return null;
509
523
  }
@@ -515,6 +529,45 @@ var PropertyMapper = class {
515
529
  return { phone_number: String(value) };
516
530
  case "status":
517
531
  return { status: { name: String(value) } };
532
+ case "relation": {
533
+ const items = Array.isArray(value) ? value : [value];
534
+ const ids = [];
535
+ for (const item of items) {
536
+ const str = String(item);
537
+ const match = str.match(WIKILINK_REGEX);
538
+ if (match?.[1] && this.wikilinkResolver) {
539
+ const pageId = this.wikilinkResolver.resolve(match[1]);
540
+ if (pageId) ids.push({ id: pageId });
541
+ } else if (str.match(/^[0-9a-f-]{32,36}$/)) {
542
+ ids.push({ id: str });
543
+ }
544
+ }
545
+ return ids.length > 0 ? { relation: ids } : null;
546
+ }
547
+ case "people": {
548
+ const users = Array.isArray(value) ? value : [value];
549
+ const peopleArr = users.map((u) => {
550
+ const s = String(u);
551
+ if (s.match(/^[0-9a-f-]{32,36}$/)) return { object: "user", id: s };
552
+ return null;
553
+ }).filter(Boolean);
554
+ return peopleArr.length > 0 ? { people: peopleArr } : null;
555
+ }
556
+ case "files": {
557
+ const fileList = Array.isArray(value) ? value : [value];
558
+ const filesArr = fileList.map((f) => {
559
+ if (typeof f === "object" && f !== null && "url" in f) {
560
+ const obj = f;
561
+ return { type: "external", name: obj.name ?? "file", external: { url: obj.url } };
562
+ }
563
+ return {
564
+ type: "external",
565
+ name: String(f),
566
+ external: { url: String(f) }
567
+ };
568
+ });
569
+ return { files: filesArr };
570
+ }
518
571
  default:
519
572
  return { rich_text: [{ text: { content: String(value) } }] };
520
573
  }
@@ -533,7 +586,7 @@ var PropertyMapper = class {
533
586
  }
534
587
  if (typeof value === "string") {
535
588
  if (DATE_REGEX.test(value)) {
536
- return { date: { start: value } };
589
+ return { date: { start: value, end: null } };
537
590
  }
538
591
  if (value.startsWith("http://") || value.startsWith("https://")) {
539
592
  return { url: value };
@@ -560,8 +613,12 @@ var PropertyMapper = class {
560
613
  }
561
614
  case "checkbox":
562
615
  return prop.checkbox;
563
- case "date":
564
- return prop.date ?? null;
616
+ case "date": {
617
+ const dateObj = prop.date;
618
+ if (!dateObj) return null;
619
+ if (dateObj.end) return { start: dateObj.start, end: dateObj.end };
620
+ return dateObj.start;
621
+ }
565
622
  case "url":
566
623
  return prop.url;
567
624
  case "email":
@@ -594,7 +651,14 @@ var PropertyMapper = class {
594
651
  }
595
652
  case "relation": {
596
653
  const rel = prop.relation;
597
- return rel?.map((r) => r.id) ?? [];
654
+ if (!rel || rel.length === 0) return [];
655
+ return rel.map((r) => {
656
+ if (this.wikilinkResolver) {
657
+ const title = this.wikilinkResolver.resolvePageId(r.id);
658
+ if (title) return `[[${title}]]`;
659
+ }
660
+ return r.id;
661
+ });
598
662
  }
599
663
  case "rollup": {
600
664
  const rollup = prop.rollup;
@@ -640,6 +704,7 @@ var NotionClient = class {
640
704
  getInternalClient() {
641
705
  return this.client;
642
706
  }
707
+ // ─── Page CRUD ───
643
708
  async getPage(pageId) {
644
709
  return this.withRateLimit(
645
710
  () => this.client.pages.retrieve({ page_id: pageId })
@@ -658,6 +723,19 @@ var NotionClient = class {
658
723
  })
659
724
  );
660
725
  }
726
+ async createPageWithMarkdown(params) {
727
+ const parent = params.parentType === "database" ? { database_id: params.parentId } : { page_id: params.parentId };
728
+ const properties = params.parentType === "page" ? { title: { title: [{ text: { content: params.title } }] } } : params.properties ?? {
729
+ title: { title: [{ text: { content: params.title } }] }
730
+ };
731
+ return this.withRateLimit(
732
+ () => this.client.pages.create({
733
+ parent,
734
+ properties,
735
+ markdown: params.markdown
736
+ })
737
+ );
738
+ }
661
739
  async updatePageProperties(pageId, properties) {
662
740
  return this.withRateLimit(
663
741
  () => this.client.pages.update({
@@ -666,6 +744,23 @@ var NotionClient = class {
666
744
  })
667
745
  );
668
746
  }
747
+ // ─── Markdown API ───
748
+ async getPageMarkdown(pageId) {
749
+ return this.withRateLimit(() => this.client.pages.retrieveMarkdown({ page_id: pageId }));
750
+ }
751
+ async replacePageMarkdown(pageId, markdown) {
752
+ return this.withRateLimit(
753
+ () => this.client.pages.updateMarkdown({
754
+ page_id: pageId,
755
+ type: "replace_content",
756
+ replace_content: {
757
+ new_str: markdown,
758
+ allow_deleting_content: true
759
+ }
760
+ })
761
+ );
762
+ }
763
+ // ─── Database / DataSource ───
669
764
  async getDatabaseSchema(databaseId) {
670
765
  const db = await this.withRateLimit(
671
766
  () => this.client.databases.retrieve({ database_id: databaseId })
@@ -679,8 +774,8 @@ var NotionClient = class {
679
774
  }
680
775
  async queryDatabase(databaseId, options) {
681
776
  const response = await this.withRateLimit(
682
- () => this.client.databases.query({
683
- database_id: databaseId,
777
+ () => this.client.dataSources.query({
778
+ data_source_id: databaseId,
684
779
  start_cursor: options?.startCursor,
685
780
  page_size: options?.pageSize ?? 100
686
781
  })
@@ -693,6 +788,7 @@ var NotionClient = class {
693
788
  async archivePage(pageId) {
694
789
  await this.withRateLimit(() => this.client.pages.update({ page_id: pageId, archived: true }));
695
790
  }
791
+ // ─── Block CRUD ───
696
792
  async listChildren(blockId, options) {
697
793
  const response = await this.withRateLimit(
698
794
  () => this.client.blocks.children.list({
@@ -731,6 +827,24 @@ var NotionClient = class {
731
827
  async deleteBlock(blockId) {
732
828
  await this.withRateLimit(() => this.client.blocks.delete({ block_id: blockId }));
733
829
  }
830
+ // ─── File Upload ───
831
+ async uploadFile(fileData, filename, contentType) {
832
+ const upload = await this.withRateLimit(
833
+ () => this.client.fileUploads.create({ filename, content_type: contentType })
834
+ );
835
+ const fileUploadId = upload.id;
836
+ await this.withRateLimit(
837
+ () => this.client.fileUploads.send({
838
+ file_upload_id: fileUploadId,
839
+ file: { data: fileData, filename }
840
+ })
841
+ );
842
+ await this.withRateLimit(
843
+ () => this.client.fileUploads.complete({ file_upload_id: fileUploadId })
844
+ );
845
+ return fileUploadId;
846
+ }
847
+ // ─── Search & Navigation ───
734
848
  async search(params) {
735
849
  const response = await this.withRateLimit(
736
850
  () => this.client.search({
@@ -768,11 +882,13 @@ var NotionClient = class {
768
882
  }
769
883
  return all;
770
884
  }
885
+ // ─── Property Extraction ───
771
886
  extractTitle(page) {
772
- for (const prop of Object.values(page.properties)) {
773
- if (prop.type === "title" && "title" in prop) {
774
- const titleArr = prop.title;
775
- if (titleArr[0]?.plain_text) return titleArr[0].plain_text;
887
+ const props = page.properties;
888
+ for (const prop of Object.values(props)) {
889
+ if (prop.type === "title" && prop.title) {
890
+ const joined = prop.title.map((t) => t.plain_text ?? "").join("");
891
+ if (joined) return joined;
776
892
  }
777
893
  }
778
894
  return "\uC81C\uBAA9 \uC5C6\uC74C";
@@ -780,6 +896,7 @@ var NotionClient = class {
780
896
  extractProperties(page) {
781
897
  return this.propertyMapper.fromNotionProperties(page.properties);
782
898
  }
899
+ // ─── Internal ───
783
900
  async withRateLimit(fn) {
784
901
  await this.sema.acquire();
785
902
  try {
@@ -792,7 +909,7 @@ var NotionClient = class {
792
909
  try {
793
910
  return await fn();
794
911
  } catch (error) {
795
- if (isRateLimited(error) && attempt < 5) {
912
+ if (isRetryable(error) && attempt < 5) {
796
913
  const baseDelay = extractRetryAfter(error) ?? 1e3 * Math.pow(2, attempt);
797
914
  const jitter = baseDelay * (0.5 + Math.random() * 0.5);
798
915
  await sleep(jitter);
@@ -802,13 +919,20 @@ var NotionClient = class {
802
919
  }
803
920
  }
804
921
  };
805
- function isRateLimited(error) {
806
- return typeof error === "object" && error !== null && "status" in error && error.status === 429;
922
+ function isRetryable(error) {
923
+ if (typeof error !== "object" || error === null) return false;
924
+ const code = error.code;
925
+ if (code === "notionhq_client_request_timeout" || code === "ECONNRESET" || code === "ETIMEDOUT") {
926
+ return true;
927
+ }
928
+ if (!("status" in error)) return false;
929
+ const status = error.status;
930
+ return status === 429 || status === 502 || status === 503 || status === 504;
807
931
  }
808
932
  function extractRetryAfter(error) {
809
933
  if (typeof error === "object" && error !== null && "headers" in error) {
810
934
  const headers = error.headers;
811
- const retryAfter = headers["retry-after"];
935
+ const retryAfter = headers?.["retry-after"];
812
936
  if (retryAfter) return Number(retryAfter) * 1e3;
813
937
  }
814
938
  return null;
@@ -1187,6 +1311,9 @@ var ChangeDetector = class {
1187
1311
  const syncedRecords = this.stateDb.getByStatus("synced");
1188
1312
  for (const record of syncedRecords) {
1189
1313
  if (!existingPaths.has(record.obsidianPath)) {
1314
+ if (record.fileType === "folder-note" || record.fileType === "folder-only") {
1315
+ if (!record.obsidianPath.endsWith(".md")) continue;
1316
+ }
1190
1317
  changes.push({
1191
1318
  path: record.obsidianPath,
1192
1319
  type: "deleted",
@@ -1314,17 +1441,11 @@ var ConversionPipeline = class {
1314
1441
  return input.content;
1315
1442
  }
1316
1443
  };
1317
- var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---/;
1318
- var WIKILINK_IN_YAML = /\[\[([^\]]+)\]\]/g;
1319
1444
  var FrontmatterExtractor = class {
1320
1445
  name = "FrontmatterExtractor";
1321
1446
  order = 10;
1322
1447
  process(input) {
1323
- const sanitized = input.content.replace(FRONTMATTER_REGEX, (match, yaml) => {
1324
- const fixed = yaml.replace(WIKILINK_IN_YAML, "$1");
1325
- return match.replace(yaml, fixed);
1326
- });
1327
- const { data, content } = matter(sanitized);
1448
+ const { data, content } = matter(input.content);
1328
1449
  return {
1329
1450
  content: content.trim(),
1330
1451
  metadata: {
@@ -1336,21 +1457,41 @@ var FrontmatterExtractor = class {
1336
1457
  };
1337
1458
 
1338
1459
  // src/converter/pre-processors/wikilink.ts
1339
- var WIKILINK_REGEX = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
1460
+ var WIKILINK_REGEX2 = /(?<!!)\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
1340
1461
  var WikilinkResolver = class {
1462
+ constructor(resolve) {
1463
+ this.resolve = resolve;
1464
+ }
1465
+ resolve;
1341
1466
  name = "WikilinkResolver";
1342
1467
  order = 20;
1343
1468
  process(input) {
1469
+ if (input.context.direction !== "push") {
1470
+ return { content: input.content, metadata: input.metadata };
1471
+ }
1472
+ const preserveMarkers = input.metadata.preserveMarkers ? [...input.metadata.preserveMarkers] : [];
1344
1473
  const resolved = input.content.replace(
1345
- WIKILINK_REGEX,
1474
+ WIKILINK_REGEX2,
1346
1475
  (_match, target, display) => {
1347
1476
  const label = display ?? target;
1348
- return `**${label}**`;
1477
+ if (this.resolve) {
1478
+ const entry = this.resolve(target);
1479
+ if (entry) {
1480
+ return `<mention-page id="${entry.notionPageId}">${label}</mention-page>`;
1481
+ }
1482
+ }
1483
+ preserveMarkers.push({
1484
+ type: "wikilink",
1485
+ params: display ? { text: target, display } : { text: target },
1486
+ startIndex: 0
1487
+ });
1488
+ const encodedTarget = encodeURIComponent(target);
1489
+ return `[${label}](im-nobsidian://wikilink/${encodedTarget})`;
1349
1490
  }
1350
1491
  );
1351
1492
  return {
1352
1493
  content: resolved,
1353
- metadata: input.metadata
1494
+ metadata: { ...input.metadata, preserveMarkers }
1354
1495
  };
1355
1496
  }
1356
1497
  };
@@ -1383,13 +1524,17 @@ var CalloutTransformer = class {
1383
1524
  name = "CalloutTransformer";
1384
1525
  order = 30;
1385
1526
  process(input) {
1527
+ if (input.context.path === "markdown-api") {
1528
+ return { content: input.content, metadata: input.metadata };
1529
+ }
1386
1530
  const content = input.content.replace(
1387
1531
  CALLOUT_REGEX,
1388
1532
  (_match, type, foldable, title) => {
1389
1533
  const emoji = CALLOUT_EMOJI_MAP[type.toLowerCase()] ?? "\u{1F4DD}";
1390
1534
  const titleText = title?.trim() || type;
1391
1535
  const foldMeta = foldable ? `
1392
- %% im-nobsidian:callout:type=${type}&foldable=${foldable === "+" ? "open" : "closed"} %%` : "";
1536
+ %% im-nobsidian:callout:type=${type}&foldable=${foldable === "+" ? "open" : "closed"} %%` : `
1537
+ %% im-nobsidian:callout:type=${type} %%`;
1393
1538
  return `> ${emoji} **${titleText}**${foldMeta}`;
1394
1539
  }
1395
1540
  );
@@ -1438,12 +1583,13 @@ var EmbedResolver = class {
1438
1583
  images.push({ url: target, localPath: target, isExternal: false });
1439
1584
  if (isPush && !isExternalUrl(target)) {
1440
1585
  const fileName = target.split("/").pop() ?? target;
1441
- return `> \u{1F4CE} ${fileName} (\uB85C\uCEEC \uC774\uBBF8\uC9C0 \u2014 Notion API \uC81C\uD55C\uC73C\uB85C \uC5C5\uB85C\uB4DC \uBD88\uAC00)
1586
+ return `> \u{1F4CE} ${fileName}
1442
1587
  > %% im-nobsidian:local-image:${target} %%`;
1443
1588
  }
1444
1589
  return `![${target}](${encodeURI(target)})`;
1445
1590
  }
1446
- return `[${target}](${encodeURI(target)})`;
1591
+ const encoded = encodeURIComponent(target);
1592
+ return `[${target}](im-nobsidian://embed/${encoded})`;
1447
1593
  });
1448
1594
  content = content.replace(MARKDOWN_IMAGE_REGEX, (_match, alt, url) => {
1449
1595
  if (isVideoUrl(url)) {
@@ -1519,8 +1665,6 @@ var UnsupportedBlockStripper = class {
1519
1665
  };
1520
1666
  }
1521
1667
  };
1522
-
1523
- // src/converter/pre-processors/properties-table.ts
1524
1668
  var PropertiesTableInjector = class {
1525
1669
  name = "PropertiesTableInjector";
1526
1670
  order = 15;
@@ -1535,32 +1679,20 @@ var PropertiesTableInjector = class {
1535
1679
  if (!properties || Object.keys(properties).length === 0) {
1536
1680
  return { content: input.content, metadata: input.metadata };
1537
1681
  }
1538
- const filteredEntries = Object.entries(properties).filter(([key]) => key !== "title");
1539
- if (filteredEntries.length === 0) {
1682
+ const filteredProps = {};
1683
+ for (const [key, value] of Object.entries(properties)) {
1684
+ filteredProps[key] = value;
1685
+ }
1686
+ if (Object.keys(filteredProps).length === 0) {
1540
1687
  return { content: input.content, metadata: input.metadata };
1541
1688
  }
1542
- const rows = filteredEntries.map(([key, value]) => {
1543
- const display = formatValue(value);
1544
- return `| ${key} | ${display} |`;
1545
- });
1546
- const table = ["| Property | Value |", "| --- | --- |", ...rows].join("\n");
1547
- const content = table + "\n\n---\n\n" + input.content;
1689
+ const yamlStr = matter.stringify("", filteredProps).trim();
1690
+ const yamlBody = yamlStr.slice(4, -3).trim();
1691
+ const codeBlock = "```yaml\n# im-nobsidian:properties\n" + yamlBody + "\n```";
1692
+ const content = codeBlock + "\n\n---\n\n" + input.content;
1548
1693
  return { content, metadata: input.metadata };
1549
1694
  }
1550
1695
  };
1551
- function formatValue(value) {
1552
- if (value === null || value === void 0) return "";
1553
- if (value instanceof Date) {
1554
- return value.toISOString().replace(/T00:00:00(?:\.000)?Z$/, "");
1555
- }
1556
- if (Array.isArray(value)) {
1557
- return value.map((v) => formatValue(v)).join(", ");
1558
- }
1559
- if (typeof value === "object") {
1560
- return JSON.stringify(value);
1561
- }
1562
- return String(value);
1563
- }
1564
1696
 
1565
1697
  // src/converter/pre-processors/html-annotation.ts
1566
1698
  var UNDERLINE_REGEX = /<u>([\s\S]*?)<\/u>/g;
@@ -1582,21 +1714,30 @@ var HtmlAnnotationStripper = class {
1582
1714
  };
1583
1715
 
1584
1716
  // src/converter/post-processors/mention-to-wikilink.ts
1585
- var NOTION_LINK_REGEX = /\[([^\]]+)\]\((?:https:\/\/www\.notion\.so\/|notion:\/\/)([a-f0-9-]+)\)/g;
1717
+ var NOTION_LINK_REGEX = /\[([^\]]+)\]\((?:https:\/\/(?:www\.)?notion\.so\/|notion:\/\/)([a-f0-9-]+)\)/g;
1718
+ var WIKILINK_PRESERVE_REGEX = /\[([^\]]+)\]\(im-nobsidian:\/\/wikilink\/([^)]+)\)/g;
1719
+ var EMBED_PRESERVE_REGEX = /\[([^\]]+)\]\(im-nobsidian:\/\/embed\/([^)]+)\)/g;
1586
1720
  var MentionToWikilink = class {
1587
1721
  name = "MentionToWikilink";
1588
1722
  order = 10;
1589
1723
  process(input) {
1590
- const content = input.content.replace(
1591
- NOTION_LINK_REGEX,
1592
- (_match, display, _pageId) => {
1593
- return `[[${display}]]`;
1724
+ let content = input.content;
1725
+ content = content.replace(NOTION_LINK_REGEX, (_match, display) => `[[${display}]]`);
1726
+ content = content.replace(
1727
+ WIKILINK_PRESERVE_REGEX,
1728
+ (_match, display, encodedTarget) => {
1729
+ const target = decodeURIComponent(encodedTarget);
1730
+ return target === display ? `[[${target}]]` : `[[${target}|${display}]]`;
1594
1731
  }
1595
1732
  );
1596
- return {
1597
- content,
1598
- metadata: input.metadata
1599
- };
1733
+ content = content.replace(
1734
+ EMBED_PRESERVE_REGEX,
1735
+ (_match, _display, encodedTarget) => {
1736
+ const target = decodeURIComponent(encodedTarget);
1737
+ return `![[${target}]]`;
1738
+ }
1739
+ );
1740
+ return { content, metadata: input.metadata };
1600
1741
  }
1601
1742
  };
1602
1743
 
@@ -1643,15 +1784,18 @@ var EMOJI_TYPE_MAP = {
1643
1784
  "\u{1F4CC}": "example",
1644
1785
  "\u{1F4AC}": "quote"
1645
1786
  };
1646
- var CALLOUT_PRESERVE_REGEX = /%% im-nobsidian:callout:type=(\w+)&foldable=(open|closed) %%/g;
1787
+ var CALLOUT_PRESERVE_REGEX = /%% im-nobsidian:callout:type=(\w+)(?:&foldable=(open|closed))? %%/g;
1647
1788
  var CalloutRestorer = class {
1648
1789
  name = "CalloutRestorer";
1649
1790
  order = 20;
1650
1791
  process(input) {
1651
1792
  let content = input.content;
1652
1793
  content = content.replace(CALLOUT_PRESERVE_REGEX, (_match, type, foldable) => {
1653
- const foldChar = foldable === "open" ? "+" : "-";
1654
- return `[!${type}]${foldChar}`;
1794
+ if (foldable) {
1795
+ const foldChar = foldable === "open" ? "+" : "-";
1796
+ return `[!${type}]${foldChar}`;
1797
+ }
1798
+ return `[!${type}]`;
1655
1799
  });
1656
1800
  for (const [emoji, type] of Object.entries(EMOJI_TYPE_MAP)) {
1657
1801
  const pattern = new RegExp(`> ${escapeRegex(emoji)} \\*\\*(.+?)\\*\\*`, "g");
@@ -1699,13 +1843,45 @@ var FrontmatterGenerator = class {
1699
1843
  if (!properties || Object.keys(properties).length === 0) {
1700
1844
  return { content: input.content, metadata: input.metadata };
1701
1845
  }
1702
- const content = matter.stringify(input.content, properties);
1846
+ const normalized = normalizeProperties(properties);
1847
+ const content = matter.stringify(input.content, normalized);
1703
1848
  return {
1704
1849
  content,
1705
1850
  metadata: input.metadata
1706
1851
  };
1707
1852
  }
1708
1853
  };
1854
+ function normalizeProperties(props) {
1855
+ const result = {};
1856
+ for (const [key, value] of Object.entries(props)) {
1857
+ result[key] = normalizeValue(value);
1858
+ }
1859
+ return result;
1860
+ }
1861
+ function normalizeValue(value) {
1862
+ if (typeof value === "string") {
1863
+ return normalizeDate(value);
1864
+ }
1865
+ if (typeof value === "object" && value !== null && "start" in value) {
1866
+ const dateObj = value;
1867
+ const normalized = {
1868
+ start: normalizeDate(dateObj.start),
1869
+ ...dateObj.end ? { end: normalizeDate(dateObj.end) } : {}
1870
+ };
1871
+ return normalized;
1872
+ }
1873
+ return value;
1874
+ }
1875
+ function normalizeDate(dateStr) {
1876
+ if (typeof dateStr !== "string") return dateStr;
1877
+ if (dateStr.match(/T00:00:00\.000[Z+]/)) {
1878
+ return dateStr.split("T")[0] ?? dateStr;
1879
+ }
1880
+ if (dateStr.match(/T00:00:00[Z+]/)) {
1881
+ return dateStr.split("T")[0] ?? dateStr;
1882
+ }
1883
+ return dateStr;
1884
+ }
1709
1885
 
1710
1886
  // src/converter/post-processors/local-image-restorer.ts
1711
1887
  var LOCAL_IMAGE_MARKER_REGEX = />\s*📎\s*[^\n]*\n>\s*%%\s*im-nobsidian:local-image:([^\s]+)\s*%%/g;
@@ -1722,9 +1898,8 @@ var LocalImageRestorer = class {
1722
1898
  return { content, metadata: input.metadata };
1723
1899
  }
1724
1900
  };
1725
-
1726
- // src/converter/post-processors/properties-table-restorer.ts
1727
- var PROPERTIES_TABLE_REGEX = /^\s*\| Property\s*\| Value\s*\|\n\s*\|\s*-{3,}\s*\|\s*-{3,}\s*\|\n((?:\|[^\n]+\|\n?)+)\n*---\n*/;
1901
+ var YAML_PROPERTIES_REGEX = /```yaml\n# im-nobsidian:properties\n([\s\S]*?)```\n*(?:---\n*)?/;
1902
+ var LEGACY_TABLE_REGEX = /^\s*\| Property\s*\| Value\s*\|\n\s*\|\s*-{3,}\s*\|\s*-{3,}\s*\|\n((?:\|[^\n]+\|\n?)+)\n*---\n*/;
1728
1903
  var PropertiesTableRestorer = class {
1729
1904
  name = "PropertiesTableRestorer";
1730
1905
  order = 5;
@@ -1732,10 +1907,39 @@ var PropertiesTableRestorer = class {
1732
1907
  if (input.context.direction !== "pull") {
1733
1908
  return { content: input.content, metadata: input.metadata };
1734
1909
  }
1735
- const match = input.content.match(PROPERTIES_TABLE_REGEX);
1736
- if (!match) {
1910
+ const yamlMatch = input.content.match(YAML_PROPERTIES_REGEX);
1911
+ if (yamlMatch) {
1912
+ return this.restoreFromYaml(input, yamlMatch);
1913
+ }
1914
+ const tableMatch = input.content.match(LEGACY_TABLE_REGEX);
1915
+ if (tableMatch) {
1916
+ return this.restoreFromTable(input, tableMatch);
1917
+ }
1918
+ return { content: input.content, metadata: input.metadata };
1919
+ }
1920
+ restoreFromYaml(input, match) {
1921
+ const yamlContent = match[1];
1922
+ const fakeDocument = `---
1923
+ ${yamlContent}---
1924
+ `;
1925
+ let parsed;
1926
+ try {
1927
+ const result = matter(fakeDocument);
1928
+ parsed = result.data;
1929
+ } catch {
1737
1930
  return { content: input.content, metadata: input.metadata };
1738
1931
  }
1932
+ const properties = {
1933
+ ...input.metadata.properties,
1934
+ ...parsed
1935
+ };
1936
+ const content = input.content.replace(YAML_PROPERTIES_REGEX, "").trimStart();
1937
+ return {
1938
+ content,
1939
+ metadata: { ...input.metadata, properties }
1940
+ };
1941
+ }
1942
+ restoreFromTable(input, match) {
1739
1943
  const rows = match[1].trim().split("\n");
1740
1944
  const properties = {
1741
1945
  ...input.metadata.properties
@@ -1745,31 +1949,21 @@ var PropertiesTableRestorer = class {
1745
1949
  if (cells.length >= 2) {
1746
1950
  const key = cells[0];
1747
1951
  const rawValue = cells[1];
1748
- properties[key] = parseValue(rawValue);
1952
+ properties[key] = parseLegacyValue(rawValue);
1749
1953
  }
1750
1954
  }
1751
- const content = input.content.replace(PROPERTIES_TABLE_REGEX, "").trimStart();
1955
+ const content = input.content.replace(LEGACY_TABLE_REGEX, "").trimStart();
1752
1956
  return {
1753
1957
  content,
1754
1958
  metadata: { ...input.metadata, properties }
1755
1959
  };
1756
1960
  }
1757
1961
  };
1758
- function stripQuotes(s) {
1759
- if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
1760
- return s.slice(1, -1);
1761
- }
1762
- return s;
1763
- }
1764
- var ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?$/;
1765
- function parseValue(raw) {
1962
+ function parseLegacyValue(raw) {
1766
1963
  if (raw === "" || raw === "null") return null;
1767
1964
  if (raw === "true") return true;
1768
1965
  if (raw === "false") return false;
1769
1966
  const unquoted = stripQuotes(raw);
1770
- if (ISO_DATE_REGEX.test(unquoted)) {
1771
- return unquoted.replace(/T00:00:00(?:\.000)?Z?$/, "");
1772
- }
1773
1967
  const num = Number(unquoted);
1774
1968
  if (!isNaN(num) && unquoted.trim() !== "") return num;
1775
1969
  if (unquoted.includes(", ")) {
@@ -1791,15 +1985,21 @@ function parseValue(raw) {
1791
1985
  }
1792
1986
  return unquoted;
1793
1987
  }
1988
+ function stripQuotes(s) {
1989
+ if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
1990
+ return s.slice(1, -1);
1991
+ }
1992
+ return s;
1993
+ }
1794
1994
 
1795
1995
  // src/converter/pipeline-factory.ts
1796
- function createDefaultPipeline() {
1996
+ function createDefaultPipeline(options) {
1797
1997
  const pipeline = new ConversionPipeline();
1798
1998
  pipeline.registerPreProcessor(new HtmlAnnotationStripper());
1799
1999
  pipeline.registerPreProcessor(new UnsupportedBlockStripper());
1800
2000
  pipeline.registerPreProcessor(new FrontmatterExtractor());
1801
2001
  pipeline.registerPreProcessor(new PropertiesTableInjector());
1802
- pipeline.registerPreProcessor(new WikilinkResolver());
2002
+ pipeline.registerPreProcessor(new WikilinkResolver(options?.wikilinkResolver));
1803
2003
  pipeline.registerPreProcessor(new CalloutTransformer());
1804
2004
  pipeline.registerPreProcessor(new MathNormalizer());
1805
2005
  pipeline.registerPreProcessor(new EmbedResolver());
@@ -1920,6 +2120,8 @@ var BlockConverter = class {
1920
2120
  this.registerTocTransformer();
1921
2121
  this.registerBreadcrumbTransformer();
1922
2122
  this.registerLinkPreviewTransformer();
2123
+ this.registerPdfTransformer();
2124
+ this.registerEmbedTransformer();
1923
2125
  this.registerCalloutTransformer();
1924
2126
  this.registerToggleHeadingTransformers();
1925
2127
  }
@@ -2092,6 +2294,24 @@ ${COLUMN_LIST_END}`;
2092
2294
  return `[${url}](${url})`;
2093
2295
  });
2094
2296
  }
2297
+ registerPdfTransformer() {
2298
+ if (!this.n2m) return;
2299
+ this.n2m.setCustomTransformer("pdf", async (block) => {
2300
+ const b = block;
2301
+ const url = b.pdf?.type === "file" ? b.pdf.file?.url ?? "" : b.pdf?.external?.url ?? "";
2302
+ const caption = b.pdf?.caption ? richTextToMarkdown(b.pdf.caption) : "";
2303
+ return `[\u{1F4C4} ${caption || "PDF"}](${url})`;
2304
+ });
2305
+ }
2306
+ registerEmbedTransformer() {
2307
+ if (!this.n2m) return;
2308
+ this.n2m.setCustomTransformer("embed", async (block) => {
2309
+ const b = block;
2310
+ const url = b.embed?.url ?? "";
2311
+ const caption = b.embed?.caption ? richTextToMarkdown(b.embed.caption) : "";
2312
+ return `[${caption || url}](${url})`;
2313
+ });
2314
+ }
2095
2315
  registerCalloutTransformer() {
2096
2316
  if (!this.n2m) return;
2097
2317
  const n2m = this.n2m;
@@ -2127,7 +2347,8 @@ ${COLUMN_LIST_END}`;
2127
2347
  markdownToNotionBlocks(markdown) {
2128
2348
  const preprocessed = this.preProcessMarkdown(markdown);
2129
2349
  const blocks = markdownToBlocks(preprocessed);
2130
- return this.postProcessBlocks(blocks);
2350
+ const processed = this.postProcessBlocks(blocks);
2351
+ return normalizeBlocksForNotion(processed);
2131
2352
  }
2132
2353
  toggleContents = /* @__PURE__ */ new Map();
2133
2354
  columnContents = /* @__PURE__ */ new Map();
@@ -2345,6 +2566,111 @@ ${COLUMN_LIST_END}`;
2345
2566
  return result.parent ?? "";
2346
2567
  }
2347
2568
  };
2569
+ var NOTION_MAX_TABLE_ROWS = 100;
2570
+ var NOTION_MAX_LIST_DEPTH = 3;
2571
+ function normalizeBlocksForNotion(blocks) {
2572
+ const result = [];
2573
+ for (const block of blocks) {
2574
+ if (block.type === "table") {
2575
+ result.push(...normalizeTable(block));
2576
+ } else if (isListBlock(block)) {
2577
+ result.push(flattenListDepth(block, 0));
2578
+ } else {
2579
+ result.push(block);
2580
+ }
2581
+ }
2582
+ return result;
2583
+ }
2584
+ function normalizeTable(block) {
2585
+ const table = block.table;
2586
+ if (!table?.children) return [block];
2587
+ const rows = table.children;
2588
+ if (rows.length === 0) return [block];
2589
+ let maxCells = table.table_width;
2590
+ for (const row of rows) {
2591
+ const tr = row.table_row;
2592
+ if (tr?.cells && tr.cells.length > maxCells) {
2593
+ maxCells = tr.cells.length;
2594
+ }
2595
+ }
2596
+ const normalizedRows = rows.map((row) => {
2597
+ const tr = row.table_row;
2598
+ if (!tr?.cells) return row;
2599
+ const cells = [...tr.cells];
2600
+ while (cells.length < maxCells) {
2601
+ cells.push([{ type: "text", text: { content: "" } }]);
2602
+ }
2603
+ if (cells.length > maxCells) {
2604
+ cells.length = maxCells;
2605
+ }
2606
+ return { ...row, table_row: { ...tr, cells } };
2607
+ });
2608
+ if (normalizedRows.length <= NOTION_MAX_TABLE_ROWS) {
2609
+ return [
2610
+ {
2611
+ ...block,
2612
+ table: { ...table, table_width: maxCells, children: normalizedRows }
2613
+ }
2614
+ ];
2615
+ }
2616
+ const tables = [];
2617
+ const headerRow = table.has_column_header ? normalizedRows[0] : null;
2618
+ const dataRows = table.has_column_header ? normalizedRows.slice(1) : normalizedRows;
2619
+ const chunkSize = headerRow ? NOTION_MAX_TABLE_ROWS - 1 : NOTION_MAX_TABLE_ROWS;
2620
+ for (let i = 0; i < dataRows.length; i += chunkSize) {
2621
+ const chunk = dataRows.slice(i, i + chunkSize);
2622
+ const children = headerRow ? [headerRow, ...chunk] : chunk;
2623
+ tables.push({
2624
+ type: "table",
2625
+ table: {
2626
+ table_width: maxCells,
2627
+ has_column_header: !!headerRow,
2628
+ has_row_header: table.has_row_header,
2629
+ children
2630
+ }
2631
+ });
2632
+ }
2633
+ return tables;
2634
+ }
2635
+ function isListBlock(block) {
2636
+ return block.type === "bulleted_list_item" || block.type === "numbered_list_item" || block.type === "to_do";
2637
+ }
2638
+ function flattenListDepth(block, depth) {
2639
+ const blockType = block.type;
2640
+ const blockData = block[blockType];
2641
+ if (!blockData?.children || blockData.children.length === 0) return block;
2642
+ if (depth >= NOTION_MAX_LIST_DEPTH - 1) {
2643
+ const flattened = collectAllDescendants(blockData.children);
2644
+ return {
2645
+ ...block,
2646
+ [blockType]: { ...blockData, children: flattened }
2647
+ };
2648
+ }
2649
+ const normalizedChildren = blockData.children.map((child) => {
2650
+ if (isListBlock(child)) {
2651
+ return flattenListDepth(child, depth + 1);
2652
+ }
2653
+ return child;
2654
+ });
2655
+ return {
2656
+ ...block,
2657
+ [blockType]: { ...blockData, children: normalizedChildren }
2658
+ };
2659
+ }
2660
+ function collectAllDescendants(blocks) {
2661
+ const result = [];
2662
+ for (const block of blocks) {
2663
+ const blockType = block.type;
2664
+ const blockData = block[blockType];
2665
+ const children = blockData?.children;
2666
+ const blockWithoutChildren = children ? { ...block, [blockType]: { ...blockData, children: void 0 } } : block;
2667
+ result.push(blockWithoutChildren);
2668
+ if (children && children.length > 0) {
2669
+ result.push(...collectAllDescendants(children));
2670
+ }
2671
+ }
2672
+ return result;
2673
+ }
2348
2674
  var IMAGE_EXTENSIONS = {
2349
2675
  "image/png": ".png",
2350
2676
  "image/jpeg": ".jpg",
@@ -2354,13 +2680,26 @@ var IMAGE_EXTENSIONS = {
2354
2680
  "image/bmp": ".bmp",
2355
2681
  "image/tiff": ".tiff"
2356
2682
  };
2683
+ var EXTENSION_TO_MIME = {
2684
+ ".png": "image/png",
2685
+ ".jpg": "image/jpeg",
2686
+ ".jpeg": "image/jpeg",
2687
+ ".gif": "image/gif",
2688
+ ".webp": "image/webp",
2689
+ ".svg": "image/svg+xml",
2690
+ ".bmp": "image/bmp",
2691
+ ".ico": "image/x-icon",
2692
+ ".tiff": "image/tiff"
2693
+ };
2357
2694
  var ImageHandler = class {
2358
- constructor(vaultFs, attachmentFolder = "attachments") {
2695
+ constructor(vaultFs, attachmentFolder = "attachments", notionClient) {
2359
2696
  this.vaultFs = vaultFs;
2360
2697
  this.attachmentFolder = attachmentFolder;
2698
+ this.notionClient = notionClient;
2361
2699
  }
2362
2700
  vaultFs;
2363
2701
  attachmentFolder;
2702
+ notionClient;
2364
2703
  async downloadImage(url, pageTitle) {
2365
2704
  const response = await fetch(url);
2366
2705
  if (!response.ok) {
@@ -2414,6 +2753,54 @@ var ImageHandler = class {
2414
2753
  }
2415
2754
  return { content: result, downloads };
2416
2755
  }
2756
+ async uploadLocalImage(localPath) {
2757
+ if (!this.notionClient) {
2758
+ throw new Error("NotionClient required for image upload");
2759
+ }
2760
+ const buffer = await this.readImageFromVault(localPath);
2761
+ const filename = localPath.split("/").pop() ?? "image.png";
2762
+ const contentType = this.getContentTypeFromPath(filename);
2763
+ const blob = new Blob([buffer], { type: contentType });
2764
+ const fileUploadId = await this.notionClient.uploadFile(blob, filename, contentType);
2765
+ return { localPath, fileUploadId };
2766
+ }
2767
+ async uploadAndAppendImages(pageId, images) {
2768
+ if (!this.notionClient) return [];
2769
+ const localImages = images.filter((img) => !img.isExternal && img.localPath);
2770
+ if (localImages.length === 0) return [];
2771
+ const results = [];
2772
+ const imageBlocks = [];
2773
+ for (const img of localImages) {
2774
+ try {
2775
+ const result = await this.uploadLocalImage(img.localPath);
2776
+ results.push(result);
2777
+ imageBlocks.push({
2778
+ type: "image",
2779
+ image: {
2780
+ type: "file_upload",
2781
+ file_upload: { id: result.fileUploadId }
2782
+ }
2783
+ });
2784
+ } catch (error) {
2785
+ getLogger().warn(`\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC \uC2E4\uD328 (${img.localPath}):`, error);
2786
+ }
2787
+ }
2788
+ if (imageBlocks.length > 0) {
2789
+ await this.notionClient.appendChildren(pageId, imageBlocks);
2790
+ }
2791
+ return results;
2792
+ }
2793
+ async readImageFromVault(localPath) {
2794
+ try {
2795
+ return await this.vaultFs.readBinary(localPath);
2796
+ } catch {
2797
+ return await this.vaultFs.readBinary(`${this.attachmentFolder}/${localPath}`);
2798
+ }
2799
+ }
2800
+ getContentTypeFromPath(filename) {
2801
+ const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
2802
+ return (ext && EXTENSION_TO_MIME[ext]) ?? "application/octet-stream";
2803
+ }
2417
2804
  isNotionImageUrl(url) {
2418
2805
  return url.includes("secure.notion-static.com") || url.includes("prod-files-secure") || url.includes("s3.us-west-2.amazonaws.com/secure.notion-static.com");
2419
2806
  }
@@ -2462,6 +2849,257 @@ function sanitizeFileName(name) {
2462
2849
  return sanitized;
2463
2850
  }
2464
2851
 
2852
+ // src/converter/enhanced-md-converter.ts
2853
+ var NOTION_CALLOUT_RE = /^::: callout\n([\s\S]*?)\n:::/gm;
2854
+ var NOTION_TOGGLE_RE = /<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gs;
2855
+ var NOTION_PAGE_MENTION_RE = /<mention-page id="([^"]+)">([\s\S]*?)<\/mention-page>/g;
2856
+ var NOTION_USER_MENTION_RE = /<mention-user id="[^"]*">([^<]*)<\/mention-user>/g;
2857
+ var NOTION_DATE_MENTION_RE = /<mention-date start="([^"]*)"(?: end="([^"]*)")?[^>]*\/>/g;
2858
+ var NOTION_UNKNOWN_RE = /<unknown id="[^"]*"[^>]*\/>/g;
2859
+ var TOGGLE_START2 = "%%im-nobsidian:toggle:start%%";
2860
+ var TOGGLE_END2 = "%%im-nobsidian:toggle:end%%";
2861
+ function notionEnhancedToObsidian(enhanced) {
2862
+ let result = enhanced;
2863
+ result = convertToggles(result);
2864
+ result = convertCallouts(result);
2865
+ result = convertPageMentions(result);
2866
+ result = convertPageLinks(result);
2867
+ result = convertUserMentions(result);
2868
+ result = convertDateMentions(result);
2869
+ result = removeUnknownBlocks(result);
2870
+ result = convertNotionMath(result);
2871
+ result = convertNotionTables(result);
2872
+ result = convertColorSpans(result);
2873
+ result = removeEmptyBlocks(result);
2874
+ result = unescapePipes(result);
2875
+ return result;
2876
+ }
2877
+ function obsidianToNotionEnhanced(obsidian) {
2878
+ let result = obsidian;
2879
+ result = convertTogglesToHtml(result);
2880
+ result = convertObsidianCallouts(result);
2881
+ return result;
2882
+ }
2883
+ function convertToggles(content) {
2884
+ function replaceToggle(match) {
2885
+ const m = /<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/s.exec(match);
2886
+ if (!m) return match;
2887
+ const title = m[1].trim();
2888
+ let body = m[2].trim();
2889
+ body = convertToggles(body);
2890
+ const indented = body.split("\n").map((line) => line ? ` ${line}` : "").join("\n").trimEnd();
2891
+ return `${TOGGLE_START2}
2892
+ - ${title}
2893
+ ${indented}
2894
+ ${TOGGLE_END2}`;
2895
+ }
2896
+ return content.replace(NOTION_TOGGLE_RE, replaceToggle);
2897
+ }
2898
+ function convertCallouts(content) {
2899
+ return content.replace(NOTION_CALLOUT_RE, (_match, body) => {
2900
+ const lines = body.trim().split("\n");
2901
+ const firstLine = lines[0] ?? "";
2902
+ const emojiMatch = /^([\p{Emoji}️‍]+)\s*(.*)/u.exec(firstLine);
2903
+ const type = emojiMatch ? emojiToCalloutType(emojiMatch[1]) : "note";
2904
+ const title = emojiMatch ? emojiMatch[2] : firstLine;
2905
+ const rest = lines.slice(1).join("\n").trim();
2906
+ const calloutTitle = title ? `> [!${type}] ${title}` : `> [!${type}]`;
2907
+ const calloutBody = rest ? "\n" + rest.split("\n").map((line) => `> ${line}`).join("\n") : "";
2908
+ return calloutTitle + calloutBody;
2909
+ });
2910
+ }
2911
+ function convertPageMentions(content) {
2912
+ return content.replace(NOTION_PAGE_MENTION_RE, (_match, _id, text) => {
2913
+ const cleaned = text.trim();
2914
+ return `[[${cleaned}]]`;
2915
+ });
2916
+ }
2917
+ function convertUserMentions(content) {
2918
+ return content.replace(NOTION_USER_MENTION_RE, (_match, name) => `@${name}`);
2919
+ }
2920
+ function convertDateMentions(content) {
2921
+ return content.replace(NOTION_DATE_MENTION_RE, (_match, start, end) => {
2922
+ return end ? `${start} \u2192 ${end}` : start;
2923
+ });
2924
+ }
2925
+ function removeUnknownBlocks(content) {
2926
+ return content.replace(NOTION_UNKNOWN_RE, "");
2927
+ }
2928
+ function convertTogglesToHtml(content) {
2929
+ const startRe = new RegExp(escapeRegex3(TOGGLE_START2), "g");
2930
+ const endRe = new RegExp(escapeRegex3(TOGGLE_END2), "g");
2931
+ let result = content;
2932
+ let safety = 0;
2933
+ while (result.includes(TOGGLE_START2) && safety++ < 100) {
2934
+ const startIdx = result.indexOf(TOGGLE_START2);
2935
+ const endIdx = findMatchingEnd(result, startIdx + TOGGLE_START2.length);
2936
+ if (endIdx === -1) break;
2937
+ const inner = result.slice(startIdx + TOGGLE_START2.length, endIdx).trim();
2938
+ const titleMatch = /^- (.+)$/m.exec(inner);
2939
+ const title = titleMatch ? titleMatch[1] : "";
2940
+ const body = inner.split("\n").slice(1).map((line) => line.replace(/^ {2}/, "")).join("\n").trim();
2941
+ const html = `<details>
2942
+ <summary>${title}</summary>
2943
+
2944
+ ${body}
2945
+
2946
+ </details>`;
2947
+ result = result.slice(0, startIdx) + html + result.slice(endIdx + TOGGLE_END2.length);
2948
+ }
2949
+ result = result.replace(startRe, "").replace(endRe, "");
2950
+ return result;
2951
+ }
2952
+ function findMatchingEnd(content, startFrom) {
2953
+ let depth = 1;
2954
+ let pos = startFrom;
2955
+ while (pos < content.length && depth > 0) {
2956
+ const nextStart = content.indexOf(TOGGLE_START2, pos);
2957
+ const nextEnd = content.indexOf(TOGGLE_END2, pos);
2958
+ if (nextEnd === -1) return -1;
2959
+ if (nextStart !== -1 && nextStart < nextEnd) {
2960
+ depth++;
2961
+ pos = nextStart + TOGGLE_START2.length;
2962
+ } else {
2963
+ depth--;
2964
+ if (depth === 0) return nextEnd;
2965
+ pos = nextEnd + TOGGLE_END2.length;
2966
+ }
2967
+ }
2968
+ return -1;
2969
+ }
2970
+ function convertObsidianCallouts(content) {
2971
+ const lines = content.split("\n");
2972
+ const result = [];
2973
+ let i = 0;
2974
+ while (i < lines.length) {
2975
+ const headerMatch = /^> \[!(\w+)\]([-+])?\s*(.*)/.exec(lines[i]);
2976
+ if (headerMatch) {
2977
+ const type = headerMatch[1];
2978
+ const title = headerMatch[3];
2979
+ const emoji = calloutTypeToEmoji(type) ?? "\u{1F4A1}";
2980
+ const calloutTitle = title ? `${emoji} ${title}` : emoji;
2981
+ const bodyLines = [];
2982
+ i++;
2983
+ while (i < lines.length && lines[i].startsWith("> ")) {
2984
+ bodyLines.push(lines[i].slice(2));
2985
+ i++;
2986
+ }
2987
+ result.push("::: callout");
2988
+ result.push(calloutTitle);
2989
+ if (bodyLines.length > 0) {
2990
+ result.push(...bodyLines);
2991
+ }
2992
+ result.push(":::");
2993
+ } else {
2994
+ result.push(lines[i]);
2995
+ i++;
2996
+ }
2997
+ }
2998
+ return result.join("\n");
2999
+ }
3000
+ var EMOJI_TYPE_MAP2 = {
3001
+ "\u{1F4A1}": "tip",
3002
+ "\u2139\uFE0F": "info",
3003
+ "\u26A0\uFE0F": "warning",
3004
+ "\u{1F525}": "danger",
3005
+ "\u2705": "success",
3006
+ "\u274C": "failure",
3007
+ "\u2753": "question",
3008
+ "\u{1F4DD}": "note",
3009
+ "\u{1F4CC}": "abstract",
3010
+ "\u{1F41B}": "bug",
3011
+ "\u{1F4AC}": "quote",
3012
+ "\u{1F4CB}": "example"
3013
+ };
3014
+ var TYPE_EMOJI_MAP = Object.fromEntries(
3015
+ Object.entries(EMOJI_TYPE_MAP2).map(([k, v]) => [v, k])
3016
+ );
3017
+ function emojiToCalloutType(emoji) {
3018
+ return EMOJI_TYPE_MAP2[emoji] ?? "note";
3019
+ }
3020
+ function calloutTypeToEmoji(type) {
3021
+ return TYPE_EMOJI_MAP[type.toLowerCase()];
3022
+ }
3023
+ function escapeRegex3(str) {
3024
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3025
+ }
3026
+ var NOTION_PAGE_LINK_RE = /<page url="[^"]*">([\s\S]*?)<\/page>/g;
3027
+ var NOTION_COLOR_SPAN_RE = /<span color="([^"]+)">([\s\S]*?)<\/span>/g;
3028
+ var NOTION_EMPTY_BLOCK_RE = /^<empty-block\/>\n?/gm;
3029
+ function convertPageLinks(content) {
3030
+ return content.replace(NOTION_PAGE_LINK_RE, (_match, text) => {
3031
+ const cleaned = text.replace(/\*\*/g, "").trim();
3032
+ return `[[${cleaned}]]`;
3033
+ });
3034
+ }
3035
+ function convertColorSpans(content) {
3036
+ return content.replace(NOTION_COLOR_SPAN_RE, (_match, _color, text) => text);
3037
+ }
3038
+ function removeEmptyBlocks(content) {
3039
+ return content.replace(NOTION_EMPTY_BLOCK_RE, "\n");
3040
+ }
3041
+ var NOTION_INLINE_MATH_RE = /\$`([^`]+)`\$/g;
3042
+ var NOTION_BLOCK_MATH_RE = /\$\$\n```\n([\s\S]*?)\n```\n\$\$/g;
3043
+ function convertNotionMath(content) {
3044
+ let result = content.replace(NOTION_BLOCK_MATH_RE, (_match, eq) => {
3045
+ return `$$
3046
+ ${eq.trim()}
3047
+ $$`;
3048
+ });
3049
+ result = result.replace(NOTION_INLINE_MATH_RE, (_match, eq) => `$${eq}$`);
3050
+ return result;
3051
+ }
3052
+ var NOTION_TABLE_RE = /<table[^>]*>([\s\S]*?)<\/table>/g;
3053
+ var TABLE_ROW_RE = /<tr>([\s\S]*?)<\/tr>/g;
3054
+ var TABLE_CELL_RE = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/g;
3055
+ function convertNotionTables(content) {
3056
+ return content.replace(NOTION_TABLE_RE, (_match, tableBody) => {
3057
+ const rows = [];
3058
+ let rowMatch;
3059
+ const rowRe = new RegExp(TABLE_ROW_RE.source, TABLE_ROW_RE.flags);
3060
+ while ((rowMatch = rowRe.exec(tableBody)) !== null) {
3061
+ const cells = [];
3062
+ let cellMatch;
3063
+ const cellRe = new RegExp(TABLE_CELL_RE.source, TABLE_CELL_RE.flags);
3064
+ while ((cellMatch = cellRe.exec(rowMatch[1])) !== null) {
3065
+ cells.push(cellMatch[1].trim());
3066
+ }
3067
+ rows.push(cells);
3068
+ }
3069
+ if (rows.length === 0) return _match;
3070
+ const colCount = Math.max(...rows.map((r) => r.length));
3071
+ const lines = [];
3072
+ for (let i = 0; i < rows.length; i++) {
3073
+ const padded = rows[i];
3074
+ while (padded.length < colCount) padded.push("");
3075
+ lines.push(`| ${padded.join(" | ")} |`);
3076
+ if (i === 0) {
3077
+ lines.push(`| ${padded.map(() => "---").join(" | ")} |`);
3078
+ }
3079
+ }
3080
+ return lines.join("\n");
3081
+ });
3082
+ }
3083
+ function unescapePipes(content) {
3084
+ const lines = content.split("\n");
3085
+ const result = [];
3086
+ let inTable = false;
3087
+ for (const line of lines) {
3088
+ if (/^\|.*\|$/.test(line.trim()) || /^\|[\s-|]+\|$/.test(line.trim())) {
3089
+ inTable = true;
3090
+ result.push(line);
3091
+ } else {
3092
+ if (inTable && line.trim() === "") {
3093
+ inTable = false;
3094
+ } else if (!/^\|/.test(line.trim())) {
3095
+ inTable = false;
3096
+ }
3097
+ result.push(inTable ? line : line.replace(/\\\|/g, "|"));
3098
+ }
3099
+ }
3100
+ return result.join("\n");
3101
+ }
3102
+
2465
3103
  // src/sync/orchestrator.ts
2466
3104
  var SyncOrchestrator = class {
2467
3105
  constructor(config, stateDb, notionClient, vaultFs) {
@@ -2470,10 +3108,16 @@ var SyncOrchestrator = class {
2470
3108
  this.notionClient = notionClient;
2471
3109
  this.vaultFs = vaultFs;
2472
3110
  this.changeDetector = new ChangeDetector(stateDb);
2473
- this.pipeline = createDefaultPipeline();
3111
+ this.pipeline = createDefaultPipeline({
3112
+ wikilinkResolver: (text) => stateDb.resolveWikilink(text)
3113
+ });
2474
3114
  this.blockConverter = new BlockConverter();
2475
- this.imageHandler = new ImageHandler(vaultFs, config.paths.attachments);
3115
+ this.imageHandler = new ImageHandler(vaultFs, config.paths.attachments, notionClient);
2476
3116
  this.propertyMapper = new PropertyMapper();
3117
+ this.propertyMapper.setWikilinkResolver({
3118
+ resolve: (title) => stateDb.resolveWikilink(title)?.notionPageId ?? null,
3119
+ resolvePageId: (pageId) => stateDb.resolvePageId(pageId)?.title ?? null
3120
+ });
2477
3121
  this.blockConverter.initNotionToMd(this.notionClient.getInternalClient());
2478
3122
  }
2479
3123
  config;
@@ -2488,16 +3132,29 @@ var SyncOrchestrator = class {
2488
3132
  dbSchemaLoaded = false;
2489
3133
  async push(options) {
2490
3134
  const startTime = Date.now();
3135
+ if (this.config.sync.direction === "pull") {
3136
+ return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
3137
+ }
3138
+ this.cleanupInterruptedSync();
2491
3139
  const files = await this.vaultFs.listMarkdownFiles();
2492
3140
  const changes = this.changeDetector.detectLocalChanges(files);
2493
3141
  const conflictPaths = new Set(this.stateDb.getByStatus("conflict").map((r) => r.obsidianPath));
2494
- const nonConflict = changes.filter((c) => !conflictPaths.has(c.path));
2495
- const filtered = options?.paths ? nonConflict.filter((c) => options.paths.some((p) => c.path.startsWith(p))) : nonConflict;
3142
+ const eligible = options?.force ? changes : changes.filter((c) => !conflictPaths.has(c.path));
3143
+ const filtered = options?.paths ? eligible.filter((c) => options.paths.some((p) => c.path.startsWith(p))) : eligible;
2496
3144
  if (filtered.length === 0) {
2497
3145
  return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
2498
3146
  }
2499
3147
  if (options?.dryRun) {
2500
- return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
3148
+ const dryCreated = filtered.filter((c) => c.type === "created").length;
3149
+ const dryUpdated = filtered.filter((c) => c.type === "modified" || c.type === "moved").length;
3150
+ const dryDeleted = filtered.filter((c) => c.type === "deleted").length;
3151
+ return {
3152
+ created: dryCreated,
3153
+ updated: dryUpdated,
3154
+ deleted: dryDeleted,
3155
+ failed: [],
3156
+ duration: Date.now() - startTime
3157
+ };
2501
3158
  }
2502
3159
  this.stateDb.setMeta("push_in_progress", "true");
2503
3160
  const folderPaths = /* @__PURE__ */ new Set();
@@ -2559,6 +3216,19 @@ var SyncOrchestrator = class {
2559
3216
  }
2560
3217
  async pull(options) {
2561
3218
  const startTime = Date.now();
3219
+ const emptyResult = {
3220
+ created: 0,
3221
+ updated: 0,
3222
+ deleted: 0,
3223
+ conflicts: [],
3224
+ writtenPaths: [],
3225
+ failed: [],
3226
+ duration: Date.now() - startTime
3227
+ };
3228
+ if (this.config.sync.direction === "push") {
3229
+ return emptyResult;
3230
+ }
3231
+ this.cleanupInterruptedSync();
2562
3232
  let created = 0;
2563
3233
  let updated = 0;
2564
3234
  let deleted = 0;
@@ -2570,11 +3240,17 @@ var SyncOrchestrator = class {
2570
3240
  const record = this.stateDb.getByNotionId(c.pageId);
2571
3241
  return record && options.paths.some((p) => record.obsidianPath.startsWith(p));
2572
3242
  }) : remoteChanges;
2573
- if (options?.dryRun || filtered.length === 0) {
3243
+ if (filtered.length === 0) {
3244
+ return emptyResult;
3245
+ }
3246
+ if (options?.dryRun) {
3247
+ const dryCreated = filtered.filter((c) => c.type === "created").length;
3248
+ const dryUpdated = filtered.filter((c) => c.type === "modified").length;
3249
+ const dryDeleted = filtered.filter((c) => c.type === "deleted").length;
2574
3250
  return {
2575
- created: 0,
2576
- updated: 0,
2577
- deleted: 0,
3251
+ created: dryCreated,
3252
+ updated: dryUpdated,
3253
+ deleted: dryDeleted,
2578
3254
  conflicts: [],
2579
3255
  writtenPaths: [],
2580
3256
  failed: [],
@@ -2729,7 +3405,6 @@ var SyncOrchestrator = class {
2729
3405
  filePath: path,
2730
3406
  parentMode: this.config.notion.parentMode
2731
3407
  });
2732
- const blocks = this.blockConverter.markdownToNotionBlocks(conversionResult.content);
2733
3408
  let effectiveParentId = parentId;
2734
3409
  let effectiveParentType = "page";
2735
3410
  let effectiveProperties = conversionResult.properties;
@@ -2742,15 +3417,14 @@ var SyncOrchestrator = class {
2742
3417
  title
2743
3418
  );
2744
3419
  }
2745
- const page = await this.notionClient.createPage({
2746
- parentId: effectiveParentId,
2747
- parentType: effectiveParentType,
3420
+ const page = await this.pushCreatePage(
3421
+ effectiveParentId,
3422
+ effectiveParentType,
2748
3423
  title,
2749
- properties: effectiveProperties
2750
- });
2751
- if (blocks.length > 0) {
2752
- await this.notionClient.appendChildren(page.id, blocks);
2753
- }
3424
+ conversionResult.content,
3425
+ effectiveProperties
3426
+ );
3427
+ await this.imageHandler.uploadAndAppendImages(page.id, conversionResult.images);
2754
3428
  const hash = computeHash(content);
2755
3429
  this.stateDb.transaction(() => {
2756
3430
  this.stateDb.upsert({
@@ -2765,11 +3439,12 @@ var SyncOrchestrator = class {
2765
3439
  status: "synced",
2766
3440
  baseSnapshot: Buffer.from(content, "utf-8")
2767
3441
  });
3442
+ const aliases = extractAliases(conversionResult.properties);
2768
3443
  this.stateDb.upsertWikilink({
2769
3444
  obsidianPath: path,
2770
3445
  notionPageId: page.id,
2771
3446
  title,
2772
- aliases: []
3447
+ aliases
2773
3448
  });
2774
3449
  this.stateDb.storePreserveMarkers(path, conversionResult.preserveMarkers);
2775
3450
  });
@@ -2790,28 +3465,32 @@ var SyncOrchestrator = class {
2790
3465
  filePath: path,
2791
3466
  parentMode: this.config.notion.parentMode
2792
3467
  });
2793
- const blocks = this.blockConverter.markdownToNotionBlocks(conversionResult.content);
2794
- const existingBlocks = await this.notionClient.fetchAllChildren(record.notionPageId);
2795
- if (blocks.length > 0) {
2796
- await this.notionClient.appendChildren(record.notionPageId, blocks);
2797
- }
2798
- for (const block of existingBlocks) {
2799
- await this.notionClient.deleteBlock(block.id);
2800
- }
3468
+ await this.pushUpdatePage(record.notionPageId, conversionResult.content);
3469
+ await this.imageHandler.uploadAndAppendImages(record.notionPageId, conversionResult.images);
2801
3470
  let propsToUpdate = conversionResult.properties;
2802
3471
  if (this.isDatabaseMode && propsToUpdate && Object.keys(propsToUpdate).length > 0) {
2803
3472
  await this.ensureDbSchema();
2804
- const title = extractTitle(path);
2805
- propsToUpdate = this.propertyMapper.toNotionProperties(propsToUpdate, title);
3473
+ const title2 = extractTitle(path);
3474
+ propsToUpdate = this.propertyMapper.toNotionProperties(propsToUpdate, title2);
2806
3475
  }
2807
3476
  if (propsToUpdate && Object.keys(propsToUpdate).length > 0) {
2808
3477
  await this.notionClient.updatePageProperties(record.notionPageId, propsToUpdate);
2809
3478
  }
3479
+ const updatedPage = await this.notionClient.getPage(record.notionPageId);
2810
3480
  const hash = computeHash(content);
3481
+ const title = extractTitle(path);
3482
+ const aliases = extractAliases(conversionResult.properties);
2811
3483
  this.stateDb.transaction(() => {
2812
3484
  this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
2813
3485
  this.stateDb.updateStatus(record.id, "synced");
3486
+ this.stateDb.setNotionLastEdited(record.id, updatedPage.last_edited_time);
2814
3487
  this.stateDb.storePreserveMarkers(path, conversionResult.preserveMarkers);
3488
+ this.stateDb.upsertWikilink({
3489
+ obsidianPath: path,
3490
+ notionPageId: record.notionPageId,
3491
+ title,
3492
+ aliases
3493
+ });
2815
3494
  });
2816
3495
  }
2817
3496
  async pushDelete(path) {
@@ -2819,8 +3498,10 @@ var SyncOrchestrator = class {
2819
3498
  if (!record?.notionPageId) return;
2820
3499
  if (this.config.sync.deleteSync) {
2821
3500
  await this.notionClient.archivePage(record.notionPageId);
3501
+ this.stateDb.delete(record.id);
3502
+ } else {
3503
+ this.stateDb.updateStatus(record.id, "pending");
2822
3504
  }
2823
- this.stateDb.delete(record.id);
2824
3505
  }
2825
3506
  async detectRemoteChanges() {
2826
3507
  const changes = [];
@@ -2885,24 +3566,25 @@ var SyncOrchestrator = class {
2885
3566
  const title = this.notionClient.extractTitle(page);
2886
3567
  const safeName = sanitizeFileName(title);
2887
3568
  const parentPath = await this.resolveParentPath(page);
2888
- const children = await this.notionClient.listChildren(pageId, { pageSize: 1 });
2889
- const hasChildPages = children.results.some((b) => "type" in b && b.type === "child_page");
2890
- const markdown = await this.blockConverter.notionBlocksToMarkdown(pageId);
3569
+ const allChildren = await this.notionClient.fetchAllChildren(pageId);
3570
+ const hasChildPages = allChildren.some((b) => "type" in b && b.type === "child_page");
3571
+ const markdown = await this.fetchPageMarkdown(pageId);
2891
3572
  const hasContent = markdown.trim().length > 0;
2892
3573
  let filePath;
2893
3574
  let fileType;
2894
3575
  if (hasChildPages && hasContent) {
2895
3576
  const folderPath = parentPath ? `${parentPath}/${safeName}` : safeName;
2896
- filePath = `${folderPath}/${safeName}.md`;
3577
+ filePath = await this.resolveUniqueFilePath(`${folderPath}/${safeName}.md`);
2897
3578
  fileType = "folder-note";
2898
3579
  await this.vaultFs.ensureFolder(folderPath);
2899
3580
  } else if (hasChildPages && !hasContent) {
2900
3581
  const folderPath = parentPath ? `${parentPath}/${safeName}` : safeName;
2901
- filePath = `${folderPath}/${safeName}.md`;
3582
+ filePath = await this.resolveUniqueFilePath(`${folderPath}/${safeName}.md`);
2902
3583
  fileType = "folder-only";
2903
3584
  await this.vaultFs.ensureFolder(folderPath);
2904
3585
  } else {
2905
- filePath = parentPath ? `${parentPath}/${safeName}.md` : `${safeName}.md`;
3586
+ const basePath = parentPath ? `${parentPath}/${safeName}.md` : `${safeName}.md`;
3587
+ filePath = await this.resolveUniqueFilePath(basePath);
2906
3588
  fileType = "file";
2907
3589
  }
2908
3590
  let properties;
@@ -2914,6 +3596,7 @@ var SyncOrchestrator = class {
2914
3596
  } else {
2915
3597
  properties = this.notionClient.extractProperties(page);
2916
3598
  }
3599
+ properties.title = title;
2917
3600
  let processedMarkdown = markdown;
2918
3601
  if (this.config.conversion.imageDownload === "immediate") {
2919
3602
  const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
@@ -2944,11 +3627,12 @@ var SyncOrchestrator = class {
2944
3627
  status: "synced",
2945
3628
  baseSnapshot: Buffer.from(finalContent, "utf-8")
2946
3629
  });
3630
+ const aliases = extractAliases(properties);
2947
3631
  this.stateDb.upsertWikilink({
2948
3632
  obsidianPath: filePath,
2949
3633
  notionPageId: pageId,
2950
3634
  title,
2951
- aliases: []
3635
+ aliases
2952
3636
  });
2953
3637
  });
2954
3638
  return filePath;
@@ -2957,7 +3641,8 @@ var SyncOrchestrator = class {
2957
3641
  const record = this.stateDb.getByNotionId(change.pageId);
2958
3642
  if (!record) return {};
2959
3643
  const page = await this.notionClient.getPage(change.pageId);
2960
- let markdown = await this.blockConverter.notionBlocksToMarkdown(change.pageId);
3644
+ let markdown = await this.fetchPageMarkdown(change.pageId);
3645
+ const title = this.notionClient.extractTitle(page);
2961
3646
  let properties;
2962
3647
  if (this.isDatabaseMode) {
2963
3648
  await this.ensureDbSchema();
@@ -2967,8 +3652,8 @@ var SyncOrchestrator = class {
2967
3652
  } else {
2968
3653
  properties = this.notionClient.extractProperties(page);
2969
3654
  }
3655
+ properties.title = title;
2970
3656
  if (this.config.conversion.imageDownload === "immediate") {
2971
- const title = this.notionClient.extractTitle(page);
2972
3657
  const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
2973
3658
  markdown = imageResult.content;
2974
3659
  }
@@ -2992,21 +3677,26 @@ var SyncOrchestrator = class {
2992
3677
  const localHash = computeHash(localContent);
2993
3678
  const localModified = localHash !== record.contentHash;
2994
3679
  if (localModified) {
2995
- const conflict = {
2996
- syncRecord: record,
2997
- localChange: {
2998
- path: record.obsidianPath,
2999
- type: "modified",
3000
- currentHash: localHash,
3001
- previousHash: record.contentHash
3002
- },
3003
- remoteChange: change,
3004
- baseContent: record.baseSnapshot?.toString("utf-8") ?? null,
3005
- localContent,
3006
- remoteContent
3007
- };
3008
- this.stateDb.updateStatus(record.id, "conflict");
3009
- return { conflict };
3680
+ const strategy = this.config.sync.conflictStrategy;
3681
+ if (strategy === "remote-first") ; else if (strategy === "local-first") {
3682
+ return { path: record.obsidianPath };
3683
+ } else {
3684
+ const conflict = {
3685
+ syncRecord: record,
3686
+ localChange: {
3687
+ path: record.obsidianPath,
3688
+ type: "modified",
3689
+ currentHash: localHash,
3690
+ previousHash: record.contentHash
3691
+ },
3692
+ remoteChange: change,
3693
+ baseContent: record.baseSnapshot?.toString("utf-8") ?? null,
3694
+ localContent,
3695
+ remoteContent
3696
+ };
3697
+ this.stateDb.updateStatus(record.id, "conflict");
3698
+ return { conflict };
3699
+ }
3010
3700
  }
3011
3701
  await this.vaultFs.writeFile(record.obsidianPath, remoteContent);
3012
3702
  const newHash = computeHash(remoteContent);
@@ -3039,6 +3729,22 @@ var SyncOrchestrator = class {
3039
3729
  async ensureFolderPage(folderPath) {
3040
3730
  const existing = this.stateDb.getByPath(folderPath);
3041
3731
  if (existing?.notionPageId) return;
3732
+ const folderNotePath = `${folderPath}/${folderPath.split("/").pop()}.md`;
3733
+ const folderNoteRecord = this.stateDb.getByPath(folderNotePath);
3734
+ if (folderNoteRecord?.notionParentId) {
3735
+ this.stateDb.upsert({
3736
+ obsidianPath: folderPath,
3737
+ notionPageId: folderNoteRecord.notionParentId,
3738
+ notionParentId: "",
3739
+ contentHash: "",
3740
+ notionLastEdited: folderNoteRecord.notionLastEdited,
3741
+ localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
3742
+ syncDirection: "both",
3743
+ fileType: "folder-note",
3744
+ status: "synced"
3745
+ });
3746
+ return;
3747
+ }
3042
3748
  const parts = folderPath.split("/");
3043
3749
  const folderName = parts[parts.length - 1];
3044
3750
  let parentId = this.config.notion.rootPageId;
@@ -3099,23 +3805,118 @@ var SyncOrchestrator = class {
3099
3805
  try {
3100
3806
  const parentPage = await this.notionClient.getPage(parentId);
3101
3807
  const parentTitle = this.notionClient.extractTitle(parentPage);
3102
- return sanitizeFileName(parentTitle);
3808
+ const grandparentPath = await this.resolveParentPath(parentPage);
3809
+ const safeName = sanitizeFileName(parentTitle);
3810
+ return grandparentPath ? `${grandparentPath}/${safeName}` : safeName;
3103
3811
  } catch {
3104
3812
  return "";
3105
3813
  }
3106
3814
  }
3815
+ async pushCreatePage(parentId, parentType, title, markdownContent, properties) {
3816
+ if (this.config.conversion.preferMarkdownApi !== false) {
3817
+ try {
3818
+ const enhanced = obsidianToNotionEnhanced(markdownContent);
3819
+ return await this.notionClient.createPageWithMarkdown({
3820
+ parentId,
3821
+ parentType,
3822
+ title,
3823
+ markdown: enhanced,
3824
+ properties
3825
+ });
3826
+ } catch {
3827
+ }
3828
+ }
3829
+ const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
3830
+ const page = await this.notionClient.createPage({
3831
+ parentId,
3832
+ parentType,
3833
+ title,
3834
+ properties
3835
+ });
3836
+ if (blocks.length > 0) {
3837
+ await this.notionClient.appendChildren(page.id, blocks);
3838
+ }
3839
+ return page;
3840
+ }
3841
+ async pushUpdatePage(pageId, markdownContent) {
3842
+ if (this.config.conversion.preferMarkdownApi !== false) {
3843
+ try {
3844
+ const enhanced = obsidianToNotionEnhanced(markdownContent);
3845
+ await this.notionClient.replacePageMarkdown(pageId, enhanced);
3846
+ return;
3847
+ } catch {
3848
+ }
3849
+ }
3850
+ const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
3851
+ const existingBlocks = await this.notionClient.fetchAllChildren(pageId);
3852
+ if (blocks.length > 0) {
3853
+ await this.notionClient.appendChildren(pageId, blocks);
3854
+ }
3855
+ const deleteSema = new Sema(this.config.advanced.concurrency);
3856
+ await Promise.all(
3857
+ existingBlocks.map(async (block) => {
3858
+ await deleteSema.acquire();
3859
+ try {
3860
+ await this.notionClient.deleteBlock(block.id);
3861
+ } finally {
3862
+ deleteSema.release();
3863
+ }
3864
+ })
3865
+ );
3866
+ }
3867
+ async fetchPageMarkdown(pageId) {
3868
+ if (this.config.conversion.preferMarkdownApi !== false) {
3869
+ try {
3870
+ const result = await this.notionClient.getPageMarkdown(pageId);
3871
+ return notionEnhancedToObsidian(result.markdown);
3872
+ } catch {
3873
+ }
3874
+ }
3875
+ return this.blockConverter.notionBlocksToMarkdown(pageId);
3876
+ }
3107
3877
  extractParentId(page) {
3108
3878
  const parent = page.parent;
3109
3879
  if (parent.type === "page_id") return parent.page_id ?? null;
3110
3880
  if (parent.type === "database_id") return parent.database_id ?? null;
3111
3881
  return null;
3112
3882
  }
3883
+ cleanupInterruptedSync() {
3884
+ const pushInProgress = this.stateDb.getMeta("push_in_progress");
3885
+ const pullInProgress = this.stateDb.getMeta("pull_in_progress");
3886
+ if (pushInProgress === "true") {
3887
+ getLogger().warn("[Im-Nobsidian] \uC774\uC804 push\uAC00 \uBE44\uC815\uC0C1 \uC885\uB8CC\uB428 \u2014 \uD50C\uB798\uADF8 \uC815\uB9AC");
3888
+ this.stateDb.setMeta("push_in_progress", "");
3889
+ }
3890
+ if (pullInProgress === "true") {
3891
+ getLogger().warn("[Im-Nobsidian] \uC774\uC804 pull\uC774 \uBE44\uC815\uC0C1 \uC885\uB8CC\uB428 \u2014 \uD50C\uB798\uADF8 \uC815\uB9AC");
3892
+ this.stateDb.setMeta("pull_in_progress", "");
3893
+ }
3894
+ }
3895
+ async resolveUniqueFilePath(basePath) {
3896
+ if (!await this.vaultFs.exists(basePath)) return basePath;
3897
+ const dir = basePath.lastIndexOf("/") >= 0 ? basePath.slice(0, basePath.lastIndexOf("/")) : "";
3898
+ const ext = ".md";
3899
+ const name = basePath.slice(dir ? dir.length + 1 : 0, -ext.length);
3900
+ for (let i = 1; i <= 99; i++) {
3901
+ const candidate = dir ? `${dir}/${name} (${i})${ext}` : `${name} (${i})${ext}`;
3902
+ if (!await this.vaultFs.exists(candidate)) return candidate;
3903
+ }
3904
+ return basePath;
3905
+ }
3113
3906
  };
3114
3907
  function extractTitle(filePath) {
3115
3908
  const parts = filePath.split("/");
3116
3909
  const filename = parts[parts.length - 1] ?? "";
3117
3910
  return filename.replace(/\.md$/, "");
3118
3911
  }
3912
+ function extractAliases(properties) {
3913
+ const raw = properties.aliases ?? properties.alias;
3914
+ if (!raw) return [];
3915
+ if (Array.isArray(raw)) return raw.filter((a) => typeof a === "string");
3916
+ if (typeof raw === "string")
3917
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
3918
+ return [];
3919
+ }
3119
3920
  var NodeVaultFS = class {
3120
3921
  constructor(rootPath, pathConfig) {
3121
3922
  this.rootPath = rootPath;
@@ -3140,6 +3941,10 @@ var NodeVaultFS = class {
3140
3941
  const fullPath = join(this.rootPath, path);
3141
3942
  return readFile(fullPath, "utf-8");
3142
3943
  }
3944
+ async readBinary(path) {
3945
+ const fullPath = join(this.rootPath, path);
3946
+ return readFile(fullPath);
3947
+ }
3143
3948
  async writeFile(path, content) {
3144
3949
  const fullPath = join(this.rootPath, path);
3145
3950
  await mkdir(dirname(fullPath), { recursive: true });