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