@im-nobsidian/core 0.1.4 → 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,7 +5,7 @@ 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';
@@ -783,15 +783,17 @@ var PropertyMapper = class {
783
783
  };
784
784
 
785
785
  // src/notion/client.ts
786
- var NotionClient = class {
786
+ var NotionClient = class _NotionClient {
787
787
  client;
788
788
  sema;
789
+ token;
789
790
  propertyMapper = new PropertyMapper();
790
791
  constructor(options) {
791
792
  this.client = new Client({
792
793
  auth: options.token,
793
794
  timeoutMs: options.timeoutMs ?? 3e4
794
795
  });
796
+ this.token = options.token;
795
797
  this.sema = new Sema(options.concurrency ?? 3);
796
798
  }
797
799
  getInternalClient() {
@@ -880,11 +882,31 @@ var NotionClient = class {
880
882
  );
881
883
  }
882
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
+ }
883
906
  async getDatabaseSchema(databaseId) {
884
- const db = await this.withRateLimit(
885
- () => this.client.databases.retrieve({ database_id: databaseId })
886
- );
907
+ const db = await this.fetchDatabaseLegacy(databaseId);
887
908
  const properties = db.properties;
909
+ if (!properties) return {};
888
910
  const schema = {};
889
911
  for (const [name, prop] of Object.entries(properties)) {
890
912
  schema[name] = { id: prop.id, type: prop.type };
@@ -892,9 +914,10 @@ var NotionClient = class {
892
914
  return schema;
893
915
  }
894
916
  async queryDatabase(databaseId, options) {
917
+ const dsId = await this.getDataSourceId(databaseId);
895
918
  const response = await this.withRateLimit(
896
919
  () => this.client.dataSources.query({
897
- data_source_id: databaseId,
920
+ data_source_id: dsId,
898
921
  start_cursor: options?.startCursor,
899
922
  page_size: options?.pageSize ?? 100,
900
923
  filter: options?.filter
@@ -929,6 +952,23 @@ var NotionClient = class {
929
952
  async archivePage(pageId) {
930
953
  await this.withRateLimit(() => this.client.pages.update({ page_id: pageId, archived: true }));
931
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
+ }
932
972
  // ─── Block CRUD ───
933
973
  async listChildren(blockId, options) {
934
974
  const response = await this.withRateLimit(
@@ -953,6 +993,35 @@ var NotionClient = class {
953
993
  } while (cursor);
954
994
  return blocks;
955
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
+ }
956
1025
  async appendChildren(blockId, children) {
957
1026
  const BATCH_SIZE = 100;
958
1027
  for (let i = 0; i < children.length; i += BATCH_SIZE) {
@@ -1004,19 +1073,23 @@ var NotionClient = class {
1004
1073
  };
1005
1074
  }
1006
1075
  async getChildPages(parentId) {
1007
- const blocks = await this.fetchAllChildren(parentId);
1076
+ const blocks = await this.fetchAllChildrenDeep(parentId);
1008
1077
  const childPageBlocks = blocks.filter((b) => b.type === "child_page");
1009
1078
  if (childPageBlocks.length === 0) return [];
1010
1079
  const pages = await Promise.all(childPageBlocks.map((b) => this.getPage(b.id)));
1011
1080
  return pages;
1012
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
+ }
1013
1086
  async getChildPagesRecursive(parentId) {
1014
1087
  const all = [];
1015
1088
  let currentLevel = [parentId];
1016
1089
  while (currentLevel.length > 0) {
1017
- const childArrays = await Promise.all(currentLevel.map((id) => this.getChildPages(id)));
1018
1090
  const nextLevel = [];
1019
- for (const children of childArrays) {
1091
+ for (const id of currentLevel) {
1092
+ const children = await this.getChildPages(id);
1020
1093
  for (const child of children) {
1021
1094
  all.push(child);
1022
1095
  nextLevel.push(child.id);
@@ -1026,6 +1099,76 @@ var NotionClient = class {
1026
1099
  }
1027
1100
  return all;
1028
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
+ }
1029
1172
  // ─── Property Extraction ───
1030
1173
  extractTitle(page) {
1031
1174
  const props = page.properties;
@@ -1041,9 +1184,17 @@ var NotionClient = class {
1041
1184
  return this.propertyMapper.fromNotionProperties(page.properties);
1042
1185
  }
1043
1186
  // ─── Internal ───
1187
+ lastRequestTime = 0;
1188
+ minRequestInterval = 350;
1044
1189
  async withRateLimit(fn) {
1045
1190
  await this.sema.acquire();
1046
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();
1047
1198
  return await this.executeWithRetry(fn);
1048
1199
  } finally {
1049
1200
  this.sema.release();
@@ -1084,6 +1235,60 @@ function extractRetryAfter(error) {
1084
1235
  function sleep(ms) {
1085
1236
  return new Promise((resolve) => setTimeout(resolve, ms));
1086
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
+ }
1087
1292
 
1088
1293
  // src/notion/block-builder.ts
1089
1294
  var VALID_COLORS = /* @__PURE__ */ new Set([
@@ -1589,7 +1794,7 @@ var FrontmatterExtractor = class {
1589
1794
  name = "FrontmatterExtractor";
1590
1795
  order = 10;
1591
1796
  process(input) {
1592
- const { data, content } = matter(input.content);
1797
+ const { data, content } = matter7(input.content);
1593
1798
  return {
1594
1799
  content: content.trim(),
1595
1800
  metadata: {
@@ -1830,7 +2035,7 @@ var PropertiesTableInjector = class {
1830
2035
  if (Object.keys(filteredProps).length === 0) {
1831
2036
  return { content: input.content, metadata: input.metadata };
1832
2037
  }
1833
- const yamlStr = matter.stringify("", filteredProps).trim();
2038
+ const yamlStr = matter7.stringify("", filteredProps).trim();
1834
2039
  const yamlBody = yamlStr.slice(4, -3).trim();
1835
2040
  const codeBlock = "```yaml\n# im-nobsidian:properties\n" + yamlBody + "\n```";
1836
2041
  const content = codeBlock + "\n\n---\n\n" + input.content;
@@ -1988,7 +2193,7 @@ var FrontmatterGenerator = class {
1988
2193
  return { content: input.content, metadata: input.metadata };
1989
2194
  }
1990
2195
  const normalized = normalizeProperties(properties);
1991
- const content = matter.stringify(input.content, normalized);
2196
+ const content = matter7.stringify(input.content, normalized);
1992
2197
  return {
1993
2198
  content,
1994
2199
  metadata: input.metadata
@@ -2006,6 +2211,9 @@ function normalizeProperties(props) {
2006
2211
  return result;
2007
2212
  }
2008
2213
  function normalizeValue(value) {
2214
+ if (value instanceof Date) {
2215
+ return normalizeDate(value.toISOString());
2216
+ }
2009
2217
  if (typeof value === "string") {
2010
2218
  return normalizeDate(value);
2011
2219
  }
@@ -2073,7 +2281,7 @@ ${yamlContent}---
2073
2281
  `;
2074
2282
  let parsed;
2075
2283
  try {
2076
- const result = matter(fakeDocument);
2284
+ const result = matter7(fakeDocument);
2077
2285
  parsed = result.data;
2078
2286
  } catch {
2079
2287
  return { content: input.content, metadata: input.metadata };
@@ -2282,7 +2490,7 @@ var BlockConverter = class {
2282
2490
  const title = richTextToMarkdown(b.toggle?.rich_text);
2283
2491
  const children = b.has_children ? await n2m.pageToMarkdown(b.id) : [];
2284
2492
  const childMd = (Array.isArray(children) ? n2m.toMarkdownString(children).parent : "") ?? "";
2285
- 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();
2286
2494
  return `${TOGGLE_START}
2287
2495
  - ${title}
2288
2496
  ${indented}
@@ -2487,7 +2695,7 @@ ${COLUMN_LIST_END}`;
2487
2695
  const calloutType = EMOJI_TO_TYPE[emoji] ?? "note";
2488
2696
  let result = `> [!${calloutType}] ${text}`;
2489
2697
  if (childMd?.trim()) {
2490
- const lines = childMd.trim().split("\n").map((l) => `> ${l}`);
2698
+ const lines = childMd.trim().split("\n").map((l) => l.trim() ? `> ${l}` : ">");
2491
2699
  result += "\n" + lines.join("\n");
2492
2700
  }
2493
2701
  return result;
@@ -2850,35 +3058,49 @@ var ImageHandler = class {
2850
3058
  attachmentFolder;
2851
3059
  notionClient;
2852
3060
  async downloadImage(url, pageTitle) {
2853
- const response = await fetch(url);
2854
- if (!response.ok) {
2855
- 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
+ }
2856
3088
  }
2857
- const contentType = response.headers.get("content-type") ?? "image/png";
2858
- const extension = IMAGE_EXTENSIONS[contentType] ?? this.guessExtension(url) ?? ".png";
2859
- const buffer = Buffer.from(await response.arrayBuffer());
2860
- const hash = createHash("sha256").update(buffer).digest("hex").slice(0, 12);
2861
- const safeName = pageTitle.replace(/[^a-zA-Z0-9가-힣\s-]/g, "").replace(/\s+/g, "-").slice(0, 50);
2862
- const fileName = `${safeName}-${hash}${extension}`;
2863
- const localPath = `${this.attachmentFolder}/${fileName}`;
2864
- await this.vaultFs.ensureFolder(this.attachmentFolder);
2865
- await this.vaultFs.writeBinary(localPath, buffer);
2866
- return {
2867
- originalUrl: url,
2868
- localPath,
2869
- hash,
2870
- size: buffer.length
2871
- };
3089
+ throw lastError;
2872
3090
  }
2873
3091
  async downloadAllImages(markdown, pageTitle) {
2874
3092
  const imageRegex = /!\[([^\]]*)\]\((https?:\/\/[^)]+)\)/g;
2875
3093
  const matches = [...markdown.matchAll(imageRegex)];
2876
3094
  const notionMatches = matches.filter((m) => m[2] && this.isNotionImageUrl(m[2]));
2877
- 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) {
2878
3098
  return { content: markdown, downloads: [] };
2879
3099
  }
2880
3100
  const sema = new Sema(3);
2881
- const results = await Promise.allSettled(
3101
+ const downloads = [];
3102
+ let result = markdown;
3103
+ const httpResults = await Promise.allSettled(
2882
3104
  notionMatches.map(async (match) => {
2883
3105
  await sema.acquire();
2884
3106
  try {
@@ -2891,15 +3113,34 @@ var ImageHandler = class {
2891
3113
  }
2892
3114
  })
2893
3115
  );
2894
- const downloads = [];
2895
- let result = markdown;
2896
- for (const settled of results) {
3116
+ for (const settled of httpResults) {
2897
3117
  if (settled.status !== "fulfilled") continue;
2898
3118
  const { match, download } = settled.value;
2899
3119
  downloads.push(download);
2900
3120
  const obsidianEmbed = `![[${download.localPath}${match[1] ? `|${match[1]}` : ""}]]`;
2901
3121
  result = result.replace(match[0], obsidianEmbed);
2902
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
+ }
2903
3144
  return { content: result, downloads };
2904
3145
  }
2905
3146
  async uploadLocalImage(localPath) {
@@ -2950,7 +3191,145 @@ var ImageHandler = class {
2950
3191
  const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
2951
3192
  return (ext && EXTENSION_TO_MIME[ext]) ?? "application/octet-stream";
2952
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
+ }
2953
3329
  isNotionImageUrl(url) {
3330
+ return this.isNotionFileUrl(url);
3331
+ }
3332
+ isNotionFileUrl(url) {
2954
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");
2955
3334
  }
2956
3335
  guessExtension(url) {
@@ -2958,6 +3337,25 @@ var ImageHandler = class {
2958
3337
  const ext = pathname.match(/\.(png|jpg|jpeg|gif|webp|svg|bmp|tiff)(\?|$)/i);
2959
3338
  return ext ? `.${ext[1].toLowerCase()}` : null;
2960
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
+ }
2961
3359
  };
2962
3360
  var EXTENSION_TO_BLOCK_TYPE = {
2963
3361
  ".png": "image",
@@ -3259,21 +3657,24 @@ function sanitizeFileName(name) {
3259
3657
 
3260
3658
  // src/converter/enhanced-md-converter.ts
3261
3659
  var NOTION_CALLOUT_RE = /^::: callout\n([\s\S]*?)\n:::/gm;
3262
- var NOTION_TOGGLE_RE = /<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gs;
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;
3263
3662
  var NOTION_PAGE_MENTION_RE = /<mention-page id="([^"]+)">([\s\S]*?)<\/mention-page>/g;
3264
3663
  var NOTION_USER_MENTION_RE = /<mention-user id="[^"]*">([^<]*)<\/mention-user>/g;
3265
3664
  var NOTION_DATE_MENTION_RE = /<mention-date start="([^"]*)"(?: end="([^"]*)")?[^>]*\/>/g;
3266
3665
  var NOTION_UNKNOWN_RE = /<unknown id="([^"]*)"([^>]*)\/>/g;
3267
- var NOTION_AUDIO_RE = /<audio src="([^"]*)">([\s\S]*?)<\/audio>/g;
3268
- var NOTION_VIDEO_RE = /<video src="([^"]*)">([\s\S]*?)<\/video>/g;
3269
- var NOTION_PDF_RE = /<pdf src="([^"]*)">([\s\S]*?)<\/pdf>/g;
3270
- 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;
3271
3670
  var NOTION_TAB_RE = /<tab title="([^"]*)">([\s\S]*?)<\/tab>/g;
3272
3671
  var NOTION_UNDERLINE_RE = /<span underline="true">([\s\S]*?)<\/span>/g;
3273
3672
  var TOGGLE_START2 = "%%im-nobsidian:toggle:start%%";
3274
3673
  var TOGGLE_END2 = "%%im-nobsidian:toggle:end%%";
3275
3674
  function notionEnhancedToObsidian(enhanced) {
3276
3675
  let result = enhanced;
3676
+ result = convertSyncedBlockRef(result);
3677
+ result = normalizeCodeBlockToggles(result);
3277
3678
  result = convertToggles(result);
3278
3679
  result = convertCallouts(result);
3279
3680
  result = convertPageMentions(result);
@@ -3287,8 +3688,13 @@ function notionEnhancedToObsidian(enhanced) {
3287
3688
  result = convertNotionTables(result);
3288
3689
  result = convertColorSpans(result);
3289
3690
  result = convertUnderlineSpans(result);
3691
+ result = convertColumnBlocks(result);
3692
+ result = convertDatabaseBlocks(result);
3693
+ result = cleanInlineColorAttrs(result);
3290
3694
  result = removeEmptyBlocks(result);
3291
3695
  result = unescapePipes(result);
3696
+ result = unescapeNotionChars(result);
3697
+ result = ensureCalloutContinuity(result);
3292
3698
  return result;
3293
3699
  }
3294
3700
  function obsidianToNotionEnhanced(obsidian) {
@@ -3302,39 +3708,62 @@ function obsidianToNotionEnhanced(obsidian) {
3302
3708
  result = restoreUnderlineSpans(result);
3303
3709
  return result;
3304
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
+ }
3305
3725
  function convertToggles(content) {
3306
3726
  function replaceToggle(match) {
3307
- 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);
3308
3728
  if (!m) return match;
3309
3729
  const title = m[1].trim();
3310
3730
  let body = m[2].trim();
3311
3731
  body = convertToggles(body);
3312
- const indented = body.split("\n").map((line) => line ? ` ${line}` : "").join("\n").trimEnd();
3313
- return `${TOGGLE_START2}
3314
- - ${title}
3315
- ${indented}
3316
- ${TOGGLE_END2}`;
3732
+ const calloutBody = body.split("\n").map((line) => line ? `> ${line}` : ">").join("\n");
3733
+ return `> [!toggle]- ${title}
3734
+ ${calloutBody}`;
3317
3735
  }
3318
3736
  return content.replace(NOTION_TOGGLE_RE, replaceToggle);
3319
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
+ }
3320
3749
  function convertCallouts(content) {
3321
- return content.replace(NOTION_CALLOUT_RE, (_match, body) => {
3322
- const lines = body.trim().split("\n");
3323
- const firstLine = lines[0] ?? "";
3324
- const emojiMatch = /^([\p{Emoji}️‍]+)\s*(.*)/u.exec(firstLine);
3325
- const type = emojiMatch ? emojiToCalloutType(emojiMatch[1]) : "note";
3326
- const title = emojiMatch ? emojiMatch[2] : firstLine;
3327
- const rest = lines.slice(1).join("\n").trim();
3328
- const calloutTitle = title ? `> [!${type}] ${title}` : `> [!${type}]`;
3329
- const calloutBody = rest ? "\n" + rest.split("\n").map((line) => `> ${line}`).join("\n") : "";
3330
- return calloutTitle + calloutBody;
3331
- });
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;
3332
3756
  }
3333
3757
  function convertPageMentions(content) {
3334
- return content.replace(NOTION_PAGE_MENTION_RE, (_match, _id, text) => {
3758
+ let result = content.replace(NOTION_PAGE_MENTION_RE, (_match, _id, text) => {
3335
3759
  const cleaned = text.trim();
3336
3760
  return `[[${cleaned}]]`;
3337
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;
3338
3767
  }
3339
3768
  function convertUserMentions(content) {
3340
3769
  return content.replace(NOTION_USER_MENTION_RE, (_match, name) => `@${name}`);
@@ -3352,6 +3781,20 @@ function preserveUnknownBlocks(content) {
3352
3781
  return `%%im-nobsidian:unknown:id=${id}&type=${blockType}%%`;
3353
3782
  });
3354
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
+ }
3355
3798
  function convertMediaTags(content) {
3356
3799
  let result = content;
3357
3800
  result = result.replace(NOTION_AUDIO_RE, (_match, src, caption) => {
@@ -3386,10 +3829,25 @@ function convertUnderlineSpans(content) {
3386
3829
  });
3387
3830
  }
3388
3831
  function convertTogglesToHtml(content) {
3389
- const startRe = new RegExp(escapeRegex3(TOGGLE_START2), "g");
3390
- const endRe = new RegExp(escapeRegex3(TOGGLE_END2), "g");
3832
+ const calloutToggleRe = /^> \[!toggle\]-\s*(.+)\n((?:>.*\n?)*)/gm;
3391
3833
  let result = content;
3834
+ let prev = "";
3392
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;
3393
3851
  while (result.includes(TOGGLE_START2) && safety++ < 100) {
3394
3852
  const startIdx = result.indexOf(TOGGLE_START2);
3395
3853
  const endIdx = findMatchingEnd(result, startIdx + TOGGLE_START2.length);
@@ -3485,7 +3943,7 @@ function escapeRegex3(str) {
3485
3943
  }
3486
3944
  var NOTION_PAGE_LINK_RE = /<page url="[^"]*">([\s\S]*?)<\/page>/g;
3487
3945
  var NOTION_COLOR_SPAN_RE = /<span color="([^"]+)">([\s\S]*?)<\/span>/g;
3488
- var NOTION_EMPTY_BLOCK_RE = /^<empty-block\/>\n?/gm;
3946
+ var NOTION_EMPTY_BLOCK_RE = /^(?:>[\t ]*)*[\t ]*<empty-block\/>\n?/gm;
3489
3947
  function convertPageLinks(content) {
3490
3948
  return content.replace(NOTION_PAGE_LINK_RE, (_match, text) => {
3491
3949
  const cleaned = text.replace(/\*\*/g, "").trim();
@@ -3498,6 +3956,20 @@ function convertColorSpans(content) {
3498
3956
  (_match, color, text) => `%%im-nobsidian:color:${color}%%${text}%%/color%%`
3499
3957
  );
3500
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
+ }
3501
3973
  function removeEmptyBlocks(content) {
3502
3974
  return content.replace(NOTION_EMPTY_BLOCK_RE, "\n");
3503
3975
  }
@@ -3510,11 +3982,21 @@ ${eq.trim()}
3510
3982
  $$`;
3511
3983
  });
3512
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
+ });
3513
3989
  return result;
3514
3990
  }
3991
+ function unescapeNotionChars(content) {
3992
+ return content.replace(/\\~/g, "~").replace(/\\\^/g, "^");
3993
+ }
3515
3994
  var NOTION_TABLE_RE = /<table[^>]*>([\s\S]*?)<\/table>/g;
3516
3995
  var TABLE_ROW_RE = /<tr>([\s\S]*?)<\/tr>/g;
3517
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
+ }
3518
4000
  function convertNotionTables(content) {
3519
4001
  return content.replace(NOTION_TABLE_RE, (_match, tableBody) => {
3520
4002
  const rows = [];
@@ -3527,7 +4009,9 @@ function convertNotionTables(content) {
3527
4009
  while ((cellMatch = cellRe.exec(rowMatch[1])) !== null) {
3528
4010
  cells.push(cellMatch[1].trim());
3529
4011
  }
3530
- rows.push(cells);
4012
+ if (!isAlignmentRow(cells)) {
4013
+ rows.push(cells);
4014
+ }
3531
4015
  }
3532
4016
  if (rows.length === 0) return _match;
3533
4017
  const colCount = Math.max(...rows.map((r) => r.length));
@@ -3607,6 +4091,23 @@ function unescapePipes(content) {
3607
4091
  }
3608
4092
  return result.join("\n");
3609
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
+ }
3610
4111
  var DatabaseSyncer = class {
3611
4112
  constructor(config, stateDb, notionClient, vaultFs, pipeline, imageHandler) {
3612
4113
  this.config = config;
@@ -3681,6 +4182,7 @@ var DatabaseSyncer = class {
3681
4182
  const schema = await this.notionClient.getDatabaseSchema(dbConfig.databaseId);
3682
4183
  this.propertyMapper.loadSchema(schema);
3683
4184
  await this.vaultFs.ensureFolder(dbConfig.localFolder);
4185
+ await this.pullDatabaseViews(dbConfig);
3684
4186
  const pages = await this.notionClient.queryAllDatabasePages(
3685
4187
  dbConfig.databaseId,
3686
4188
  dbConfig.pullFilter
@@ -3710,6 +4212,26 @@ var DatabaseSyncer = class {
3710
4212
  getLogger().info(`[DB Sync] ${dbConfig.localFolder}: ${created} \uC0DD\uC131, ${updated} \uC5C5\uB370\uC774\uD2B8`);
3711
4213
  return { created, updated, failed };
3712
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
+ }
3713
4235
  async pullDatabasePage(page, dbConfig) {
3714
4236
  const title = this.notionClient.extractTitle(page);
3715
4237
  const safeName = sanitizeFileName(title);
@@ -3717,6 +4239,27 @@ var DatabaseSyncer = class {
3717
4239
  page.properties
3718
4240
  );
3719
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
+ }
3720
4263
  let markdown = "";
3721
4264
  try {
3722
4265
  const mdResult = await this.notionClient.getPageMarkdown(page.id);
@@ -3727,6 +4270,10 @@ var DatabaseSyncer = class {
3727
4270
  const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
3728
4271
  markdown = imageResult.content;
3729
4272
  }
4273
+ if (markdown) {
4274
+ const fileResult = await this.imageHandler.downloadAllFiles(markdown, title);
4275
+ markdown = fileResult.content;
4276
+ }
3730
4277
  const existingRecord = this.stateDb.getByNotionId(page.id);
3731
4278
  const filePath = existingRecord?.obsidianPath ?? `${dbConfig.localFolder}/${safeName}.md`;
3732
4279
  const finalContent = this.pipeline.convertToMarkdown(
@@ -3779,7 +4326,7 @@ var DatabaseSyncer = class {
3779
4326
  const hash = computeHash(content);
3780
4327
  const record = this.stateDb.getByPath(file.path);
3781
4328
  if (record?.contentHash === hash) continue;
3782
- const parsed = matter(content);
4329
+ const parsed = matter7(content);
3783
4330
  const frontmatter = parsed.data;
3784
4331
  const body = parsed.content;
3785
4332
  const title = frontmatter.title ?? extractTitleFromPath(file.path);
@@ -3906,8 +4453,13 @@ var SyncOrchestrator = class {
3906
4453
  const changes = this.changeDetector.detectLocalChanges(files);
3907
4454
  const conflictPaths = new Set(this.stateDb.getByStatus("conflict").map((r) => r.obsidianPath));
3908
4455
  const eligible = options?.force ? changes : changes.filter((c) => !conflictPaths.has(c.path));
3909
- const filtered = options?.paths ? eligible.filter((c) => options.paths.some((p) => c.path.startsWith(p))) : eligible;
3910
- 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) {
3911
4463
  return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
3912
4464
  }
3913
4465
  if (options?.dryRun) {
@@ -3978,6 +4530,46 @@ var SyncOrchestrator = class {
3978
4530
  }
3979
4531
  });
3980
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
+ }
3981
4573
  if (this.config.sync.syncFiles !== false) {
3982
4574
  try {
3983
4575
  const fileResults = await this.fileHandler.pushAllFiles();
@@ -4091,6 +4683,54 @@ var SyncOrchestrator = class {
4091
4683
  }
4092
4684
  });
4093
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
+ }
4094
4734
  if ((this.config.notion.databases?.length ?? 0) > 0) {
4095
4735
  try {
4096
4736
  const dbResult = await this.databaseSyncer.pullAll();
@@ -4105,6 +4745,44 @@ var SyncOrchestrator = class {
4105
4745
  getLogger().warn("[Im-Nobsidian] DB Pull \uC911 \uC624\uB958:", error);
4106
4746
  }
4107
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);
4108
4786
  this.stateDb.setMeta("last_pull_at", (/* @__PURE__ */ new Date()).toISOString());
4109
4787
  this.stateDb.setMeta("last_sync_at", (/* @__PURE__ */ new Date()).toISOString());
4110
4788
  this.stateDb.setMeta("pull_in_progress", "");
@@ -4193,6 +4871,72 @@ var SyncOrchestrator = class {
4193
4871
  get isDatabaseMode() {
4194
4872
  return this.config.notion.parentMode === "database" && !!this.config.notion.databaseId;
4195
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
+ }
4196
4940
  async ensureDbSchema() {
4197
4941
  if (this.dbSchemaLoaded || !this.isDatabaseMode) return;
4198
4942
  const schema = await this.notionClient.getDatabaseSchema(this.config.notion.databaseId);
@@ -4392,7 +5136,7 @@ var SyncOrchestrator = class {
4392
5136
  const title = this.notionClient.extractTitle(page);
4393
5137
  const safeName = sanitizeFileName(title);
4394
5138
  const parentPath = await this.resolveParentPath(page);
4395
- const allChildren = await this.notionClient.fetchAllChildren(pageId);
5139
+ const allChildren = await this.notionClient.fetchAllChildrenDeep(pageId);
4396
5140
  const hasChildPages = allChildren.some((b) => "type" in b && b.type === "child_page");
4397
5141
  const markdown = await this.fetchPageMarkdown(pageId);
4398
5142
  const hasContent = markdown.trim().length > 0;
@@ -4428,6 +5172,8 @@ var SyncOrchestrator = class {
4428
5172
  const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
4429
5173
  processedMarkdown = imageResult.content;
4430
5174
  }
5175
+ const fileResult = await this.imageHandler.downloadAllFiles(processedMarkdown, title);
5176
+ processedMarkdown = fileResult.content;
4431
5177
  const finalContent = this.pipeline.convertToMarkdown(
4432
5178
  processedMarkdown,
4433
5179
  {
@@ -4440,11 +5186,12 @@ var SyncOrchestrator = class {
4440
5186
  );
4441
5187
  await this.vaultFs.writeFile(filePath, finalContent);
4442
5188
  const hash = computeHash(finalContent);
5189
+ const resolvedParentId = await this.extractParentId(page);
4443
5190
  this.stateDb.transaction(() => {
4444
5191
  this.stateDb.upsert({
4445
5192
  obsidianPath: filePath,
4446
5193
  notionPageId: pageId,
4447
- notionParentId: this.extractParentId(page),
5194
+ notionParentId: resolvedParentId,
4448
5195
  contentHash: hash,
4449
5196
  notionLastEdited: page.last_edited_time,
4450
5197
  localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4483,6 +5230,8 @@ var SyncOrchestrator = class {
4483
5230
  const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
4484
5231
  markdown = imageResult.content;
4485
5232
  }
5233
+ const fileResult = await this.imageHandler.downloadAllFiles(markdown, title);
5234
+ markdown = fileResult.content;
4486
5235
  const savedMarkers = this.stateDb.getPreserveMarkers(record.obsidianPath);
4487
5236
  const remoteContent = this.pipeline.convertToMarkdown(
4488
5237
  markdown,
@@ -4614,7 +5363,7 @@ var SyncOrchestrator = class {
4614
5363
  return fileName === folderName;
4615
5364
  }
4616
5365
  async resolveParentPath(page) {
4617
- const parentId = this.extractParentId(page);
5366
+ const parentId = await this.extractParentId(page);
4618
5367
  if (!parentId || notionIdsEqual(parentId, this.config.notion.rootPageId)) return "";
4619
5368
  const parentRecord = this.stateDb.getByNotionId(parentId);
4620
5369
  if (parentRecord) {
@@ -4758,10 +5507,30 @@ var SyncOrchestrator = class {
4758
5507
  }
4759
5508
  return this.blockConverter.notionBlocksToMarkdown(pageId);
4760
5509
  }
4761
- extractParentId(page) {
5510
+ async extractParentId(page) {
4762
5511
  const parent = page.parent;
4763
5512
  if (parent.type === "page_id") return parent.page_id ?? null;
4764
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
+ }
4765
5534
  return null;
4766
5535
  }
4767
5536
  cleanupInterruptedSync() {
@@ -5391,6 +6160,325 @@ var WatchSyncService = class {
5391
6160
  }
5392
6161
  };
5393
6162
 
5394
- export { BlockConverter, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConversionPipeline, DEFAULT_CONFIG, DatabaseSyncer, EmbedResolver, FileHandler, 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 };
5395
6483
  //# sourceMappingURL=index.js.map
5396
6484
  //# sourceMappingURL=index.js.map