@im-nobsidian/core 0.1.3 → 0.1.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/index.js CHANGED
@@ -5,13 +5,20 @@ import Database from 'better-sqlite3';
5
5
  import { randomUUID, createHash } from 'crypto';
6
6
  import { Client } from '@notionhq/client';
7
7
  import { Sema } from 'async-sema';
8
- import matter from 'gray-matter';
8
+ import matter7 from 'gray-matter';
9
9
  import { markdownToBlocks } from '@tryfabric/martian';
10
10
  import { NotionToMarkdown } from 'notion-to-md';
11
11
  import { readFileSync } from 'fs';
12
12
  import { watch } from 'chokidar';
13
13
 
14
14
  // src/types/config.ts
15
+ var DatabaseSyncSchema = z.object({
16
+ databaseId: z.string(),
17
+ localFolder: z.string(),
18
+ propertyMapping: z.record(z.string()).optional(),
19
+ pullFilter: z.unknown().optional(),
20
+ titleProperty: z.string().default("Name")
21
+ });
15
22
  var ConfigSchema = z.object({
16
23
  version: z.literal(1),
17
24
  notion: z.object({
@@ -19,14 +26,16 @@ var ConfigSchema = z.object({
19
26
  rootPageId: z.string(),
20
27
  workspaceId: z.string().optional(),
21
28
  parentMode: z.enum(["page", "database"]).default("page"),
22
- databaseId: z.string().optional()
29
+ databaseId: z.string().optional(),
30
+ databases: z.array(DatabaseSyncSchema).default([])
23
31
  }),
24
32
  sync: z.object({
25
33
  direction: z.enum(["push", "pull", "both"]).default("both"),
26
34
  conflictStrategy: z.enum(["local-first", "remote-first", "manual", "duplicate"]).default("manual"),
27
35
  autoSync: z.boolean().default(false),
28
36
  autoSyncInterval: z.number().min(30).max(3600).default(300),
29
- deleteSync: z.boolean().default(false)
37
+ deleteSync: z.boolean().default(false),
38
+ syncFiles: z.boolean().default(true)
30
39
  }),
31
40
  paths: z.object({
32
41
  include: z.array(z.string()).default(["**/*"]),
@@ -51,14 +60,16 @@ var DEFAULT_CONFIG = {
51
60
  notion: {
52
61
  token: "",
53
62
  rootPageId: "",
54
- parentMode: "page"
63
+ parentMode: "page",
64
+ databases: []
55
65
  },
56
66
  sync: {
57
67
  direction: "both",
58
68
  conflictStrategy: "manual",
59
69
  autoSync: false,
60
70
  autoSyncInterval: 300,
61
- deleteSync: false
71
+ deleteSync: false,
72
+ syncFiles: true
62
73
  },
63
74
  paths: {
64
75
  include: ["**/*"],
@@ -257,6 +268,28 @@ CREATE TABLE IF NOT EXISTS sync_metadata (
257
268
  INSERT OR IGNORE INTO sync_metadata (key, value) VALUES ('schema_version', '1');
258
269
  `;
259
270
 
271
+ // src/state/migrations/002-file-registry.ts
272
+ var FILE_REGISTRY_MIGRATION = `
273
+ CREATE TABLE IF NOT EXISTS file_registry (
274
+ id TEXT PRIMARY KEY,
275
+ local_path TEXT NOT NULL,
276
+ notion_page_id TEXT NOT NULL,
277
+ file_upload_id TEXT NOT NULL,
278
+ file_type TEXT NOT NULL DEFAULT 'file',
279
+ file_hash TEXT NOT NULL,
280
+ file_size INTEGER NOT NULL DEFAULT 0,
281
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
282
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
283
+
284
+ CONSTRAINT chk_file_type CHECK (file_type IN ('image', 'pdf', 'video', 'audio', 'file'))
285
+ );
286
+
287
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_file_registry_path ON file_registry(local_path);
288
+ CREATE INDEX IF NOT EXISTS idx_file_registry_page ON file_registry(notion_page_id);
289
+
290
+ UPDATE sync_metadata SET value = '2' WHERE key = 'schema_version';
291
+ `;
292
+
260
293
  // src/state/state-db.ts
261
294
  var StateDB = class _StateDB {
262
295
  db;
@@ -276,6 +309,9 @@ var StateDB = class _StateDB {
276
309
  if (version < 1) {
277
310
  this.db.exec(INITIAL_MIGRATION);
278
311
  }
312
+ if (version < 2) {
313
+ this.db.exec(FILE_REGISTRY_MIGRATION);
314
+ }
279
315
  }
280
316
  getSchemaVersion() {
281
317
  try {
@@ -406,6 +442,49 @@ var StateDB = class _StateDB {
406
442
  setMeta(key, value) {
407
443
  this.db.prepare("INSERT OR REPLACE INTO sync_metadata (key, value) VALUES (?, ?)").run(key, value);
408
444
  }
445
+ // --- file_registry ---
446
+ isFileRegistered(localPath) {
447
+ const row = this.db.prepare("SELECT 1 FROM file_registry WHERE local_path = ?").get(localPath);
448
+ return !!row;
449
+ }
450
+ getFileRegistry(localPath) {
451
+ const row = this.db.prepare("SELECT * FROM file_registry WHERE local_path = ?").get(localPath);
452
+ return row ? this.mapFileRegistryRow(row) : null;
453
+ }
454
+ getFilesByPageId(notionPageId) {
455
+ const rows = this.db.prepare("SELECT * FROM file_registry WHERE notion_page_id = ?").all(notionPageId);
456
+ return rows.map((r) => this.mapFileRegistryRow(r));
457
+ }
458
+ registerFile(entry) {
459
+ const id = generateId();
460
+ this.db.prepare(
461
+ `INSERT OR REPLACE INTO file_registry
462
+ (id, local_path, notion_page_id, file_upload_id, file_type, file_hash, file_size)
463
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
464
+ ).run(
465
+ id,
466
+ entry.localPath,
467
+ entry.notionPageId,
468
+ entry.fileUploadId,
469
+ entry.fileType,
470
+ entry.fileHash,
471
+ entry.fileSize
472
+ );
473
+ }
474
+ deleteFileRegistry(localPath) {
475
+ this.db.prepare("DELETE FROM file_registry WHERE local_path = ?").run(localPath);
476
+ }
477
+ mapFileRegistryRow(row) {
478
+ return {
479
+ id: row.id,
480
+ localPath: row.local_path,
481
+ notionPageId: row.notion_page_id,
482
+ fileUploadId: row.file_upload_id,
483
+ fileType: row.file_type,
484
+ fileHash: row.file_hash,
485
+ fileSize: row.file_size
486
+ };
487
+ }
409
488
  // --- preserve markers ---
410
489
  storePreserveMarkers(path, markers) {
411
490
  if (markers.length === 0) {
@@ -704,15 +783,17 @@ var PropertyMapper = class {
704
783
  };
705
784
 
706
785
  // src/notion/client.ts
707
- var NotionClient = class {
786
+ var NotionClient = class _NotionClient {
708
787
  client;
709
788
  sema;
789
+ token;
710
790
  propertyMapper = new PropertyMapper();
711
791
  constructor(options) {
712
792
  this.client = new Client({
713
793
  auth: options.token,
714
794
  timeoutMs: options.timeoutMs ?? 3e4
715
795
  });
796
+ this.token = options.token;
716
797
  this.sema = new Sema(options.concurrency ?? 3);
717
798
  }
718
799
  getInternalClient() {
@@ -801,11 +882,31 @@ var NotionClient = class {
801
882
  );
802
883
  }
803
884
  // ─── Database / DataSource ───
885
+ async fetchDatabaseLegacy(databaseId) {
886
+ const cleanId = databaseId.replace(/-/g, "");
887
+ const formatted = `${cleanId.slice(0, 8)}-${cleanId.slice(8, 12)}-${cleanId.slice(12, 16)}-${cleanId.slice(16, 20)}-${cleanId.slice(20)}`;
888
+ const resp = await this.withRateLimit(async () => {
889
+ const r = await fetch(`https://api.notion.com/v1/databases/${formatted}`, {
890
+ headers: {
891
+ Authorization: `Bearer ${this.token}`,
892
+ "Notion-Version": "2022-06-28",
893
+ "Content-Type": "application/json"
894
+ }
895
+ });
896
+ if (!r.ok) throw new Error(`Database fetch failed: ${r.status}`);
897
+ return await r.json();
898
+ });
899
+ return resp;
900
+ }
901
+ async getDatabaseTitle(databaseId) {
902
+ const db = await this.fetchDatabaseLegacy(databaseId);
903
+ const titleArr = db.title;
904
+ return titleArr?.[0]?.plain_text ?? "";
905
+ }
804
906
  async getDatabaseSchema(databaseId) {
805
- const db = await this.withRateLimit(
806
- () => this.client.databases.retrieve({ database_id: databaseId })
807
- );
907
+ const db = await this.fetchDatabaseLegacy(databaseId);
808
908
  const properties = db.properties;
909
+ if (!properties) return {};
809
910
  const schema = {};
810
911
  for (const [name, prop] of Object.entries(properties)) {
811
912
  schema[name] = { id: prop.id, type: prop.type };
@@ -813,11 +914,13 @@ var NotionClient = class {
813
914
  return schema;
814
915
  }
815
916
  async queryDatabase(databaseId, options) {
917
+ const dsId = await this.getDataSourceId(databaseId);
816
918
  const response = await this.withRateLimit(
817
919
  () => this.client.dataSources.query({
818
- data_source_id: databaseId,
920
+ data_source_id: dsId,
819
921
  start_cursor: options?.startCursor,
820
- page_size: options?.pageSize ?? 100
922
+ page_size: options?.pageSize ?? 100,
923
+ filter: options?.filter
821
924
  })
822
925
  );
823
926
  return {
@@ -825,9 +928,47 @@ var NotionClient = class {
825
928
  nextCursor: response.next_cursor
826
929
  };
827
930
  }
931
+ async queryAllDatabasePages(databaseId, filter) {
932
+ const all = [];
933
+ let cursor;
934
+ do {
935
+ const response = await this.queryDatabase(databaseId, {
936
+ startCursor: cursor,
937
+ pageSize: 100,
938
+ filter
939
+ });
940
+ all.push(...response.results);
941
+ cursor = response.nextCursor ?? void 0;
942
+ } while (cursor);
943
+ return all;
944
+ }
945
+ async getDataSourceId(databaseId) {
946
+ const db = await this.withRateLimit(
947
+ () => this.client.databases.retrieve({ database_id: databaseId })
948
+ );
949
+ const dataSources = db.data_sources;
950
+ return dataSources?.[0]?.id ?? databaseId;
951
+ }
828
952
  async archivePage(pageId) {
829
953
  await this.withRateLimit(() => this.client.pages.update({ page_id: pageId, archived: true }));
830
954
  }
955
+ async getFileBlockUrl(blockId) {
956
+ try {
957
+ const block = await this.withRateLimit(
958
+ () => this.client.blocks.retrieve({ block_id: blockId })
959
+ );
960
+ const b = block;
961
+ const media = b.file ?? b.pdf ?? b.video ?? b.audio ?? b.image;
962
+ return media?.file?.url ?? media?.external?.url ?? null;
963
+ } catch {
964
+ return null;
965
+ }
966
+ }
967
+ async getBlock(blockId) {
968
+ return this.withRateLimit(
969
+ () => this.client.blocks.retrieve({ block_id: blockId })
970
+ );
971
+ }
831
972
  // ─── Block CRUD ───
832
973
  async listChildren(blockId, options) {
833
974
  const response = await this.withRateLimit(
@@ -852,6 +993,35 @@ var NotionClient = class {
852
993
  } while (cursor);
853
994
  return blocks;
854
995
  }
996
+ static CONTAINER_BLOCK_TYPES = /* @__PURE__ */ new Set([
997
+ "column_list",
998
+ "column",
999
+ "callout",
1000
+ "toggle",
1001
+ "quote"
1002
+ ]);
1003
+ async fetchAllChildrenDeep(blockId) {
1004
+ const directChildren = await this.fetchAllChildren(blockId);
1005
+ const allBlocks = [...directChildren];
1006
+ const toExpand = directChildren.filter((b) => {
1007
+ if (!b.has_children) return false;
1008
+ if (b.type === "child_page" || b.type === "child_database") return false;
1009
+ if (_NotionClient.CONTAINER_BLOCK_TYPES.has(b.type)) return true;
1010
+ if (b.type === "synced_block") {
1011
+ const sb = b;
1012
+ return sb.synced_block?.synced_from === null;
1013
+ }
1014
+ return true;
1015
+ });
1016
+ for (const container of toExpand) {
1017
+ try {
1018
+ const nested = await this.fetchAllChildrenDeep(container.id);
1019
+ allBlocks.push(...nested);
1020
+ } catch {
1021
+ }
1022
+ }
1023
+ return allBlocks;
1024
+ }
855
1025
  async appendChildren(blockId, children) {
856
1026
  const BATCH_SIZE = 100;
857
1027
  for (let i = 0; i < children.length; i += BATCH_SIZE) {
@@ -873,15 +1043,18 @@ var NotionClient = class {
873
1043
  () => this.client.fileUploads.create({ filename, content_type: contentType })
874
1044
  );
875
1045
  const fileUploadId = upload.id;
876
- await this.withRateLimit(
1046
+ const sendResult = await this.withRateLimit(
877
1047
  () => this.client.fileUploads.send({
878
1048
  file_upload_id: fileUploadId,
879
1049
  file: { data: fileData, filename }
880
1050
  })
881
1051
  );
882
- await this.withRateLimit(
883
- () => this.client.fileUploads.complete({ file_upload_id: fileUploadId })
884
- );
1052
+ const status = sendResult.status;
1053
+ if (status === "pending") {
1054
+ await this.withRateLimit(
1055
+ () => this.client.fileUploads.complete({ file_upload_id: fileUploadId })
1056
+ );
1057
+ }
885
1058
  return fileUploadId;
886
1059
  }
887
1060
  // ─── Search & Navigation ───
@@ -900,19 +1073,23 @@ var NotionClient = class {
900
1073
  };
901
1074
  }
902
1075
  async getChildPages(parentId) {
903
- const blocks = await this.fetchAllChildren(parentId);
1076
+ const blocks = await this.fetchAllChildrenDeep(parentId);
904
1077
  const childPageBlocks = blocks.filter((b) => b.type === "child_page");
905
1078
  if (childPageBlocks.length === 0) return [];
906
1079
  const pages = await Promise.all(childPageBlocks.map((b) => this.getPage(b.id)));
907
1080
  return pages;
908
1081
  }
1082
+ async getChildDatabaseIds(parentId) {
1083
+ const blocks = await this.fetchAllChildrenDeep(parentId);
1084
+ return blocks.filter((b) => b.type === "child_database").map((b) => b.id);
1085
+ }
909
1086
  async getChildPagesRecursive(parentId) {
910
1087
  const all = [];
911
1088
  let currentLevel = [parentId];
912
1089
  while (currentLevel.length > 0) {
913
- const childArrays = await Promise.all(currentLevel.map((id) => this.getChildPages(id)));
914
1090
  const nextLevel = [];
915
- for (const children of childArrays) {
1091
+ for (const id of currentLevel) {
1092
+ const children = await this.getChildPages(id);
916
1093
  for (const child of children) {
917
1094
  all.push(child);
918
1095
  nextLevel.push(child.id);
@@ -922,6 +1099,76 @@ var NotionClient = class {
922
1099
  }
923
1100
  return all;
924
1101
  }
1102
+ // ─── Views API ───
1103
+ async listDatabaseViews(databaseId) {
1104
+ const allViews = [];
1105
+ let cursor;
1106
+ do {
1107
+ const response = await this.withRateLimit(
1108
+ () => this.client.views.list({
1109
+ database_id: databaseId,
1110
+ start_cursor: cursor,
1111
+ page_size: 100
1112
+ })
1113
+ );
1114
+ const viewRefs = response.results;
1115
+ for (const ref of viewRefs) {
1116
+ try {
1117
+ const detail = await this.getView(ref.id);
1118
+ if (detail) allViews.push(detail);
1119
+ } catch {
1120
+ }
1121
+ }
1122
+ cursor = response.next_cursor;
1123
+ } while (cursor);
1124
+ return allViews;
1125
+ }
1126
+ async getView(viewId) {
1127
+ const view = await this.withRateLimit(() => this.client.views.retrieve({ view_id: viewId }));
1128
+ const v = view;
1129
+ if (!v.type) return null;
1130
+ return parseViewResponse(v);
1131
+ }
1132
+ async getDatabaseViewsConfig(databaseId) {
1133
+ const views = await this.listDatabaseViews(databaseId);
1134
+ return {
1135
+ databaseId,
1136
+ lastSynced: (/* @__PURE__ */ new Date()).toISOString(),
1137
+ views
1138
+ };
1139
+ }
1140
+ // ─── Cover / Icon Extraction ───
1141
+ extractCover(page) {
1142
+ const raw = page;
1143
+ if (!raw.cover) return null;
1144
+ if (raw.cover.type === "file" && raw.cover.file) {
1145
+ return {
1146
+ type: "file",
1147
+ url: raw.cover.file.url,
1148
+ expiryTime: raw.cover.file.expiry_time
1149
+ };
1150
+ }
1151
+ if (raw.cover.type === "external" && raw.cover.external) {
1152
+ return { type: "external", url: raw.cover.external.url };
1153
+ }
1154
+ return null;
1155
+ }
1156
+ extractIcon(page) {
1157
+ const raw = page;
1158
+ if (!raw.icon) return null;
1159
+ switch (raw.icon.type) {
1160
+ case "emoji":
1161
+ return { type: "emoji", value: raw.icon.emoji };
1162
+ case "external":
1163
+ return { type: "external", value: raw.icon.external.url };
1164
+ case "file":
1165
+ return { type: "file", value: raw.icon.file.url };
1166
+ case "icon":
1167
+ return { type: "icon", value: raw.icon.icon.name, color: raw.icon.icon.color };
1168
+ default:
1169
+ return null;
1170
+ }
1171
+ }
925
1172
  // ─── Property Extraction ───
926
1173
  extractTitle(page) {
927
1174
  const props = page.properties;
@@ -937,9 +1184,17 @@ var NotionClient = class {
937
1184
  return this.propertyMapper.fromNotionProperties(page.properties);
938
1185
  }
939
1186
  // ─── Internal ───
1187
+ lastRequestTime = 0;
1188
+ minRequestInterval = 350;
940
1189
  async withRateLimit(fn) {
941
1190
  await this.sema.acquire();
942
1191
  try {
1192
+ const now = Date.now();
1193
+ const elapsed = now - this.lastRequestTime;
1194
+ if (elapsed < this.minRequestInterval) {
1195
+ await sleep(this.minRequestInterval - elapsed);
1196
+ }
1197
+ this.lastRequestTime = Date.now();
943
1198
  return await this.executeWithRetry(fn);
944
1199
  } finally {
945
1200
  this.sema.release();
@@ -980,6 +1235,60 @@ function extractRetryAfter(error) {
980
1235
  function sleep(ms) {
981
1236
  return new Promise((resolve) => setTimeout(resolve, ms));
982
1237
  }
1238
+ function parseViewResponse(v) {
1239
+ const config = {
1240
+ id: v.id,
1241
+ name: v.name,
1242
+ type: v.type,
1243
+ dataSourceId: v.data_source_id,
1244
+ filter: v.filter,
1245
+ sorts: v.sorts?.map((s) => {
1246
+ if ("property" in s) {
1247
+ return { property: s.property, direction: s.direction };
1248
+ }
1249
+ return { timestamp: s.timestamp, direction: s.direction };
1250
+ }),
1251
+ ...extractViewTypeConfig(v.configuration)
1252
+ };
1253
+ return config;
1254
+ }
1255
+ function extractViewTypeConfig(cfg) {
1256
+ if (!cfg) return {};
1257
+ const result = {};
1258
+ if ("properties" in cfg && cfg.properties) {
1259
+ result.properties = cfg.properties.map((p) => ({
1260
+ propertyId: p.property_id,
1261
+ propertyName: p.property_name,
1262
+ visible: p.visible,
1263
+ width: p.width,
1264
+ wrap: p.wrap
1265
+ }));
1266
+ }
1267
+ if ("group_by" in cfg && cfg.group_by) {
1268
+ const gb = cfg.group_by;
1269
+ result.groupBy = {
1270
+ type: gb.type,
1271
+ propertyId: gb.property_id,
1272
+ propertyName: "property_name" in gb ? gb.property_name : void 0,
1273
+ sort: "sort" in gb && gb.sort ? gb.sort.type : void 0,
1274
+ hideEmptyGroups: "hide_empty_groups" in gb ? gb.hide_empty_groups : void 0
1275
+ };
1276
+ }
1277
+ if ("cover" in cfg && cfg.cover) {
1278
+ result.cover = {
1279
+ type: cfg.cover.type,
1280
+ propertyId: cfg.cover.property_id
1281
+ };
1282
+ }
1283
+ if ("cover_size" in cfg) result.coverSize = cfg.cover_size;
1284
+ if ("cover_aspect" in cfg) result.coverAspect = cfg.cover_aspect;
1285
+ if ("date_property_id" in cfg) {
1286
+ result.datePropertyId = cfg.date_property_id;
1287
+ result.datePropertyName = cfg.date_property_name;
1288
+ }
1289
+ if ("view_range" in cfg) result.viewRange = cfg.view_range;
1290
+ return result;
1291
+ }
983
1292
 
984
1293
  // src/notion/block-builder.ts
985
1294
  var VALID_COLORS = /* @__PURE__ */ new Set([
@@ -1485,7 +1794,7 @@ var FrontmatterExtractor = class {
1485
1794
  name = "FrontmatterExtractor";
1486
1795
  order = 10;
1487
1796
  process(input) {
1488
- const { data, content } = matter(input.content);
1797
+ const { data, content } = matter7(input.content);
1489
1798
  return {
1490
1799
  content: content.trim(),
1491
1800
  metadata: {
@@ -1726,7 +2035,7 @@ var PropertiesTableInjector = class {
1726
2035
  if (Object.keys(filteredProps).length === 0) {
1727
2036
  return { content: input.content, metadata: input.metadata };
1728
2037
  }
1729
- const yamlStr = matter.stringify("", filteredProps).trim();
2038
+ const yamlStr = matter7.stringify("", filteredProps).trim();
1730
2039
  const yamlBody = yamlStr.slice(4, -3).trim();
1731
2040
  const codeBlock = "```yaml\n# im-nobsidian:properties\n" + yamlBody + "\n```";
1732
2041
  const content = codeBlock + "\n\n---\n\n" + input.content;
@@ -1884,7 +2193,7 @@ var FrontmatterGenerator = class {
1884
2193
  return { content: input.content, metadata: input.metadata };
1885
2194
  }
1886
2195
  const normalized = normalizeProperties(properties);
1887
- const content = matter.stringify(input.content, normalized);
2196
+ const content = matter7.stringify(input.content, normalized);
1888
2197
  return {
1889
2198
  content,
1890
2199
  metadata: input.metadata
@@ -1902,6 +2211,9 @@ function normalizeProperties(props) {
1902
2211
  return result;
1903
2212
  }
1904
2213
  function normalizeValue(value) {
2214
+ if (value instanceof Date) {
2215
+ return normalizeDate(value.toISOString());
2216
+ }
1905
2217
  if (typeof value === "string") {
1906
2218
  return normalizeDate(value);
1907
2219
  }
@@ -1969,7 +2281,7 @@ ${yamlContent}---
1969
2281
  `;
1970
2282
  let parsed;
1971
2283
  try {
1972
- const result = matter(fakeDocument);
2284
+ const result = matter7(fakeDocument);
1973
2285
  parsed = result.data;
1974
2286
  } catch {
1975
2287
  return { content: input.content, metadata: input.metadata };
@@ -2178,7 +2490,7 @@ var BlockConverter = class {
2178
2490
  const title = richTextToMarkdown(b.toggle?.rich_text);
2179
2491
  const children = b.has_children ? await n2m.pageToMarkdown(b.id) : [];
2180
2492
  const childMd = (Array.isArray(children) ? n2m.toMarkdownString(children).parent : "") ?? "";
2181
- const indented = childMd.split("\n").map((line) => line ? ` ${line}` : "").join("\n").trimEnd();
2493
+ const indented = childMd.split("\n").map((line) => line.trim() ? ` ${line}` : " ").join("\n").trimEnd();
2182
2494
  return `${TOGGLE_START}
2183
2495
  - ${title}
2184
2496
  ${indented}
@@ -2383,7 +2695,7 @@ ${COLUMN_LIST_END}`;
2383
2695
  const calloutType = EMOJI_TO_TYPE[emoji] ?? "note";
2384
2696
  let result = `> [!${calloutType}] ${text}`;
2385
2697
  if (childMd?.trim()) {
2386
- const lines = childMd.trim().split("\n").map((l) => `> ${l}`);
2698
+ const lines = childMd.trim().split("\n").map((l) => l.trim() ? `> ${l}` : ">");
2387
2699
  result += "\n" + lines.join("\n");
2388
2700
  }
2389
2701
  return result;
@@ -2746,35 +3058,49 @@ var ImageHandler = class {
2746
3058
  attachmentFolder;
2747
3059
  notionClient;
2748
3060
  async downloadImage(url, pageTitle) {
2749
- const response = await fetch(url);
2750
- if (!response.ok) {
2751
- throw new Error(`\uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
3061
+ const maxRetries = 3;
3062
+ let lastError = null;
3063
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
3064
+ try {
3065
+ if (attempt > 0) {
3066
+ await new Promise((r) => setTimeout(r, 1e3 * attempt));
3067
+ getLogger().info(
3068
+ `[Im-Nobsidian] \uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC7AC\uC2DC\uB3C4 (${attempt + 1}/${maxRetries}): ${pageTitle}`
3069
+ );
3070
+ }
3071
+ const response = await fetch(url);
3072
+ if (!response.ok) {
3073
+ throw new Error(`\uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
3074
+ }
3075
+ const contentType = response.headers.get("content-type") ?? "image/png";
3076
+ const extension = IMAGE_EXTENSIONS[contentType] ?? this.guessExtension(url) ?? ".png";
3077
+ const buffer = Buffer.from(await response.arrayBuffer());
3078
+ const hash = createHash("sha256").update(buffer).digest("hex").slice(0, 12);
3079
+ const safeName = pageTitle.replace(/[^a-zA-Z0-9가-힣\s-]/g, "").replace(/\s+/g, "-").slice(0, 50);
3080
+ const fileName = `${safeName}-${hash}${extension}`;
3081
+ const localPath = `${this.attachmentFolder}/${fileName}`;
3082
+ await this.vaultFs.ensureFolder(this.attachmentFolder);
3083
+ await this.vaultFs.writeBinary(localPath, buffer);
3084
+ return { originalUrl: url, localPath, hash, size: buffer.length };
3085
+ } catch (error) {
3086
+ lastError = error instanceof Error ? error : new Error(String(error));
3087
+ }
2752
3088
  }
2753
- const contentType = response.headers.get("content-type") ?? "image/png";
2754
- const extension = IMAGE_EXTENSIONS[contentType] ?? this.guessExtension(url) ?? ".png";
2755
- const buffer = Buffer.from(await response.arrayBuffer());
2756
- const hash = createHash("sha256").update(buffer).digest("hex").slice(0, 12);
2757
- const safeName = pageTitle.replace(/[^a-zA-Z0-9가-힣\s-]/g, "").replace(/\s+/g, "-").slice(0, 50);
2758
- const fileName = `${safeName}-${hash}${extension}`;
2759
- const localPath = `${this.attachmentFolder}/${fileName}`;
2760
- await this.vaultFs.ensureFolder(this.attachmentFolder);
2761
- await this.vaultFs.writeBinary(localPath, buffer);
2762
- return {
2763
- originalUrl: url,
2764
- localPath,
2765
- hash,
2766
- size: buffer.length
2767
- };
3089
+ throw lastError;
2768
3090
  }
2769
3091
  async downloadAllImages(markdown, pageTitle) {
2770
3092
  const imageRegex = /!\[([^\]]*)\]\((https?:\/\/[^)]+)\)/g;
2771
3093
  const matches = [...markdown.matchAll(imageRegex)];
2772
3094
  const notionMatches = matches.filter((m) => m[2] && this.isNotionImageUrl(m[2]));
2773
- if (notionMatches.length === 0) {
3095
+ const internalImageRegex = /!\[([^\]]*)\]\((file:\/\/%7B.*?%7D%7D)\)/g;
3096
+ const internalMatches = [...markdown.matchAll(internalImageRegex)];
3097
+ if (notionMatches.length === 0 && internalMatches.length === 0) {
2774
3098
  return { content: markdown, downloads: [] };
2775
3099
  }
2776
3100
  const sema = new Sema(3);
2777
- const results = await Promise.allSettled(
3101
+ const downloads = [];
3102
+ let result = markdown;
3103
+ const httpResults = await Promise.allSettled(
2778
3104
  notionMatches.map(async (match) => {
2779
3105
  await sema.acquire();
2780
3106
  try {
@@ -2787,15 +3113,34 @@ var ImageHandler = class {
2787
3113
  }
2788
3114
  })
2789
3115
  );
2790
- const downloads = [];
2791
- let result = markdown;
2792
- for (const settled of results) {
3116
+ for (const settled of httpResults) {
2793
3117
  if (settled.status !== "fulfilled") continue;
2794
3118
  const { match, download } = settled.value;
2795
3119
  downloads.push(download);
2796
3120
  const obsidianEmbed = `![[${download.localPath}${match[1] ? `|${match[1]}` : ""}]]`;
2797
3121
  result = result.replace(match[0], obsidianEmbed);
2798
3122
  }
3123
+ for (const match of internalMatches) {
3124
+ await sema.acquire();
3125
+ try {
3126
+ const fileUrl = match[2];
3127
+ const parsed = this.parseNotionFileUrl(fileUrl);
3128
+ if (!parsed || !this.notionClient) continue;
3129
+ const realUrl = await this.notionClient.getFileBlockUrl(parsed.blockId);
3130
+ if (!realUrl) {
3131
+ getLogger().warn(`[Im-Nobsidian] \uC774\uBBF8\uC9C0 Block URL \uC870\uD68C \uC2E4\uD328: ${parsed.fileName}`);
3132
+ continue;
3133
+ }
3134
+ const download = await this.downloadImage(realUrl, pageTitle);
3135
+ downloads.push(download);
3136
+ const obsidianEmbed = `![[${download.localPath}${match[1] ? `|${match[1]}` : ""}]]`;
3137
+ result = result.replace(match[0], obsidianEmbed);
3138
+ } catch (error) {
3139
+ getLogger().warn(`\uB0B4\uBD80 \uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
3140
+ } finally {
3141
+ sema.release();
3142
+ }
3143
+ }
2799
3144
  return { content: result, downloads };
2800
3145
  }
2801
3146
  async uploadLocalImage(localPath) {
@@ -2846,7 +3191,145 @@ var ImageHandler = class {
2846
3191
  const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
2847
3192
  return (ext && EXTENSION_TO_MIME[ext]) ?? "application/octet-stream";
2848
3193
  }
3194
+ async downloadAllFiles(markdown, pageTitle) {
3195
+ const mdLinkRegex = /\[(?:📎|🎬|🎞|📄|🔊)\s*([^\]]*)\]\((https?:\/\/[^)]+)\)/g;
3196
+ const mdHttpMatches = [...markdown.matchAll(mdLinkRegex)];
3197
+ const notionHttpMatches = mdHttpMatches.filter((m) => m[2] && this.isNotionFileUrl(m[2]));
3198
+ const mdInternalRegex = /\[(?:📎|🎬|🎞|📄|🔊)\s*([^\]]*)\]\((file:\/\/%7B.*?%7D%7D)\)/g;
3199
+ const mdInternalMatches = [...markdown.matchAll(mdInternalRegex)];
3200
+ const xmlHttpRegex = /<(?:file|video|audio|pdf)\s+src="(https?:\/\/[^"]+)">([\s\S]*?)<\/(?:file|video|audio|pdf)>/g;
3201
+ const xmlHttpMatches = [...markdown.matchAll(xmlHttpRegex)];
3202
+ const notionXmlHttpMatches = xmlHttpMatches.filter((m) => m[1] && this.isNotionFileUrl(m[1]));
3203
+ const xmlInternalRegex = /<(?:file|video|audio|pdf)\s+src="(file:\/\/%7B.*?%7D%7D)">([\s\S]*?)<\/(?:file|video|audio|pdf)>/g;
3204
+ const xmlInternalMatches = [...markdown.matchAll(xmlInternalRegex)];
3205
+ const totalMatches = notionHttpMatches.length + mdInternalMatches.length + notionXmlHttpMatches.length + xmlInternalMatches.length;
3206
+ if (totalMatches === 0) {
3207
+ return { content: markdown, downloads: [] };
3208
+ }
3209
+ const sema = new Sema(3);
3210
+ const downloads = [];
3211
+ let result = markdown;
3212
+ for (const match of notionHttpMatches) {
3213
+ await sema.acquire();
3214
+ try {
3215
+ const url = match[2];
3216
+ const caption = match[1]?.trim() || "file";
3217
+ const download = await this.downloadFile(url, pageTitle, caption);
3218
+ downloads.push(download);
3219
+ const obsidianLink = `[[${download.localPath}|${caption}]]`;
3220
+ result = result.replace(match[0], obsidianLink);
3221
+ } catch (error) {
3222
+ getLogger().warn(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
3223
+ } finally {
3224
+ sema.release();
3225
+ }
3226
+ }
3227
+ for (const match of mdInternalMatches) {
3228
+ await sema.acquire();
3229
+ try {
3230
+ const fileUrl = match[2];
3231
+ const parsed = this.parseNotionFileUrl(fileUrl);
3232
+ if (!parsed || !this.notionClient) continue;
3233
+ const blockId = parsed.blockId;
3234
+ const fileName = parsed.fileName;
3235
+ const realUrl = await this.notionClient.getFileBlockUrl(blockId);
3236
+ if (!realUrl) continue;
3237
+ const caption = fileName || match[1]?.trim() || "file";
3238
+ const download = await this.downloadFile(realUrl, pageTitle, caption);
3239
+ downloads.push(download);
3240
+ const obsidianLink = `[[${download.localPath}|${caption}]]`;
3241
+ result = result.replace(match[0], obsidianLink);
3242
+ } catch (error) {
3243
+ getLogger().warn(`\uB0B4\uBD80 \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
3244
+ } finally {
3245
+ sema.release();
3246
+ }
3247
+ }
3248
+ for (const match of notionXmlHttpMatches) {
3249
+ await sema.acquire();
3250
+ try {
3251
+ const url = match[1];
3252
+ const caption = match[2]?.trim() || "file";
3253
+ const download = await this.downloadFile(url, pageTitle, caption);
3254
+ downloads.push(download);
3255
+ const obsidianLink = `[[${download.localPath}|${caption}]]`;
3256
+ result = result.replace(match[0], obsidianLink);
3257
+ } catch (error) {
3258
+ getLogger().warn(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
3259
+ } finally {
3260
+ sema.release();
3261
+ }
3262
+ }
3263
+ for (const match of xmlInternalMatches) {
3264
+ await sema.acquire();
3265
+ try {
3266
+ const fileUrl = match[1];
3267
+ const parsed = this.parseNotionFileUrl(fileUrl);
3268
+ if (!parsed || !this.notionClient) continue;
3269
+ const blockId = parsed.blockId;
3270
+ const fileName = parsed.fileName;
3271
+ const realUrl = await this.notionClient.getFileBlockUrl(blockId);
3272
+ if (!realUrl) continue;
3273
+ const caption = fileName || match[2]?.trim() || "file";
3274
+ const download = await this.downloadFile(realUrl, pageTitle, caption);
3275
+ downloads.push(download);
3276
+ const obsidianLink = `[[${download.localPath}|${caption}]]`;
3277
+ result = result.replace(match[0], obsidianLink);
3278
+ } catch (error) {
3279
+ getLogger().warn(`\uB0B4\uBD80 \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
3280
+ } finally {
3281
+ sema.release();
3282
+ }
3283
+ }
3284
+ return { content: result, downloads };
3285
+ }
3286
+ parseNotionFileUrl(fileUrl) {
3287
+ try {
3288
+ const decoded = decodeURIComponent(fileUrl.replace("file://", ""));
3289
+ const json = JSON.parse(decoded);
3290
+ const blockId = json.permissionRecord?.id ?? "";
3291
+ const source = json.source ?? "";
3292
+ const fileNameMatch = source.match(/attachment:[^:]+:(.+)$/);
3293
+ const fileName = fileNameMatch ? decodeURIComponent(fileNameMatch[1]) : "";
3294
+ return blockId ? { blockId, fileName } : null;
3295
+ } catch {
3296
+ return null;
3297
+ }
3298
+ }
3299
+ async downloadFile(url, _pageTitle, caption) {
3300
+ const maxRetries = 3;
3301
+ let lastError = null;
3302
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
3303
+ try {
3304
+ if (attempt > 0) {
3305
+ await new Promise((r) => setTimeout(r, 1e3 * attempt));
3306
+ getLogger().info(
3307
+ `[Im-Nobsidian] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC7AC\uC2DC\uB3C4 (${attempt + 1}/${maxRetries}): ${caption}`
3308
+ );
3309
+ }
3310
+ const response = await fetch(url);
3311
+ if (!response.ok) {
3312
+ throw new Error(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
3313
+ }
3314
+ const buffer = Buffer.from(await response.arrayBuffer());
3315
+ const hash = createHash("sha256").update(buffer).digest("hex").slice(0, 12);
3316
+ const extension = this.guessFileExtension(url, response.headers.get("content-type"));
3317
+ const safeName = caption.replace(/[^a-zA-Z0-9가-힣\s._-]/g, "").replace(/\s+/g, "-").slice(0, 80);
3318
+ const fileName = `${safeName}-${hash}${extension}`;
3319
+ const localPath = `${this.attachmentFolder}/${fileName}`;
3320
+ await this.vaultFs.ensureFolder(this.attachmentFolder);
3321
+ await this.vaultFs.writeBinary(localPath, buffer);
3322
+ return { originalUrl: url, localPath, hash, size: buffer.length };
3323
+ } catch (error) {
3324
+ lastError = error instanceof Error ? error : new Error(String(error));
3325
+ }
3326
+ }
3327
+ throw lastError;
3328
+ }
2849
3329
  isNotionImageUrl(url) {
3330
+ return this.isNotionFileUrl(url);
3331
+ }
3332
+ isNotionFileUrl(url) {
2850
3333
  return url.includes("secure.notion-static.com") || url.includes("prod-files-secure") || url.includes("s3.us-west-2.amazonaws.com/secure.notion-static.com");
2851
3334
  }
2852
3335
  guessExtension(url) {
@@ -2854,63 +3337,344 @@ var ImageHandler = class {
2854
3337
  const ext = pathname.match(/\.(png|jpg|jpeg|gif|webp|svg|bmp|tiff)(\?|$)/i);
2855
3338
  return ext ? `.${ext[1].toLowerCase()}` : null;
2856
3339
  }
3340
+ guessFileExtension(url, contentType) {
3341
+ try {
3342
+ const pathname = new URL(url).pathname;
3343
+ const extMatch = pathname.match(/\.([a-zA-Z0-9]{1,10})(?:\?|$)/);
3344
+ if (extMatch) return `.${extMatch[1].toLowerCase()}`;
3345
+ } catch {
3346
+ }
3347
+ const mimeMap = {
3348
+ "application/pdf": ".pdf",
3349
+ "application/zip": ".zip",
3350
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
3351
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
3352
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
3353
+ "application/x-hwp": ".hwp",
3354
+ "application/gzip": ".gz"
3355
+ };
3356
+ if (contentType && mimeMap[contentType]) return mimeMap[contentType];
3357
+ return "";
3358
+ }
2857
3359
  };
2858
-
2859
- // src/utils/sanitize.ts
2860
- var INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/g;
2861
- var TRAILING_DOTS = /\.+$/;
2862
- var RESERVED_NAMES = /* @__PURE__ */ new Set([
2863
- "CON",
2864
- "PRN",
2865
- "AUX",
2866
- "NUL",
2867
- "COM1",
2868
- "COM2",
2869
- "COM3",
2870
- "COM4",
2871
- "COM5",
2872
- "COM6",
2873
- "COM7",
2874
- "COM8",
2875
- "COM9",
2876
- "LPT1",
2877
- "LPT2",
2878
- "LPT3",
2879
- "LPT4",
2880
- "LPT5",
2881
- "LPT6",
2882
- "LPT7",
2883
- "LPT8",
2884
- "LPT9"
2885
- ]);
2886
- function sanitizeFileName(name) {
2887
- let sanitized = name.replace(INVALID_CHARS, "_").replace(TRAILING_DOTS, "").trim().slice(0, 200);
2888
- if (RESERVED_NAMES.has(sanitized.toUpperCase())) {
2889
- sanitized = `_${sanitized}`;
3360
+ var EXTENSION_TO_BLOCK_TYPE = {
3361
+ ".png": "image",
3362
+ ".jpg": "image",
3363
+ ".jpeg": "image",
3364
+ ".gif": "image",
3365
+ ".svg": "image",
3366
+ ".webp": "image",
3367
+ ".ico": "image",
3368
+ ".bmp": "image",
3369
+ ".tiff": "image",
3370
+ ".tif": "image",
3371
+ ".avif": "image",
3372
+ ".apng": "image",
3373
+ ".heic": "image",
3374
+ ".pdf": "pdf",
3375
+ ".mp4": "video",
3376
+ ".mov": "video",
3377
+ ".webm": "video",
3378
+ ".avi": "video",
3379
+ ".mkv": "video",
3380
+ ".flv": "video",
3381
+ ".wmv": "video",
3382
+ ".m4v": "video",
3383
+ ".mpeg": "video",
3384
+ ".ogv": "video",
3385
+ ".3gp": "video",
3386
+ ".mp3": "audio",
3387
+ ".wav": "audio",
3388
+ ".ogg": "audio",
3389
+ ".m4a": "audio",
3390
+ ".flac": "audio",
3391
+ ".aac": "audio",
3392
+ ".wma": "audio",
3393
+ ".opus": "audio",
3394
+ ".weba": "audio"
3395
+ };
3396
+ var EXTENSION_TO_MIME2 = {
3397
+ ".png": "image/png",
3398
+ ".jpg": "image/jpeg",
3399
+ ".jpeg": "image/jpeg",
3400
+ ".gif": "image/gif",
3401
+ ".webp": "image/webp",
3402
+ ".svg": "image/svg+xml",
3403
+ ".bmp": "image/bmp",
3404
+ ".ico": "image/x-icon",
3405
+ ".tiff": "image/tiff",
3406
+ ".tif": "image/tiff",
3407
+ ".avif": "image/avif",
3408
+ ".heic": "image/heic",
3409
+ ".pdf": "application/pdf",
3410
+ ".mp4": "video/mp4",
3411
+ ".mov": "video/quicktime",
3412
+ ".webm": "video/webm",
3413
+ ".avi": "video/x-msvideo",
3414
+ ".mkv": "video/x-matroska",
3415
+ ".mp3": "audio/mpeg",
3416
+ ".wav": "audio/wav",
3417
+ ".ogg": "audio/ogg",
3418
+ ".m4a": "audio/mp4",
3419
+ ".flac": "audio/flac",
3420
+ ".aac": "audio/aac",
3421
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
3422
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
3423
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
3424
+ ".zip": "application/zip",
3425
+ ".gz": "application/gzip",
3426
+ ".tar": "application/x-tar",
3427
+ ".rar": "application/vnd.rar",
3428
+ ".7z": "application/x-7z-compressed",
3429
+ ".py": "text/x-python",
3430
+ ".js": "text/javascript",
3431
+ ".ts": "text/typescript",
3432
+ ".json": "application/json",
3433
+ ".csv": "text/csv",
3434
+ ".txt": "text/plain",
3435
+ ".html": "text/html",
3436
+ ".xml": "application/xml",
3437
+ ".hwp": "application/x-hwp",
3438
+ ".xls": "application/vnd.ms-excel",
3439
+ ".doc": "application/msword",
3440
+ ".ppt": "application/vnd.ms-powerpoint"
3441
+ };
3442
+ function getBlockType(filename) {
3443
+ const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
3444
+ if (!ext) return "file";
3445
+ return EXTENSION_TO_BLOCK_TYPE[ext] ?? "file";
3446
+ }
3447
+ function getMimeType(filename) {
3448
+ const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
3449
+ if (!ext) return "application/octet-stream";
3450
+ return EXTENSION_TO_MIME2[ext] ?? "application/octet-stream";
3451
+ }
3452
+ function getFolderPath(filePath) {
3453
+ const parts = filePath.split("/");
3454
+ return parts.length > 1 ? parts.slice(0, -1).join("/") : "";
3455
+ }
3456
+ var FileHandler = class {
3457
+ constructor(vaultFs, notionClient, stateDb) {
3458
+ this.vaultFs = vaultFs;
3459
+ this.notionClient = notionClient;
3460
+ this.stateDb = stateDb;
2890
3461
  }
2891
- if (sanitized.length === 0) {
2892
- sanitized = "_untitled";
3462
+ vaultFs;
3463
+ notionClient;
3464
+ stateDb;
3465
+ sema = new Sema(2);
3466
+ async pushFilesForFolder(folderPageId, folderPath) {
3467
+ const allFiles = await this.vaultFs.listNonMarkdownFiles();
3468
+ const folderFiles = allFiles.filter((f) => getFolderPath(f.path) === folderPath);
3469
+ if (folderFiles.length === 0) return [];
3470
+ const results = [];
3471
+ for (const file of folderFiles) {
3472
+ if (this.stateDb.isFileRegistered(file.path)) {
3473
+ const existing = this.stateDb.getFileRegistry(file.path);
3474
+ if (existing) {
3475
+ const buffer = await this.vaultFs.readBinary(file.path);
3476
+ const currentHash = createHash("sha256").update(buffer).digest("hex");
3477
+ if (existing.fileHash === currentHash) continue;
3478
+ }
3479
+ }
3480
+ await this.sema.acquire();
3481
+ try {
3482
+ const result = await this.uploadFile(file, folderPageId);
3483
+ if (result) results.push(result);
3484
+ } catch (error) {
3485
+ getLogger().warn(`\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC2E4\uD328 (${file.path}):`, error);
3486
+ } finally {
3487
+ this.sema.release();
3488
+ }
3489
+ }
3490
+ return results;
2893
3491
  }
2894
- return sanitized;
2895
- }
2896
-
2897
- // src/converter/enhanced-md-converter.ts
2898
- var NOTION_CALLOUT_RE = /^::: callout\n([\s\S]*?)\n:::/gm;
2899
- var NOTION_TOGGLE_RE = /<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gs;
3492
+ async pushAllFiles() {
3493
+ const allFiles = await this.vaultFs.listNonMarkdownFiles();
3494
+ if (allFiles.length === 0) return [];
3495
+ const filesByFolder = /* @__PURE__ */ new Map();
3496
+ for (const file of allFiles) {
3497
+ const folder = getFolderPath(file.path);
3498
+ const list = filesByFolder.get(folder) ?? [];
3499
+ list.push(file);
3500
+ filesByFolder.set(folder, list);
3501
+ }
3502
+ const results = [];
3503
+ for (const [folderPath, files] of filesByFolder) {
3504
+ const folderRecord = folderPath ? this.stateDb.getByPath(folderPath) : this.stateDb.getByPath("__root__");
3505
+ const folderPageId = folderRecord?.notionPageId;
3506
+ if (!folderPageId) {
3507
+ getLogger().warn(
3508
+ `\uD3F4\uB354 \uD398\uC774\uC9C0 \uC5C6\uC74C, \uD30C\uC77C \uC2A4\uD0B5: ${folderPath || "(\uB8E8\uD2B8)"} (${files.length}\uAC1C)`
3509
+ );
3510
+ continue;
3511
+ }
3512
+ for (const file of files) {
3513
+ if (this.stateDb.isFileRegistered(file.path)) {
3514
+ const existing = this.stateDb.getFileRegistry(file.path);
3515
+ if (existing) {
3516
+ const buffer = await this.vaultFs.readBinary(file.path);
3517
+ const currentHash = createHash("sha256").update(buffer).digest("hex");
3518
+ if (existing.fileHash === currentHash) continue;
3519
+ }
3520
+ }
3521
+ await this.sema.acquire();
3522
+ try {
3523
+ const result = await this.uploadFile(file, folderPageId);
3524
+ if (result) results.push(result);
3525
+ } catch (error) {
3526
+ getLogger().warn(`\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC2E4\uD328 (${file.path}):`, error);
3527
+ } finally {
3528
+ this.sema.release();
3529
+ }
3530
+ }
3531
+ }
3532
+ return results;
3533
+ }
3534
+ async downloadFileBlock(url, filename, targetFolder) {
3535
+ const response = await fetch(url);
3536
+ if (!response.ok) {
3537
+ throw new Error(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${response.status} ${response.statusText}`);
3538
+ }
3539
+ const buffer = Buffer.from(await response.arrayBuffer());
3540
+ const hash = createHash("sha256").update(buffer).digest("hex");
3541
+ const localPath = targetFolder ? `${targetFolder}/${filename}` : filename;
3542
+ await this.vaultFs.ensureFolder(targetFolder || ".");
3543
+ await this.vaultFs.writeBinary(localPath, buffer);
3544
+ return { url, localPath, hash, size: buffer.length };
3545
+ }
3546
+ async downloadFileBlocks(blocks, targetFolder) {
3547
+ const results = [];
3548
+ for (const block of blocks) {
3549
+ const filename = this.extractFilename(block.url, block.caption);
3550
+ if (this.stateDb.isFileRegistered(`${targetFolder}/${filename}`)) continue;
3551
+ await this.sema.acquire();
3552
+ try {
3553
+ const result = await this.downloadFileBlock(block.url, filename, targetFolder);
3554
+ results.push(result);
3555
+ } catch (error) {
3556
+ getLogger().warn(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328 (${block.url}):`, error);
3557
+ } finally {
3558
+ this.sema.release();
3559
+ }
3560
+ }
3561
+ return results;
3562
+ }
3563
+ async uploadFile(file, parentPageId) {
3564
+ const buffer = await this.vaultFs.readBinary(file.path);
3565
+ const filename = file.path.split("/").pop() ?? "file";
3566
+ const contentType = getMimeType(filename);
3567
+ const blockType = getBlockType(filename);
3568
+ const hash = createHash("sha256").update(buffer).digest("hex");
3569
+ const blob = new Blob([buffer], { type: contentType });
3570
+ const fileUploadId = await this.notionClient.uploadFile(blob, filename, contentType);
3571
+ const block = this.createFileBlock(blockType, fileUploadId);
3572
+ await this.notionClient.appendChildren(parentPageId, [block]);
3573
+ this.stateDb.registerFile({
3574
+ localPath: file.path,
3575
+ notionPageId: parentPageId,
3576
+ fileUploadId,
3577
+ fileType: blockType,
3578
+ fileHash: hash,
3579
+ fileSize: file.size
3580
+ });
3581
+ getLogger().info(`\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC: ${file.path} \u2192 ${blockType} \uBE14\uB85D`);
3582
+ return { localPath: file.path, fileUploadId, blockType };
3583
+ }
3584
+ createFileBlock(blockType, fileUploadId) {
3585
+ const fileRef = {
3586
+ type: "file_upload",
3587
+ file_upload: { id: fileUploadId }
3588
+ };
3589
+ switch (blockType) {
3590
+ case "image":
3591
+ return { type: "image", image: fileRef };
3592
+ case "pdf":
3593
+ return { type: "pdf", pdf: fileRef };
3594
+ case "video":
3595
+ return { type: "video", video: fileRef };
3596
+ case "audio":
3597
+ return { type: "audio", audio: fileRef };
3598
+ case "file":
3599
+ default:
3600
+ return { type: "file", file: fileRef };
3601
+ }
3602
+ }
3603
+ extractFilename(url, caption) {
3604
+ if (caption) {
3605
+ const safe = caption.replace(/[^a-zA-Z0-9가-힣._\s-]/g, "").trim();
3606
+ if (safe) return safe;
3607
+ }
3608
+ try {
3609
+ const pathname = new URL(url).pathname;
3610
+ const segments = pathname.split("/");
3611
+ const last = segments[segments.length - 1];
3612
+ if (last && last.includes(".")) return decodeURIComponent(last);
3613
+ } catch {
3614
+ }
3615
+ const ext = url.match(/\.(png|jpg|jpeg|gif|pdf|mp4|mp3|docx|xlsx|zip|webp)(\?|$)/i);
3616
+ return `file-${Date.now()}${ext ? `.${ext[1]}` : ""}`;
3617
+ }
3618
+ };
3619
+
3620
+ // src/utils/sanitize.ts
3621
+ var INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/g;
3622
+ var TRAILING_DOTS = /\.+$/;
3623
+ var RESERVED_NAMES = /* @__PURE__ */ new Set([
3624
+ "CON",
3625
+ "PRN",
3626
+ "AUX",
3627
+ "NUL",
3628
+ "COM1",
3629
+ "COM2",
3630
+ "COM3",
3631
+ "COM4",
3632
+ "COM5",
3633
+ "COM6",
3634
+ "COM7",
3635
+ "COM8",
3636
+ "COM9",
3637
+ "LPT1",
3638
+ "LPT2",
3639
+ "LPT3",
3640
+ "LPT4",
3641
+ "LPT5",
3642
+ "LPT6",
3643
+ "LPT7",
3644
+ "LPT8",
3645
+ "LPT9"
3646
+ ]);
3647
+ function sanitizeFileName(name) {
3648
+ let sanitized = name.replace(INVALID_CHARS, "_").replace(TRAILING_DOTS, "").trim().slice(0, 200);
3649
+ if (RESERVED_NAMES.has(sanitized.toUpperCase())) {
3650
+ sanitized = `_${sanitized}`;
3651
+ }
3652
+ if (sanitized.length === 0) {
3653
+ sanitized = "_untitled";
3654
+ }
3655
+ return sanitized;
3656
+ }
3657
+
3658
+ // src/converter/enhanced-md-converter.ts
3659
+ var NOTION_CALLOUT_RE = /^::: callout\n([\s\S]*?)\n:::/gm;
3660
+ var NOTION_CALLOUT_TAG_RE = /<callout[^>]*>\n?([\s\S]*?)<\/callout>/g;
3661
+ var NOTION_TOGGLE_RE = /[\t ]*<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gs;
2900
3662
  var NOTION_PAGE_MENTION_RE = /<mention-page id="([^"]+)">([\s\S]*?)<\/mention-page>/g;
2901
3663
  var NOTION_USER_MENTION_RE = /<mention-user id="[^"]*">([^<]*)<\/mention-user>/g;
2902
3664
  var NOTION_DATE_MENTION_RE = /<mention-date start="([^"]*)"(?: end="([^"]*)")?[^>]*\/>/g;
2903
3665
  var NOTION_UNKNOWN_RE = /<unknown id="([^"]*)"([^>]*)\/>/g;
2904
- var NOTION_AUDIO_RE = /<audio src="([^"]*)">([\s\S]*?)<\/audio>/g;
2905
- var NOTION_VIDEO_RE = /<video src="([^"]*)">([\s\S]*?)<\/video>/g;
2906
- var NOTION_PDF_RE = /<pdf src="([^"]*)">([\s\S]*?)<\/pdf>/g;
2907
- var NOTION_FILE_RE = /<file src="([^"]*)">([\s\S]*?)<\/file>/g;
3666
+ var NOTION_AUDIO_RE = /[\t ]*<audio src="([^"]*)">([\s\S]*?)<\/audio>/g;
3667
+ var NOTION_VIDEO_RE = /[\t ]*<video src="([^"]*)">([\s\S]*?)<\/video>/g;
3668
+ var NOTION_PDF_RE = /[\t ]*<pdf src="([^"]*)">([\s\S]*?)<\/pdf>/g;
3669
+ var NOTION_FILE_RE = /[\t ]*<file src="([^"]*)">([\s\S]*?)<\/file>/g;
2908
3670
  var NOTION_TAB_RE = /<tab title="([^"]*)">([\s\S]*?)<\/tab>/g;
2909
3671
  var NOTION_UNDERLINE_RE = /<span underline="true">([\s\S]*?)<\/span>/g;
2910
3672
  var TOGGLE_START2 = "%%im-nobsidian:toggle:start%%";
2911
3673
  var TOGGLE_END2 = "%%im-nobsidian:toggle:end%%";
2912
3674
  function notionEnhancedToObsidian(enhanced) {
2913
3675
  let result = enhanced;
3676
+ result = convertSyncedBlockRef(result);
3677
+ result = normalizeCodeBlockToggles(result);
2914
3678
  result = convertToggles(result);
2915
3679
  result = convertCallouts(result);
2916
3680
  result = convertPageMentions(result);
@@ -2924,8 +3688,13 @@ function notionEnhancedToObsidian(enhanced) {
2924
3688
  result = convertNotionTables(result);
2925
3689
  result = convertColorSpans(result);
2926
3690
  result = convertUnderlineSpans(result);
3691
+ result = convertColumnBlocks(result);
3692
+ result = convertDatabaseBlocks(result);
3693
+ result = cleanInlineColorAttrs(result);
2927
3694
  result = removeEmptyBlocks(result);
2928
3695
  result = unescapePipes(result);
3696
+ result = unescapeNotionChars(result);
3697
+ result = ensureCalloutContinuity(result);
2929
3698
  return result;
2930
3699
  }
2931
3700
  function obsidianToNotionEnhanced(obsidian) {
@@ -2939,39 +3708,62 @@ function obsidianToNotionEnhanced(obsidian) {
2939
3708
  result = restoreUnderlineSpans(result);
2940
3709
  return result;
2941
3710
  }
3711
+ function normalizeCodeBlockToggles(content) {
3712
+ return content.replace(
3713
+ /^- (.+)\n\t```(\w*)\n([\s\S]*?)\n\t```$/gm,
3714
+ (_match, title, lang, body) => {
3715
+ const langTag = lang ? lang : "";
3716
+ return `<details>
3717
+ <summary>${title.trim()}</summary>
3718
+ \`\`\`${langTag}
3719
+ ${body}
3720
+ \`\`\`
3721
+ </details>`;
3722
+ }
3723
+ );
3724
+ }
2942
3725
  function convertToggles(content) {
2943
3726
  function replaceToggle(match) {
2944
- const m = /<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/s.exec(match);
3727
+ const m = /[\t ]*<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/s.exec(match);
2945
3728
  if (!m) return match;
2946
3729
  const title = m[1].trim();
2947
3730
  let body = m[2].trim();
2948
3731
  body = convertToggles(body);
2949
- const indented = body.split("\n").map((line) => line ? ` ${line}` : "").join("\n").trimEnd();
2950
- return `${TOGGLE_START2}
2951
- - ${title}
2952
- ${indented}
2953
- ${TOGGLE_END2}`;
3732
+ const calloutBody = body.split("\n").map((line) => line ? `> ${line}` : ">").join("\n");
3733
+ return `> [!toggle]- ${title}
3734
+ ${calloutBody}`;
2954
3735
  }
2955
3736
  return content.replace(NOTION_TOGGLE_RE, replaceToggle);
2956
3737
  }
3738
+ function calloutBodyToObsidian(body) {
3739
+ const lines = body.split("\n").map((l) => l.replace(/^\t/, "")).filter((l, i, arr) => !(i === 0 && l === "") && !(i === arr.length - 1 && l === ""));
3740
+ const firstLine = lines[0] ?? "";
3741
+ const emojiMatch = /^([\p{Emoji}️‍]+)\s*(.*)/u.exec(firstLine);
3742
+ const type = emojiMatch ? emojiToCalloutType(emojiMatch[1]) : "note";
3743
+ const title = emojiMatch ? emojiMatch[2] : firstLine;
3744
+ const rest = lines.slice(1).join("\n").trim();
3745
+ const calloutTitle = title ? `> [!${type}] ${title}` : `> [!${type}]`;
3746
+ const calloutBody = rest ? "\n" + rest.split("\n").map((line) => line.trim() ? `> ${line}` : ">").join("\n") : "";
3747
+ return calloutTitle + calloutBody;
3748
+ }
2957
3749
  function convertCallouts(content) {
2958
- return content.replace(NOTION_CALLOUT_RE, (_match, body) => {
2959
- const lines = body.trim().split("\n");
2960
- const firstLine = lines[0] ?? "";
2961
- const emojiMatch = /^([\p{Emoji}️‍]+)\s*(.*)/u.exec(firstLine);
2962
- const type = emojiMatch ? emojiToCalloutType(emojiMatch[1]) : "note";
2963
- const title = emojiMatch ? emojiMatch[2] : firstLine;
2964
- const rest = lines.slice(1).join("\n").trim();
2965
- const calloutTitle = title ? `> [!${type}] ${title}` : `> [!${type}]`;
2966
- const calloutBody = rest ? "\n" + rest.split("\n").map((line) => `> ${line}`).join("\n") : "";
2967
- return calloutTitle + calloutBody;
2968
- });
3750
+ let result = content.replace(
3751
+ NOTION_CALLOUT_TAG_RE,
3752
+ (_match, body) => calloutBodyToObsidian(body)
3753
+ );
3754
+ result = result.replace(NOTION_CALLOUT_RE, (_match, body) => calloutBodyToObsidian(body));
3755
+ return result;
2969
3756
  }
2970
3757
  function convertPageMentions(content) {
2971
- return content.replace(NOTION_PAGE_MENTION_RE, (_match, _id, text) => {
3758
+ let result = content.replace(NOTION_PAGE_MENTION_RE, (_match, _id, text) => {
2972
3759
  const cleaned = text.trim();
2973
3760
  return `[[${cleaned}]]`;
2974
3761
  });
3762
+ result = result.replace(
3763
+ /<mention-page\s+url="https?:\/\/(?:www\.)?notion\.so\/([a-f0-9]{32})"[^>]*\/>/g,
3764
+ (_match, id) => `[[notion:${id}]]`
3765
+ );
3766
+ return result;
2975
3767
  }
2976
3768
  function convertUserMentions(content) {
2977
3769
  return content.replace(NOTION_USER_MENTION_RE, (_match, name) => `@${name}`);
@@ -2989,6 +3781,20 @@ function preserveUnknownBlocks(content) {
2989
3781
  return `%%im-nobsidian:unknown:id=${id}&type=${blockType}%%`;
2990
3782
  });
2991
3783
  }
3784
+ function convertSyncedBlockRef(content) {
3785
+ let result = content.replace(
3786
+ /<synced_block_reference[^>]*>[\s\S]*?<\/synced_block_reference>/g,
3787
+ (match) => {
3788
+ const inner = match.replace(/<synced_block_reference[^>]*>\n?/, "").replace(/<\/synced_block_reference>/, "").split("\n").map((l) => l.replace(/^\t/, "")).join("\n").trim();
3789
+ return inner;
3790
+ }
3791
+ );
3792
+ result = result.replace(
3793
+ /<synced_block[^>]*>\n?([\s\S]*?)<\/synced_block>/g,
3794
+ (_match, inner) => inner.trim()
3795
+ );
3796
+ return result;
3797
+ }
2992
3798
  function convertMediaTags(content) {
2993
3799
  let result = content;
2994
3800
  result = result.replace(NOTION_AUDIO_RE, (_match, src, caption) => {
@@ -3023,10 +3829,25 @@ function convertUnderlineSpans(content) {
3023
3829
  });
3024
3830
  }
3025
3831
  function convertTogglesToHtml(content) {
3026
- const startRe = new RegExp(escapeRegex3(TOGGLE_START2), "g");
3027
- const endRe = new RegExp(escapeRegex3(TOGGLE_END2), "g");
3832
+ const calloutToggleRe = /^> \[!toggle\]-\s*(.+)\n((?:>.*\n?)*)/gm;
3028
3833
  let result = content;
3834
+ let prev = "";
3029
3835
  let safety = 0;
3836
+ while (result !== prev && safety++ < 100) {
3837
+ prev = result;
3838
+ result = result.replace(calloutToggleRe, (_match, title, body) => {
3839
+ const bodyText = body.split("\n").map((line) => line.replace(/^>\s?/, "")).join("\n").trim();
3840
+ return `<details>
3841
+ <summary>${title.trim()}</summary>
3842
+
3843
+ ${bodyText}
3844
+
3845
+ </details>`;
3846
+ });
3847
+ }
3848
+ const startRe = new RegExp(escapeRegex3(TOGGLE_START2), "g");
3849
+ const endRe = new RegExp(escapeRegex3(TOGGLE_END2), "g");
3850
+ safety = 0;
3030
3851
  while (result.includes(TOGGLE_START2) && safety++ < 100) {
3031
3852
  const startIdx = result.indexOf(TOGGLE_START2);
3032
3853
  const endIdx = findMatchingEnd(result, startIdx + TOGGLE_START2.length);
@@ -3122,7 +3943,7 @@ function escapeRegex3(str) {
3122
3943
  }
3123
3944
  var NOTION_PAGE_LINK_RE = /<page url="[^"]*">([\s\S]*?)<\/page>/g;
3124
3945
  var NOTION_COLOR_SPAN_RE = /<span color="([^"]+)">([\s\S]*?)<\/span>/g;
3125
- var NOTION_EMPTY_BLOCK_RE = /^<empty-block\/>\n?/gm;
3946
+ var NOTION_EMPTY_BLOCK_RE = /^(?:>[\t ]*)*[\t ]*<empty-block\/>\n?/gm;
3126
3947
  function convertPageLinks(content) {
3127
3948
  return content.replace(NOTION_PAGE_LINK_RE, (_match, text) => {
3128
3949
  const cleaned = text.replace(/\*\*/g, "").trim();
@@ -3135,6 +3956,20 @@ function convertColorSpans(content) {
3135
3956
  (_match, color, text) => `%%im-nobsidian:color:${color}%%${text}%%/color%%`
3136
3957
  );
3137
3958
  }
3959
+ function convertDatabaseBlocks(content) {
3960
+ return content.replace(
3961
+ /<database[^>]*>([\s\S]*?)<\/database>/g,
3962
+ (_match, title) => `**${title.trim()}** *(Notion DB)*`
3963
+ );
3964
+ }
3965
+ function cleanInlineColorAttrs(content) {
3966
+ return content.replace(/\s*\{color="[^"]*"\}/g, "");
3967
+ }
3968
+ function convertColumnBlocks(content) {
3969
+ return content.replace(/<columns>\n?([\s\S]*?)<\/columns>/g, (_match, inner) => {
3970
+ return inner.replace(/\t?<column>\n?/g, "").replace(/\t?<\/column>\n?/g, "").trim();
3971
+ });
3972
+ }
3138
3973
  function removeEmptyBlocks(content) {
3139
3974
  return content.replace(NOTION_EMPTY_BLOCK_RE, "\n");
3140
3975
  }
@@ -3147,11 +3982,21 @@ ${eq.trim()}
3147
3982
  $$`;
3148
3983
  });
3149
3984
  result = result.replace(NOTION_INLINE_MATH_RE, (_match, eq) => `$${eq}$`);
3985
+ result = result.replace(/\\\$([^$]+?)\\\$/g, (_match, inner) => {
3986
+ const unescaped = inner.replace(/\\\^/g, "^").replace(/\\~/g, "~");
3987
+ return `$${unescaped}$`;
3988
+ });
3150
3989
  return result;
3151
3990
  }
3991
+ function unescapeNotionChars(content) {
3992
+ return content.replace(/\\~/g, "~").replace(/\\\^/g, "^");
3993
+ }
3152
3994
  var NOTION_TABLE_RE = /<table[^>]*>([\s\S]*?)<\/table>/g;
3153
3995
  var TABLE_ROW_RE = /<tr>([\s\S]*?)<\/tr>/g;
3154
3996
  var TABLE_CELL_RE = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/g;
3997
+ function isAlignmentRow(cells) {
3998
+ return cells.every((c) => /^:?-{2,}:?$/.test(c.trim()));
3999
+ }
3155
4000
  function convertNotionTables(content) {
3156
4001
  return content.replace(NOTION_TABLE_RE, (_match, tableBody) => {
3157
4002
  const rows = [];
@@ -3164,7 +4009,9 @@ function convertNotionTables(content) {
3164
4009
  while ((cellMatch = cellRe.exec(rowMatch[1])) !== null) {
3165
4010
  cells.push(cellMatch[1].trim());
3166
4011
  }
3167
- rows.push(cells);
4012
+ if (!isAlignmentRow(cells)) {
4013
+ rows.push(cells);
4014
+ }
3168
4015
  }
3169
4016
  if (rows.length === 0) return _match;
3170
4017
  const colCount = Math.max(...rows.map((r) => r.length));
@@ -3244,6 +4091,316 @@ function unescapePipes(content) {
3244
4091
  }
3245
4092
  return result.join("\n");
3246
4093
  }
4094
+ function ensureCalloutContinuity(content) {
4095
+ const lines = content.split("\n");
4096
+ const result = [];
4097
+ for (let i = 0; i < lines.length; i++) {
4098
+ const line = lines[i];
4099
+ if (line.trim() === "" && i > 0 && i < lines.length - 1) {
4100
+ const prev = result[result.length - 1] ?? "";
4101
+ const next = lines[i + 1] ?? "";
4102
+ if (/^>/.test(prev) && /^>/.test(next) && !/^> \[!/.test(next)) {
4103
+ result.push(">");
4104
+ continue;
4105
+ }
4106
+ }
4107
+ result.push(line);
4108
+ }
4109
+ return result.join("\n");
4110
+ }
4111
+ var DatabaseSyncer = class {
4112
+ constructor(config, stateDb, notionClient, vaultFs, pipeline, imageHandler) {
4113
+ this.config = config;
4114
+ this.stateDb = stateDb;
4115
+ this.notionClient = notionClient;
4116
+ this.vaultFs = vaultFs;
4117
+ this.pipeline = pipeline;
4118
+ this.imageHandler = imageHandler;
4119
+ this.propertyMapper.setWikilinkResolver({
4120
+ resolve: (title) => stateDb.resolveWikilink(title)?.notionPageId ?? null,
4121
+ resolvePageId: (pageId) => stateDb.resolvePageId(pageId)?.title ?? null
4122
+ });
4123
+ }
4124
+ config;
4125
+ stateDb;
4126
+ notionClient;
4127
+ vaultFs;
4128
+ pipeline;
4129
+ imageHandler;
4130
+ propertyMapper = new PropertyMapper();
4131
+ async pullAll() {
4132
+ const databases = this.config.notion.databases;
4133
+ if (!databases || databases.length === 0) {
4134
+ return { created: 0, updated: 0, failed: [] };
4135
+ }
4136
+ let created = 0;
4137
+ let updated = 0;
4138
+ const failed = [];
4139
+ for (const dbConfig of databases) {
4140
+ try {
4141
+ const result = await this.pullDatabase(dbConfig);
4142
+ created += result.created;
4143
+ updated += result.updated;
4144
+ failed.push(...result.failed);
4145
+ } catch (error) {
4146
+ getLogger().warn(`[DB Sync] DB ${dbConfig.databaseId} pull \uC2E4\uD328:`, error);
4147
+ failed.push({
4148
+ path: dbConfig.localFolder,
4149
+ operation: "create",
4150
+ error: error instanceof Error ? error.message : String(error)
4151
+ });
4152
+ }
4153
+ }
4154
+ return { created, updated, failed };
4155
+ }
4156
+ async pushAll() {
4157
+ const databases = this.config.notion.databases;
4158
+ if (!databases || databases.length === 0) {
4159
+ return { created: 0, updated: 0, failed: [] };
4160
+ }
4161
+ let created = 0;
4162
+ let updated = 0;
4163
+ const failed = [];
4164
+ for (const dbConfig of databases) {
4165
+ try {
4166
+ const result = await this.pushDatabase(dbConfig);
4167
+ created += result.created;
4168
+ updated += result.updated;
4169
+ failed.push(...result.failed);
4170
+ } catch (error) {
4171
+ getLogger().warn(`[DB Sync] DB ${dbConfig.databaseId} push \uC2E4\uD328:`, error);
4172
+ failed.push({
4173
+ path: dbConfig.localFolder,
4174
+ operation: "create",
4175
+ error: error instanceof Error ? error.message : String(error)
4176
+ });
4177
+ }
4178
+ }
4179
+ return { created, updated, failed };
4180
+ }
4181
+ async pullDatabase(dbConfig) {
4182
+ const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId);
4183
+ this.propertyMapper.loadSchema(schema);
4184
+ await this.vaultFs.ensureFolder(dbConfig.localFolder);
4185
+ await this.pullDatabaseViews(dbConfig);
4186
+ const pages = await this.notionClient.queryAllDatabasePages(
4187
+ dbConfig.databaseId,
4188
+ dbConfig.pullFilter
4189
+ );
4190
+ let created = 0;
4191
+ let updated = 0;
4192
+ const failed = [];
4193
+ for (const page of pages) {
4194
+ try {
4195
+ const record = this.stateDb.getByNotionId(page.id);
4196
+ if (record) {
4197
+ if (page.last_edited_time === record.notionLastEdited) continue;
4198
+ await this.pullDatabasePage(page, dbConfig);
4199
+ updated++;
4200
+ } else {
4201
+ await this.pullDatabasePage(page, dbConfig);
4202
+ created++;
4203
+ }
4204
+ } catch (error) {
4205
+ failed.push({
4206
+ path: `${dbConfig.localFolder}/${page.id}`,
4207
+ operation: "create",
4208
+ error: error instanceof Error ? error.message : String(error)
4209
+ });
4210
+ }
4211
+ }
4212
+ getLogger().info(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
4213
+ return { created, updated, failed };
4214
+ }
4215
+ async pullDatabaseViews(dbConfig) {
4216
+ try {
4217
+ const viewsConfig = await this.notionClient.getDatabaseViewsConfig(dbConfig.databaseId);
4218
+ const configPath = ".im-nobsidian/db-views.json";
4219
+ await this.vaultFs.ensureFolder(".im-nobsidian");
4220
+ let allViewsConfigs = {};
4221
+ try {
4222
+ const existing = await this.vaultFs.readFile(configPath);
4223
+ allViewsConfigs = JSON.parse(existing);
4224
+ } catch {
4225
+ }
4226
+ allViewsConfigs[dbConfig.databaseId] = viewsConfig;
4227
+ await this.vaultFs.writeFile(configPath, JSON.stringify(allViewsConfigs, null, 2));
4228
+ getLogger().info(
4229
+ `[DB Sync] \uBDF0 \uC124\uC815 ${viewsConfig.views.length}\uAC1C \uC800\uC7A5: ${dbConfig.databaseId}`
4230
+ );
4231
+ } catch (error) {
4232
+ getLogger().warn(`[DB Sync] \uBDF0 \uC124\uC815 Pull \uC2E4\uD328 (\uACC4\uC18D \uC9C4\uD589):`, error);
4233
+ }
4234
+ }
4235
+ async pullDatabasePage(page, dbConfig) {
4236
+ const title = this.notionClient.extractTitle(page);
4237
+ const safeName = sanitizeFileName(title);
4238
+ const properties = this.propertyMapper.fromNotionProperties(
4239
+ page.properties
4240
+ );
4241
+ properties.title = title;
4242
+ const cover = this.notionClient.extractCover(page);
4243
+ const icon = this.notionClient.extractIcon(page);
4244
+ if (cover) {
4245
+ try {
4246
+ const coverResult = await this.imageHandler.downloadAllImages(
4247
+ `![cover](${cover.url})`,
4248
+ `${safeName}-cover`
4249
+ );
4250
+ const localUrlMatch = coverResult.content.match(/!\[cover\]\((.+?)\)/);
4251
+ if (localUrlMatch?.[1]) {
4252
+ properties.cover = localUrlMatch[1];
4253
+ } else {
4254
+ properties.cover = cover.url;
4255
+ }
4256
+ } catch {
4257
+ properties.cover = cover.url;
4258
+ }
4259
+ }
4260
+ if (icon) {
4261
+ properties.icon = icon.value;
4262
+ }
4263
+ let markdown = "";
4264
+ try {
4265
+ const mdResult = await this.notionClient.getPageMarkdown(page.id);
4266
+ markdown = notionEnhancedToObsidian(mdResult.markdown);
4267
+ } catch {
4268
+ }
4269
+ if (this.config.conversion.imageDownload === "immediate" && markdown) {
4270
+ const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
4271
+ markdown = imageResult.content;
4272
+ }
4273
+ if (markdown) {
4274
+ const fileResult = await this.imageHandler.downloadAllFiles(markdown, title);
4275
+ markdown = fileResult.content;
4276
+ }
4277
+ const existingRecord = this.stateDb.getByNotionId(page.id);
4278
+ const filePath = existingRecord?.obsidianPath ?? `${dbConfig.localFolder}/${safeName}.md`;
4279
+ const finalContent = this.pipeline.convertToMarkdown(
4280
+ markdown,
4281
+ {
4282
+ direction: "pull",
4283
+ path: "markdown-api",
4284
+ filePath,
4285
+ parentMode: "database"
4286
+ },
4287
+ { properties }
4288
+ );
4289
+ await this.vaultFs.ensureFolder(dbConfig.localFolder);
4290
+ await this.vaultFs.writeFile(filePath, finalContent);
4291
+ const hash = computeHash(finalContent);
4292
+ this.stateDb.transaction(() => {
4293
+ this.stateDb.upsert({
4294
+ obsidianPath: filePath,
4295
+ notionPageId: page.id,
4296
+ notionParentId: dbConfig.databaseId,
4297
+ contentHash: hash,
4298
+ notionLastEdited: page.last_edited_time,
4299
+ localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
4300
+ syncDirection: "both",
4301
+ fileType: "db-row",
4302
+ status: "synced",
4303
+ baseSnapshot: Buffer.from(finalContent, "utf-8")
4304
+ });
4305
+ this.stateDb.upsertWikilink({
4306
+ obsidianPath: filePath,
4307
+ notionPageId: page.id,
4308
+ title,
4309
+ aliases: extractAliases(properties)
4310
+ });
4311
+ });
4312
+ return filePath;
4313
+ }
4314
+ async pushDatabase(dbConfig) {
4315
+ const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId);
4316
+ this.propertyMapper.loadSchema(schema);
4317
+ const allFiles = await this.vaultFs.listMarkdownFiles();
4318
+ const prefix = dbConfig.localFolder.endsWith("/") ? dbConfig.localFolder : dbConfig.localFolder + "/";
4319
+ const dbFiles = allFiles.filter((f) => f.path.startsWith(prefix));
4320
+ let created = 0;
4321
+ let updated = 0;
4322
+ const failed = [];
4323
+ for (const file of dbFiles) {
4324
+ try {
4325
+ const content = await this.vaultFs.readFile(file.path);
4326
+ const hash = computeHash(content);
4327
+ const record = this.stateDb.getByPath(file.path);
4328
+ if (record?.contentHash === hash) continue;
4329
+ const parsed = matter7(content);
4330
+ const frontmatter = parsed.data;
4331
+ const body = parsed.content;
4332
+ const title = frontmatter.title ?? extractTitleFromPath(file.path);
4333
+ delete frontmatter.title;
4334
+ const notionProps = this.propertyMapper.toNotionProperties(frontmatter, title);
4335
+ const enhanced = obsidianToNotionEnhanced(body.trim());
4336
+ if (record?.notionPageId) {
4337
+ await this.notionClient.updatePageProperties(record.notionPageId, notionProps);
4338
+ try {
4339
+ await this.notionClient.replacePageMarkdown(record.notionPageId, enhanced);
4340
+ } catch {
4341
+ }
4342
+ const updatedPage = await this.notionClient.getPage(record.notionPageId);
4343
+ this.stateDb.transaction(() => {
4344
+ this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
4345
+ this.stateDb.updateStatus(record.id, "synced");
4346
+ this.stateDb.setNotionLastEdited(record.id, updatedPage.last_edited_time);
4347
+ });
4348
+ updated++;
4349
+ } else {
4350
+ const page = await this.notionClient.createPageWithMarkdown({
4351
+ parentId: dbConfig.databaseId,
4352
+ parentType: "database",
4353
+ title,
4354
+ markdown: enhanced,
4355
+ properties: notionProps
4356
+ });
4357
+ this.stateDb.transaction(() => {
4358
+ this.stateDb.upsert({
4359
+ obsidianPath: file.path,
4360
+ notionPageId: page.id,
4361
+ notionParentId: dbConfig.databaseId,
4362
+ contentHash: hash,
4363
+ notionLastEdited: page.last_edited_time,
4364
+ localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
4365
+ syncDirection: "both",
4366
+ fileType: "db-row",
4367
+ status: "synced",
4368
+ baseSnapshot: Buffer.from(content, "utf-8")
4369
+ });
4370
+ this.stateDb.upsertWikilink({
4371
+ obsidianPath: file.path,
4372
+ notionPageId: page.id,
4373
+ title,
4374
+ aliases: extractAliases(frontmatter)
4375
+ });
4376
+ });
4377
+ created++;
4378
+ }
4379
+ } catch (error) {
4380
+ failed.push({
4381
+ path: file.path,
4382
+ operation: "create",
4383
+ error: error instanceof Error ? error.message : String(error)
4384
+ });
4385
+ }
4386
+ }
4387
+ getLogger().info(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
4388
+ return { created, updated, failed };
4389
+ }
4390
+ };
4391
+ function extractAliases(properties) {
4392
+ const raw = properties.aliases ?? properties.alias;
4393
+ if (!raw) return [];
4394
+ if (Array.isArray(raw)) return raw.filter((a) => typeof a === "string");
4395
+ if (typeof raw === "string")
4396
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
4397
+ return [];
4398
+ }
4399
+ function extractTitleFromPath(filePath) {
4400
+ const parts = filePath.split("/");
4401
+ const filename = parts[parts.length - 1] ?? "";
4402
+ return filename.replace(/\.md$/, "");
4403
+ }
3247
4404
 
3248
4405
  // src/sync/orchestrator.ts
3249
4406
  var SyncOrchestrator = class {
@@ -3258,7 +4415,16 @@ var SyncOrchestrator = class {
3258
4415
  });
3259
4416
  this.blockConverter = new BlockConverter();
3260
4417
  this.imageHandler = new ImageHandler(vaultFs, config.paths.attachments, notionClient);
4418
+ this.fileHandler = new FileHandler(vaultFs, notionClient, stateDb);
3261
4419
  this.propertyMapper = new PropertyMapper();
4420
+ this.databaseSyncer = new DatabaseSyncer(
4421
+ config,
4422
+ stateDb,
4423
+ notionClient,
4424
+ vaultFs,
4425
+ this.pipeline,
4426
+ this.imageHandler
4427
+ );
3262
4428
  this.propertyMapper.setWikilinkResolver({
3263
4429
  resolve: (title) => stateDb.resolveWikilink(title)?.notionPageId ?? null,
3264
4430
  resolvePageId: (pageId) => stateDb.resolvePageId(pageId)?.title ?? null
@@ -3273,6 +4439,8 @@ var SyncOrchestrator = class {
3273
4439
  pipeline;
3274
4440
  blockConverter;
3275
4441
  imageHandler;
4442
+ fileHandler;
4443
+ databaseSyncer;
3276
4444
  propertyMapper;
3277
4445
  dbSchemaLoaded = false;
3278
4446
  async push(options) {
@@ -3285,8 +4453,13 @@ var SyncOrchestrator = class {
3285
4453
  const changes = this.changeDetector.detectLocalChanges(files);
3286
4454
  const conflictPaths = new Set(this.stateDb.getByStatus("conflict").map((r) => r.obsidianPath));
3287
4455
  const eligible = options?.force ? changes : changes.filter((c) => !conflictPaths.has(c.path));
3288
- const filtered = options?.paths ? eligible.filter((c) => options.paths.some((p) => c.path.startsWith(p))) : eligible;
3289
- if (filtered.length === 0) {
4456
+ const dbFolders = (this.config.notion.databases ?? []).map(
4457
+ (d) => d.localFolder.endsWith("/") ? d.localFolder : d.localFolder + "/"
4458
+ );
4459
+ const isDbPath = (path) => dbFolders.some((prefix) => path.startsWith(prefix));
4460
+ const filtered = (options?.paths ? eligible.filter((c) => options.paths.some((p) => c.path.startsWith(p))) : eligible).filter((c) => !isDbPath(c.path));
4461
+ const hasDbConfigs = (this.config.notion.databases?.length ?? 0) > 0;
4462
+ if (filtered.length === 0 && !hasDbConfigs) {
3290
4463
  return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
3291
4464
  }
3292
4465
  if (options?.dryRun) {
@@ -3357,6 +4530,66 @@ var SyncOrchestrator = class {
3357
4530
  }
3358
4531
  });
3359
4532
  await Promise.all(tasks.map((t) => t()));
4533
+ if (failed.length > 0) {
4534
+ const retryTargets = failed.splice(0, failed.length);
4535
+ getLogger().info(`[Im-Nobsidian] Push ${retryTargets.length}\uAC74 \uC7AC\uC2DC\uB3C4 (2\uCD08 \uD6C4)`);
4536
+ await new Promise((r) => setTimeout(r, 2e3));
4537
+ for (const target of retryTargets) {
4538
+ const change = filtered.find((c) => c.path === target.path);
4539
+ if (!change) {
4540
+ failed.push(target);
4541
+ continue;
4542
+ }
4543
+ try {
4544
+ switch (change.type) {
4545
+ case "created":
4546
+ await this.pushCreate(change.path);
4547
+ created++;
4548
+ break;
4549
+ case "modified":
4550
+ await this.pushUpdate(change.path);
4551
+ updated++;
4552
+ break;
4553
+ case "moved":
4554
+ await this.pushMove(change.path);
4555
+ updated++;
4556
+ break;
4557
+ case "deleted":
4558
+ await this.pushDelete(change.path);
4559
+ deleted++;
4560
+ break;
4561
+ }
4562
+ getLogger().info(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC131\uACF5: ${change.path}`);
4563
+ } catch (error) {
4564
+ failed.push({
4565
+ path: change.path,
4566
+ operation: target.operation,
4567
+ error: error instanceof Error ? error.message : String(error)
4568
+ });
4569
+ getLogger().warn(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC2E4\uD328: ${change.path}`);
4570
+ }
4571
+ }
4572
+ }
4573
+ if (this.config.sync.syncFiles !== false) {
4574
+ try {
4575
+ const fileResults = await this.fileHandler.pushAllFiles();
4576
+ if (fileResults.length > 0) {
4577
+ getLogger().info(`[Im-Nobsidian] ${fileResults.length}\uAC1C \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC644\uB8CC`);
4578
+ }
4579
+ } catch (error) {
4580
+ getLogger().warn("[Im-Nobsidian] \uD30C\uC77C \uC5C5\uB85C\uB4DC \uC911 \uC624\uB958:", error);
4581
+ }
4582
+ }
4583
+ if ((this.config.notion.databases?.length ?? 0) > 0) {
4584
+ try {
4585
+ const dbResult = await this.databaseSyncer.pushAll();
4586
+ created += dbResult.created;
4587
+ updated += dbResult.updated;
4588
+ failed.push(...dbResult.failed);
4589
+ } catch (error) {
4590
+ getLogger().warn("[Im-Nobsidian] DB Push \uC911 \uC624\uB958:", error);
4591
+ }
4592
+ }
3360
4593
  this.stateDb.setMeta("last_push_at", (/* @__PURE__ */ new Date()).toISOString());
3361
4594
  this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
3362
4595
  this.stateDb.setMeta("push_in_progress", "");
@@ -3450,6 +4683,106 @@ var SyncOrchestrator = class {
3450
4683
  }
3451
4684
  });
3452
4685
  await Promise.all(tasks.map((t) => t()));
4686
+ if (failed.length > 0) {
4687
+ const retryTargets = failed.splice(0, failed.length);
4688
+ getLogger().info(`[Im-Nobsidian] Pull ${retryTargets.length}\uAC74 \uC7AC\uC2DC\uB3C4 (2\uCD08 \uD6C4)`);
4689
+ await new Promise((r) => setTimeout(r, 2e3));
4690
+ for (const target of retryTargets) {
4691
+ const change = filtered.find((c) => {
4692
+ const record = this.stateDb.getByNotionId(c.pageId);
4693
+ return (record?.obsidianPath ?? c.pageId) === target.path;
4694
+ });
4695
+ if (!change) {
4696
+ failed.push(target);
4697
+ continue;
4698
+ }
4699
+ try {
4700
+ switch (change.type) {
4701
+ case "created": {
4702
+ const path = await this.pullCreate(change.pageId);
4703
+ writtenPaths.push(path);
4704
+ created++;
4705
+ break;
4706
+ }
4707
+ case "modified": {
4708
+ const result = await this.pullUpdate(change);
4709
+ if (result.conflict) {
4710
+ conflicts.push(result.conflict);
4711
+ } else if (result.path) {
4712
+ writtenPaths.push(result.path);
4713
+ updated++;
4714
+ }
4715
+ break;
4716
+ }
4717
+ case "deleted": {
4718
+ const path = await this.pullDelete(change.pageId);
4719
+ if (path) deleted++;
4720
+ break;
4721
+ }
4722
+ }
4723
+ getLogger().info(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC131\uACF5: ${target.path}`);
4724
+ } catch (error) {
4725
+ failed.push({
4726
+ path: target.path,
4727
+ operation: target.operation,
4728
+ error: error instanceof Error ? error.message : String(error)
4729
+ });
4730
+ getLogger().warn(`[Im-Nobsidian] \uC7AC\uC2DC\uB3C4 \uC2E4\uD328: ${target.path}`);
4731
+ }
4732
+ }
4733
+ }
4734
+ if ((this.config.notion.databases?.length ?? 0) > 0) {
4735
+ try {
4736
+ const dbResult = await this.databaseSyncer.pullAll();
4737
+ created += dbResult.created;
4738
+ updated += dbResult.updated;
4739
+ failed.push(...dbResult.failed);
4740
+ if (dbResult.created + dbResult.updated > 0) {
4741
+ const dbPaths = this.stateDb.getByStatus("synced").filter((r) => r.fileType === "db-row").map((r) => r.obsidianPath);
4742
+ writtenPaths.push(...dbPaths.slice(-dbResult.created - dbResult.updated));
4743
+ }
4744
+ } catch (error) {
4745
+ getLogger().warn("[Im-Nobsidian] DB Pull \uC911 \uC624\uB958:", error);
4746
+ }
4747
+ }
4748
+ if (!this.isDatabaseMode) {
4749
+ try {
4750
+ const discoveredDbs = await this.discoverChildDatabases();
4751
+ const configuredIds = new Set(
4752
+ (this.config.notion.databases ?? []).map((d) => d.databaseId.replace(/-/g, ""))
4753
+ );
4754
+ const newDbs = discoveredDbs.filter((db) => !configuredIds.has(db.dbId.replace(/-/g, "")));
4755
+ for (const { dbId, parentPageId } of newDbs) {
4756
+ try {
4757
+ const dbTitle = await this.notionClient.getDatabaseTitle(dbId);
4758
+ const safeName = (dbTitle || dbId.slice(0, 8)).replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-");
4759
+ const parentEntry = parentPageId ? this.stateDb.getByNotionId(parentPageId) : null;
4760
+ let parentFolder = "databases";
4761
+ if (parentEntry?.obsidianPath) {
4762
+ const parts = parentEntry.obsidianPath.split("/");
4763
+ parts.pop();
4764
+ parentFolder = parts.length > 0 ? parts.join("/") : "databases";
4765
+ }
4766
+ const folderName = `${parentFolder}/${safeName}`;
4767
+ const dbConfig = {
4768
+ databaseId: dbId,
4769
+ localFolder: folderName,
4770
+ titleProperty: "Name"
4771
+ };
4772
+ const dbResult = await this.databaseSyncer.pullDatabase(dbConfig);
4773
+ created += dbResult.created;
4774
+ updated += dbResult.updated;
4775
+ failed.push(...dbResult.failed);
4776
+ } catch (error) {
4777
+ getLogger().warn(`[Im-Nobsidian] \uC790\uB3D9 \uBC1C\uACAC DB ${dbId} \uB3D9\uAE30\uD654 \uC2E4\uD328:`, error);
4778
+ }
4779
+ }
4780
+ } catch (error) {
4781
+ getLogger().warn("[Im-Nobsidian] child_database \uC790\uB3D9 \uBC1C\uACAC \uC2E4\uD328:", error);
4782
+ }
4783
+ }
4784
+ const allSyncedMdPaths = this.stateDb.getAll().filter((r) => r.obsidianPath?.endsWith(".md")).map((r) => r.obsidianPath);
4785
+ await this.resolveNotionLinks(allSyncedMdPaths);
3453
4786
  this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
3454
4787
  this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
3455
4788
  this.stateDb.setMeta("pull_in_progress", "");
@@ -3538,6 +4871,72 @@ var SyncOrchestrator = class {
3538
4871
  get isDatabaseMode() {
3539
4872
  return this.config.notion.parentMode === "database" && !!this.config.notion.databaseId;
3540
4873
  }
4874
+ async discoverChildDatabases() {
4875
+ const rootId = this.config.notion.rootPageId;
4876
+ const allDbs = [];
4877
+ let currentLevel = [rootId];
4878
+ while (currentLevel.length > 0) {
4879
+ const nextLevel = [];
4880
+ for (const parentId of currentLevel) {
4881
+ try {
4882
+ const dbIds = await this.notionClient.getChildDatabaseIds(parentId);
4883
+ for (const dbId of dbIds) {
4884
+ allDbs.push({ dbId, parentPageId: parentId });
4885
+ }
4886
+ const childPages = await this.notionClient.getChildPages(parentId);
4887
+ nextLevel.push(...childPages.map((p) => p.id));
4888
+ } catch {
4889
+ }
4890
+ }
4891
+ currentLevel = nextLevel;
4892
+ }
4893
+ return allDbs;
4894
+ }
4895
+ async resolveNotionLinks(paths) {
4896
+ const allRecords = this.stateDb.getAll();
4897
+ const idToTitle = /* @__PURE__ */ new Map();
4898
+ for (const r of allRecords) {
4899
+ if (r.notionPageId && r.obsidianPath) {
4900
+ const title = r.obsidianPath.replace(/\.md$/, "").split("/").pop() ?? "";
4901
+ const cleanId = r.notionPageId.replace(/-/g, "");
4902
+ idToTitle.set(cleanId, title);
4903
+ idToTitle.set(r.notionPageId, title);
4904
+ }
4905
+ }
4906
+ if (idToTitle.size === 0) return;
4907
+ for (const filePath of paths) {
4908
+ if (!filePath.endsWith(".md")) continue;
4909
+ try {
4910
+ let content = await this.vaultFs.readFile(filePath);
4911
+ let changed = false;
4912
+ const resolved = content.replace(/\[\[notion:([a-f0-9-]+)\]\]/g, (_match, id) => {
4913
+ const title = idToTitle.get(id.replace(/-/g, ""));
4914
+ if (title) {
4915
+ changed = true;
4916
+ return `[[${title}]]`;
4917
+ }
4918
+ return _match;
4919
+ });
4920
+ content = resolved;
4921
+ const resolved2 = content.replace(
4922
+ /\[([^\]]+)\]\(\/([a-f0-9]{32})\?pvs=\d+\)/g,
4923
+ (_match, text, id) => {
4924
+ const title = idToTitle.get(id);
4925
+ if (title) {
4926
+ changed = true;
4927
+ return `[[${title}|${text}]]`;
4928
+ }
4929
+ return _match;
4930
+ }
4931
+ );
4932
+ content = resolved2;
4933
+ if (changed) {
4934
+ await this.vaultFs.writeFile(filePath, content);
4935
+ }
4936
+ } catch {
4937
+ }
4938
+ }
4939
+ }
3541
4940
  async ensureDbSchema() {
3542
4941
  if (this.dbSchemaLoaded || !this.isDatabaseMode) return;
3543
4942
  const schema = await this.notionClient.getDatabaseSchema(this.config.notion.databaseId);
@@ -3594,7 +4993,7 @@ var SyncOrchestrator = class {
3594
4993
  status: "synced",
3595
4994
  baseSnapshot: Buffer.from(content, "utf-8")
3596
4995
  });
3597
- const aliases = extractAliases(conversionResult.properties);
4996
+ const aliases = extractAliases2(conversionResult.properties);
3598
4997
  this.stateDb.upsertWikilink({
3599
4998
  obsidianPath: path,
3600
4999
  notionPageId: page.id,
@@ -3634,7 +5033,7 @@ var SyncOrchestrator = class {
3634
5033
  const updatedPage = await this.notionClient.getPage(record.notionPageId);
3635
5034
  const hash = computeHash(content);
3636
5035
  const title = extractTitle(path);
3637
- const aliases = extractAliases(conversionResult.properties);
5036
+ const aliases = extractAliases2(conversionResult.properties);
3638
5037
  this.stateDb.transaction(() => {
3639
5038
  this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
3640
5039
  this.stateDb.updateStatus(record.id, "synced");
@@ -3737,7 +5136,7 @@ var SyncOrchestrator = class {
3737
5136
  const title = this.notionClient.extractTitle(page);
3738
5137
  const safeName = sanitizeFileName(title);
3739
5138
  const parentPath = await this.resolveParentPath(page);
3740
- const allChildren = await this.notionClient.fetchAllChildren(pageId);
5139
+ const allChildren = await this.notionClient.fetchAllChildrenDeep(pageId);
3741
5140
  const hasChildPages = allChildren.some((b) => "type" in b && b.type === "child_page");
3742
5141
  const markdown = await this.fetchPageMarkdown(pageId);
3743
5142
  const hasContent = markdown.trim().length > 0;
@@ -3773,6 +5172,8 @@ var SyncOrchestrator = class {
3773
5172
  const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
3774
5173
  processedMarkdown = imageResult.content;
3775
5174
  }
5175
+ const fileResult = await this.imageHandler.downloadAllFiles(processedMarkdown, title);
5176
+ processedMarkdown = fileResult.content;
3776
5177
  const finalContent = this.pipeline.convertToMarkdown(
3777
5178
  processedMarkdown,
3778
5179
  {
@@ -3785,11 +5186,12 @@ var SyncOrchestrator = class {
3785
5186
  );
3786
5187
  await this.vaultFs.writeFile(filePath, finalContent);
3787
5188
  const hash = computeHash(finalContent);
5189
+ const resolvedParentId = await this.extractParentId(page);
3788
5190
  this.stateDb.transaction(() => {
3789
5191
  this.stateDb.upsert({
3790
5192
  obsidianPath: filePath,
3791
5193
  notionPageId: pageId,
3792
- notionParentId: this.extractParentId(page),
5194
+ notionParentId: resolvedParentId,
3793
5195
  contentHash: hash,
3794
5196
  notionLastEdited: page.last_edited_time,
3795
5197
  localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3798,7 +5200,7 @@ var SyncOrchestrator = class {
3798
5200
  status: "synced",
3799
5201
  baseSnapshot: Buffer.from(finalContent, "utf-8")
3800
5202
  });
3801
- const aliases = extractAliases(properties);
5203
+ const aliases = extractAliases2(properties);
3802
5204
  this.stateDb.upsertWikilink({
3803
5205
  obsidianPath: filePath,
3804
5206
  notionPageId: pageId,
@@ -3828,6 +5230,8 @@ var SyncOrchestrator = class {
3828
5230
  const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
3829
5231
  markdown = imageResult.content;
3830
5232
  }
5233
+ const fileResult = await this.imageHandler.downloadAllFiles(markdown, title);
5234
+ markdown = fileResult.content;
3831
5235
  const savedMarkers = this.stateDb.getPreserveMarkers(record.obsidianPath);
3832
5236
  const remoteContent = this.pipeline.convertToMarkdown(
3833
5237
  markdown,
@@ -3959,7 +5363,7 @@ var SyncOrchestrator = class {
3959
5363
  return fileName === folderName;
3960
5364
  }
3961
5365
  async resolveParentPath(page) {
3962
- const parentId = this.extractParentId(page);
5366
+ const parentId = await this.extractParentId(page);
3963
5367
  if (!parentId || notionIdsEqual(parentId, this.config.notion.rootPageId)) return "";
3964
5368
  const parentRecord = this.stateDb.getByNotionId(parentId);
3965
5369
  if (parentRecord) {
@@ -4017,8 +5421,13 @@ var SyncOrchestrator = class {
4017
5421
  const baseEnhanced = obsidianToNotionEnhanced(baseSnapshot.toString("utf-8"));
4018
5422
  const patches = this.computePatches(baseEnhanced, enhanced);
4019
5423
  if (patches.length > 0 && patches.length <= 20) {
4020
- await this.notionClient.updatePageMarkdownPartial(pageId, patches);
4021
- return;
5424
+ try {
5425
+ await this.notionClient.updatePageMarkdownPartial(pageId, patches);
5426
+ return;
5427
+ } catch {
5428
+ await this.notionClient.replacePageMarkdown(pageId, enhanced);
5429
+ return;
5430
+ }
4022
5431
  }
4023
5432
  }
4024
5433
  await this.notionClient.replacePageMarkdown(pageId, enhanced);
@@ -4098,10 +5507,30 @@ var SyncOrchestrator = class {
4098
5507
  }
4099
5508
  return this.blockConverter.notionBlocksToMarkdown(pageId);
4100
5509
  }
4101
- extractParentId(page) {
5510
+ async extractParentId(page) {
4102
5511
  const parent = page.parent;
4103
5512
  if (parent.type === "page_id") return parent.page_id ?? null;
4104
5513
  if (parent.type === "database_id") return parent.database_id ?? null;
5514
+ if (parent.type === "block_id" && parent.block_id) {
5515
+ return this.resolveBlockToPageId(parent.block_id);
5516
+ }
5517
+ return null;
5518
+ }
5519
+ async resolveBlockToPageId(blockId) {
5520
+ for (let i = 0; i < 10; i++) {
5521
+ try {
5522
+ const block = await this.notionClient.getBlock(blockId);
5523
+ const bp = block.parent;
5524
+ if (bp.type === "page_id") return bp.page_id ?? null;
5525
+ if (bp.type === "block_id" && bp.block_id) {
5526
+ blockId = bp.block_id;
5527
+ continue;
5528
+ }
5529
+ return null;
5530
+ } catch {
5531
+ return null;
5532
+ }
5533
+ }
4105
5534
  return null;
4106
5535
  }
4107
5536
  cleanupInterruptedSync() {
@@ -4133,7 +5562,7 @@ function extractTitle(filePath) {
4133
5562
  const filename = parts[parts.length - 1] ?? "";
4134
5563
  return filename.replace(/\.md$/, "");
4135
5564
  }
4136
- function extractAliases(properties) {
5565
+ function extractAliases2(properties) {
4137
5566
  const raw = properties.aliases ?? properties.alias;
4138
5567
  if (!raw) return [];
4139
5568
  if (Array.isArray(raw)) return raw.filter((a) => typeof a === "string");
@@ -4205,6 +5634,11 @@ var NodeVaultFS = class {
4205
5634
  await this.walkDir(this.rootPath, files);
4206
5635
  return files;
4207
5636
  }
5637
+ async listNonMarkdownFiles() {
5638
+ const files = [];
5639
+ await this.walkDirNonMd(this.rootPath, files);
5640
+ return files;
5641
+ }
4208
5642
  matchesPattern(relativePath, pattern) {
4209
5643
  if (pattern === "**/*" || pattern === "**") return true;
4210
5644
  if (pattern.endsWith("/**")) {
@@ -4227,6 +5661,25 @@ var NodeVaultFS = class {
4227
5661
  }
4228
5662
  return this.excludePatterns.some((p) => this.matchesPattern(relativePath, p));
4229
5663
  }
5664
+ async walkDirNonMd(dir, result) {
5665
+ const entries = await readdir(dir, { withFileTypes: true });
5666
+ for (const entry of entries) {
5667
+ const fullPath = join(dir, entry.name);
5668
+ if (entry.name.startsWith(".")) continue;
5669
+ const relativePath = relative(this.rootPath, fullPath);
5670
+ if (this.isExcluded(relativePath)) continue;
5671
+ if (entry.isDirectory()) {
5672
+ await this.walkDirNonMd(fullPath, result);
5673
+ } else if (entry.isFile() && !entry.name.endsWith(".md")) {
5674
+ const fileStat = await stat(fullPath);
5675
+ result.push({
5676
+ path: relativePath,
5677
+ size: fileStat.size,
5678
+ mtime: fileStat.mtime.toISOString()
5679
+ });
5680
+ }
5681
+ }
5682
+ }
4230
5683
  async walkDir(dir, result) {
4231
5684
  const entries = await readdir(dir, { withFileTypes: true });
4232
5685
  for (const entry of entries) {
@@ -4707,6 +6160,325 @@ var WatchSyncService = class {
4707
6160
  }
4708
6161
  };
4709
6162
 
4710
- export { BlockConverter, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConversionPipeline, DEFAULT_CONFIG, EmbedResolver, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, HtmlAnnotationStripper, ImageHandler, InlineDBParser, LocalImageRestorer, MathNormalizer, MentionToWikilink, NodeVaultFS, NotionBlockBuilder, NotionClient, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, StateDB, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, WatchSyncService, WikilinkResolver, computeBufferHash, computeHash, createDefaultPipeline, generateId, getLogger, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, threeWayMerge };
6163
+ // src/view/filter-engine.ts
6164
+ function sortEntries(entries, sorts) {
6165
+ if (sorts.length === 0) return entries;
6166
+ return [...entries].sort((a, b) => {
6167
+ for (const sort of sorts) {
6168
+ const key = sort.property ?? sort.timestamp;
6169
+ if (!key) continue;
6170
+ const aVal = a.properties[key] ?? null;
6171
+ const bVal = b.properties[key] ?? null;
6172
+ const cmp = compareValues(aVal, bVal);
6173
+ if (cmp !== 0) return sort.direction === "ascending" ? cmp : -cmp;
6174
+ }
6175
+ return 0;
6176
+ });
6177
+ }
6178
+ function groupEntries(entries, groupByProperty, options) {
6179
+ const groups = /* @__PURE__ */ new Map();
6180
+ const ungrouped = [];
6181
+ for (const entry of entries) {
6182
+ const val = entry.properties[groupByProperty] ?? null;
6183
+ const groupName = extractGroupName(val);
6184
+ if (groupName === null) {
6185
+ ungrouped.push(entry);
6186
+ } else {
6187
+ const existing = groups.get(groupName);
6188
+ if (existing) {
6189
+ existing.push(entry);
6190
+ } else {
6191
+ groups.set(groupName, [entry]);
6192
+ }
6193
+ }
6194
+ }
6195
+ let result = Array.from(groups.entries()).map(([name, items]) => ({
6196
+ groupName: name,
6197
+ entries: items
6198
+ }));
6199
+ if (options?.sort === "ascending") {
6200
+ result.sort((a, b) => a.groupName.localeCompare(b.groupName));
6201
+ } else if (options?.sort === "descending") {
6202
+ result.sort((a, b) => b.groupName.localeCompare(a.groupName));
6203
+ }
6204
+ if (ungrouped.length > 0) {
6205
+ result.push({ groupName: "\uADF8\uB8F9 \uC5C6\uC74C", entries: ungrouped });
6206
+ }
6207
+ if (options?.hideEmptyGroups) {
6208
+ result = result.filter((g) => g.entries.length > 0);
6209
+ }
6210
+ return result;
6211
+ }
6212
+ function extractCalendarEntries(entries, dateProperty) {
6213
+ const result = [];
6214
+ for (const entry of entries) {
6215
+ const val = entry.properties[dateProperty];
6216
+ if (!val) continue;
6217
+ if (typeof val === "string") {
6218
+ const date = parseDate(val);
6219
+ if (date) result.push({ entry, date });
6220
+ } else if (typeof val === "object" && val !== null && !Array.isArray(val) && "start" in val) {
6221
+ const date = parseDate(val.start);
6222
+ if (date) {
6223
+ result.push({
6224
+ entry,
6225
+ date,
6226
+ endDate: val.end ? parseDate(val.end) ?? void 0 : void 0
6227
+ });
6228
+ }
6229
+ }
6230
+ }
6231
+ return result;
6232
+ }
6233
+ function filterByMonth(calendarEntries, year, month) {
6234
+ const monthStr = `${year}-${String(month).padStart(2, "0")}`;
6235
+ return calendarEntries.filter((ce) => ce.date.startsWith(monthStr));
6236
+ }
6237
+ function getVisibleProperties(viewConfig) {
6238
+ if (!viewConfig.properties) return [];
6239
+ return viewConfig.properties.filter((p) => p.visible !== false).map((p) => ({
6240
+ id: p.propertyId,
6241
+ name: p.propertyName ?? p.propertyId,
6242
+ width: p.width
6243
+ }));
6244
+ }
6245
+ function compareValues(a, b) {
6246
+ if (a === null && b === null) return 0;
6247
+ if (a === null) return 1;
6248
+ if (b === null) return -1;
6249
+ if (typeof a === "string" && typeof b === "string") return a.localeCompare(b);
6250
+ if (typeof a === "number" && typeof b === "number") return a - b;
6251
+ if (typeof a === "boolean" && typeof b === "boolean") return (a ? 1 : 0) - (b ? 1 : 0);
6252
+ if (Array.isArray(a) && Array.isArray(b)) {
6253
+ return a.join(",").localeCompare(b.join(","));
6254
+ }
6255
+ if (typeof a === "object" && typeof b === "object" && a !== null && b !== null) {
6256
+ if ("start" in a && "start" in b) {
6257
+ return (a.start ?? "").localeCompare(b.start ?? "");
6258
+ }
6259
+ }
6260
+ return String(a).localeCompare(String(b));
6261
+ }
6262
+ function extractGroupName(val) {
6263
+ if (val === null || val === void 0) return null;
6264
+ if (typeof val === "string") return val || null;
6265
+ if (typeof val === "boolean") return val ? "true" : "false";
6266
+ if (typeof val === "number") return String(val);
6267
+ if (Array.isArray(val)) return val.length > 0 ? val.join(", ") : null;
6268
+ if (typeof val === "object" && "start" in val) return val.start;
6269
+ return null;
6270
+ }
6271
+ function parseDate(value) {
6272
+ const match = /^(\d{4}-\d{2}-\d{2})/.exec(value);
6273
+ return match?.[1] ?? null;
6274
+ }
6275
+ var DB_VIEWS_PATH = ".im-nobsidian/db-views.json";
6276
+ var ViewDataProvider = class {
6277
+ constructor(vaultFs) {
6278
+ this.vaultFs = vaultFs;
6279
+ }
6280
+ vaultFs;
6281
+ async loadAllViewConfigs() {
6282
+ try {
6283
+ const raw = await this.vaultFs.readFile(DB_VIEWS_PATH);
6284
+ return JSON.parse(raw);
6285
+ } catch {
6286
+ return {};
6287
+ }
6288
+ }
6289
+ async getViewConfigs(databaseId) {
6290
+ const all = await this.loadAllViewConfigs();
6291
+ return all[databaseId] ?? null;
6292
+ }
6293
+ async collectEntries(folderPath) {
6294
+ const allFiles = await this.vaultFs.listMarkdownFiles();
6295
+ const prefix = folderPath.endsWith("/") ? folderPath : folderPath + "/";
6296
+ const dbFiles = allFiles.filter((f) => f.path.startsWith(prefix));
6297
+ const entries = [];
6298
+ for (const file of dbFiles) {
6299
+ try {
6300
+ const content = await this.vaultFs.readFile(file.path);
6301
+ const entry = parseEntry(file.path, content);
6302
+ if (entry) entries.push(entry);
6303
+ } catch {
6304
+ }
6305
+ }
6306
+ return entries;
6307
+ }
6308
+ async buildViewData(databaseId, viewId, folderPath, schema) {
6309
+ const dbConfig = await this.getViewConfigs(databaseId);
6310
+ if (!dbConfig) return null;
6311
+ const viewConfig = dbConfig.views.find((v) => v.id === viewId);
6312
+ if (!viewConfig) return null;
6313
+ const entries = await this.collectEntries(folderPath);
6314
+ return this.processViewData(entries, viewConfig, databaseId, dbConfig.databaseName, schema);
6315
+ }
6316
+ async buildDefaultViewData(databaseId, folderPath, schema) {
6317
+ const dbConfig = await this.getViewConfigs(databaseId);
6318
+ if (!dbConfig || dbConfig.views.length === 0) return null;
6319
+ const viewConfig = dbConfig.views[0];
6320
+ const entries = await this.collectEntries(folderPath);
6321
+ return this.processViewData(entries, viewConfig, databaseId, dbConfig.databaseName, schema);
6322
+ }
6323
+ processViewData(entries, viewConfig, databaseId, databaseName, schema) {
6324
+ let processed = entries;
6325
+ if (viewConfig.sorts && viewConfig.sorts.length > 0) {
6326
+ processed = sortEntries(processed, viewConfig.sorts);
6327
+ }
6328
+ const visibleProps = getVisibleProperties(viewConfig);
6329
+ const propertyNames = visibleProps.length > 0 ? visibleProps.map((p) => p.name) : collectPropertyNames(processed);
6330
+ const data = {
6331
+ databaseId,
6332
+ databaseName,
6333
+ viewConfig,
6334
+ entries: processed,
6335
+ propertyNames,
6336
+ schema
6337
+ };
6338
+ if (viewConfig.type === "board" && viewConfig.groupBy) {
6339
+ const grouped = groupEntries(
6340
+ processed,
6341
+ viewConfig.groupBy.propertyName ?? viewConfig.groupBy.propertyId,
6342
+ {
6343
+ sort: viewConfig.groupBy.sort === "manual" ? void 0 : viewConfig.groupBy.sort,
6344
+ hideEmptyGroups: viewConfig.groupBy.hideEmptyGroups
6345
+ }
6346
+ );
6347
+ return { ...data, grouped };
6348
+ }
6349
+ if (viewConfig.type === "calendar" && viewConfig.datePropertyName) {
6350
+ const calendarEntries = extractCalendarEntries(processed, viewConfig.datePropertyName);
6351
+ return { ...data, calendarEntries };
6352
+ }
6353
+ return data;
6354
+ }
6355
+ };
6356
+ function parseEntry(path, content) {
6357
+ try {
6358
+ const parsed = matter7(content);
6359
+ const fm = parsed.data;
6360
+ const title = fm.title ?? extractTitleFromPath2(path);
6361
+ const icon = fm.icon;
6362
+ const cover = fm.cover;
6363
+ const properties = {};
6364
+ for (const [key, value] of Object.entries(fm)) {
6365
+ if (key === "title" || key === "icon" || key === "cover") continue;
6366
+ properties[key] = normalizePropertyValue(value);
6367
+ }
6368
+ return { path, title, icon, cover, properties };
6369
+ } catch {
6370
+ return null;
6371
+ }
6372
+ }
6373
+ function normalizePropertyValue(value) {
6374
+ if (value === null || value === void 0) return null;
6375
+ if (typeof value === "string") return value;
6376
+ if (typeof value === "number") return value;
6377
+ if (typeof value === "boolean") return value;
6378
+ if (value instanceof Date) {
6379
+ return value.toISOString().split("T")[0];
6380
+ }
6381
+ if (Array.isArray(value)) {
6382
+ return value.map((v) => String(v));
6383
+ }
6384
+ if (typeof value === "object" && value !== null) {
6385
+ const obj = value;
6386
+ if ("start" in obj) {
6387
+ return {
6388
+ start: String(obj.start),
6389
+ end: obj.end ? String(obj.end) : void 0
6390
+ };
6391
+ }
6392
+ }
6393
+ return String(value);
6394
+ }
6395
+ function extractTitleFromPath2(filePath) {
6396
+ const parts = filePath.split("/");
6397
+ const filename = parts[parts.length - 1] ?? "";
6398
+ return filename.replace(/\.md$/, "");
6399
+ }
6400
+ function collectPropertyNames(entries) {
6401
+ const names = /* @__PURE__ */ new Set();
6402
+ for (const entry of entries) {
6403
+ for (const key of Object.keys(entry.properties)) {
6404
+ names.add(key);
6405
+ }
6406
+ }
6407
+ return Array.from(names);
6408
+ }
6409
+ var EntryEditor = class {
6410
+ constructor(vaultFs) {
6411
+ this.vaultFs = vaultFs;
6412
+ }
6413
+ vaultFs;
6414
+ async updateProperty(entryPath, propertyName, newValue) {
6415
+ const content = await this.vaultFs.readFile(entryPath);
6416
+ const parsed = matter7(content);
6417
+ parsed.data[propertyName] = newValue;
6418
+ const updated = matter7.stringify(parsed.content, parsed.data);
6419
+ await this.vaultFs.writeFile(entryPath, updated);
6420
+ }
6421
+ async createEntry(folderPath, title, properties) {
6422
+ const safeName = title.replace(/[\\/:*?"<>|]/g, "_").trim() || "Untitled";
6423
+ const filePath = `${folderPath}/${safeName}.md`;
6424
+ const frontmatter = { title, ...properties };
6425
+ const content = matter7.stringify("", frontmatter);
6426
+ await this.vaultFs.ensureFolder(folderPath);
6427
+ await this.vaultFs.writeFile(filePath, content);
6428
+ return filePath;
6429
+ }
6430
+ async moveEntryToGroup(entry, groupProperty, newGroupValue) {
6431
+ await this.updateProperty(entry.path, groupProperty, newGroupValue);
6432
+ }
6433
+ async updateDate(entryPath, dateProperty, newDate) {
6434
+ await this.updateProperty(entryPath, dateProperty, newDate);
6435
+ }
6436
+ };
6437
+
6438
+ // src/view/color-map.ts
6439
+ var NOTION_COLORS = {
6440
+ default: "#37352F",
6441
+ gray: "#787774",
6442
+ brown: "#9F6B53",
6443
+ orange: "#D9730D",
6444
+ yellow: "#CB912F",
6445
+ green: "#448361",
6446
+ blue: "#337EA9",
6447
+ purple: "#9065B0",
6448
+ pink: "#C14C8A",
6449
+ red: "#D44C47"
6450
+ };
6451
+ var NOTION_BG_COLORS = {
6452
+ default: "#F1F1EF",
6453
+ gray: "#F1F1EF",
6454
+ brown: "#F4EEEE",
6455
+ orange: "#FBECDD",
6456
+ yellow: "#FBF3DB",
6457
+ green: "#EDF3EC",
6458
+ blue: "#E7F3F8",
6459
+ purple: "#F4F0F7",
6460
+ pink: "#F9F0F5",
6461
+ red: "#FDEBEC"
6462
+ };
6463
+ function getNotionColor(color) {
6464
+ return NOTION_COLORS[color] ?? NOTION_COLORS.default;
6465
+ }
6466
+ function getNotionBgColor(color) {
6467
+ return NOTION_BG_COLORS[color] ?? NOTION_BG_COLORS.default;
6468
+ }
6469
+ function generateColorCSS() {
6470
+ const lines = [];
6471
+ for (const [name, hex] of Object.entries(NOTION_COLORS)) {
6472
+ lines.push(` --notion-color-${name}: ${hex};`);
6473
+ }
6474
+ for (const [name, hex] of Object.entries(NOTION_BG_COLORS)) {
6475
+ lines.push(` --notion-bg-${name}: ${hex};`);
6476
+ }
6477
+ return `:root {
6478
+ ${lines.join("\n")}
6479
+ }`;
6480
+ }
6481
+
6482
+ export { BlockConverter, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConversionPipeline, DEFAULT_CONFIG, DatabaseSyncer, EmbedResolver, EntryEditor, FileHandler, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, HtmlAnnotationStripper, ImageHandler, InlineDBParser, LocalImageRestorer, MathNormalizer, MentionToWikilink, NodeVaultFS, NotionBlockBuilder, NotionClient, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, StateDB, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, ViewDataProvider, WatchSyncService, WikilinkResolver, computeBufferHash, computeHash, createDefaultPipeline, extractCalendarEntries, filterByMonth, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, sortEntries, threeWayMerge };
4711
6483
  //# sourceMappingURL=index.js.map
4712
6484
  //# sourceMappingURL=index.js.map