@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.d.ts +164 -2
- package/dist/index.js +1161 -73
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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.
|
|
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:
|
|
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.
|
|
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
|
|
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 } =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
2854
|
-
|
|
2855
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
3268
|
-
var NOTION_VIDEO_RE =
|
|
3269
|
-
var NOTION_PDF_RE =
|
|
3270
|
-
var NOTION_FILE_RE =
|
|
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 =
|
|
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
|
|
3313
|
-
return
|
|
3314
|
-
|
|
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
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
+
``,
|
|
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 =
|
|
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
|
|
3910
|
-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|