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