@im-nobsidian/core 0.1.0 → 0.1.1
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 +39 -4
- package/dist/index.js +825 -135
- 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
|
}
|
|
@@ -640,6 +645,7 @@ var NotionClient = class {
|
|
|
640
645
|
getInternalClient() {
|
|
641
646
|
return this.client;
|
|
642
647
|
}
|
|
648
|
+
// ─── Page CRUD ───
|
|
643
649
|
async getPage(pageId) {
|
|
644
650
|
return this.withRateLimit(
|
|
645
651
|
() => this.client.pages.retrieve({ page_id: pageId })
|
|
@@ -658,6 +664,19 @@ var NotionClient = class {
|
|
|
658
664
|
})
|
|
659
665
|
);
|
|
660
666
|
}
|
|
667
|
+
async createPageWithMarkdown(params) {
|
|
668
|
+
const parent = params.parentType === "database" ? { database_id: params.parentId } : { page_id: params.parentId };
|
|
669
|
+
const properties = params.parentType === "page" ? { title: { title: [{ text: { content: params.title } }] } } : params.properties ?? {
|
|
670
|
+
title: { title: [{ text: { content: params.title } }] }
|
|
671
|
+
};
|
|
672
|
+
return this.withRateLimit(
|
|
673
|
+
() => this.client.pages.create({
|
|
674
|
+
parent,
|
|
675
|
+
properties,
|
|
676
|
+
markdown: params.markdown
|
|
677
|
+
})
|
|
678
|
+
);
|
|
679
|
+
}
|
|
661
680
|
async updatePageProperties(pageId, properties) {
|
|
662
681
|
return this.withRateLimit(
|
|
663
682
|
() => this.client.pages.update({
|
|
@@ -666,6 +685,23 @@ var NotionClient = class {
|
|
|
666
685
|
})
|
|
667
686
|
);
|
|
668
687
|
}
|
|
688
|
+
// ─── Markdown API ───
|
|
689
|
+
async getPageMarkdown(pageId) {
|
|
690
|
+
return this.withRateLimit(() => this.client.pages.retrieveMarkdown({ page_id: pageId }));
|
|
691
|
+
}
|
|
692
|
+
async replacePageMarkdown(pageId, markdown) {
|
|
693
|
+
return this.withRateLimit(
|
|
694
|
+
() => this.client.pages.updateMarkdown({
|
|
695
|
+
page_id: pageId,
|
|
696
|
+
type: "replace_content",
|
|
697
|
+
replace_content: {
|
|
698
|
+
new_str: markdown,
|
|
699
|
+
allow_deleting_content: true
|
|
700
|
+
}
|
|
701
|
+
})
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
// ─── Database / DataSource ───
|
|
669
705
|
async getDatabaseSchema(databaseId) {
|
|
670
706
|
const db = await this.withRateLimit(
|
|
671
707
|
() => this.client.databases.retrieve({ database_id: databaseId })
|
|
@@ -679,8 +715,8 @@ var NotionClient = class {
|
|
|
679
715
|
}
|
|
680
716
|
async queryDatabase(databaseId, options) {
|
|
681
717
|
const response = await this.withRateLimit(
|
|
682
|
-
() => this.client.
|
|
683
|
-
|
|
718
|
+
() => this.client.dataSources.query({
|
|
719
|
+
data_source_id: databaseId,
|
|
684
720
|
start_cursor: options?.startCursor,
|
|
685
721
|
page_size: options?.pageSize ?? 100
|
|
686
722
|
})
|
|
@@ -693,6 +729,7 @@ var NotionClient = class {
|
|
|
693
729
|
async archivePage(pageId) {
|
|
694
730
|
await this.withRateLimit(() => this.client.pages.update({ page_id: pageId, archived: true }));
|
|
695
731
|
}
|
|
732
|
+
// ─── Block CRUD ───
|
|
696
733
|
async listChildren(blockId, options) {
|
|
697
734
|
const response = await this.withRateLimit(
|
|
698
735
|
() => this.client.blocks.children.list({
|
|
@@ -731,6 +768,24 @@ var NotionClient = class {
|
|
|
731
768
|
async deleteBlock(blockId) {
|
|
732
769
|
await this.withRateLimit(() => this.client.blocks.delete({ block_id: blockId }));
|
|
733
770
|
}
|
|
771
|
+
// ─── File Upload ───
|
|
772
|
+
async uploadFile(fileData, filename, contentType) {
|
|
773
|
+
const upload = await this.withRateLimit(
|
|
774
|
+
() => this.client.fileUploads.create({ filename, content_type: contentType })
|
|
775
|
+
);
|
|
776
|
+
const fileUploadId = upload.id;
|
|
777
|
+
await this.withRateLimit(
|
|
778
|
+
() => this.client.fileUploads.send({
|
|
779
|
+
file_upload_id: fileUploadId,
|
|
780
|
+
file: { data: fileData, filename }
|
|
781
|
+
})
|
|
782
|
+
);
|
|
783
|
+
await this.withRateLimit(
|
|
784
|
+
() => this.client.fileUploads.complete({ file_upload_id: fileUploadId })
|
|
785
|
+
);
|
|
786
|
+
return fileUploadId;
|
|
787
|
+
}
|
|
788
|
+
// ─── Search & Navigation ───
|
|
734
789
|
async search(params) {
|
|
735
790
|
const response = await this.withRateLimit(
|
|
736
791
|
() => this.client.search({
|
|
@@ -768,11 +823,13 @@ var NotionClient = class {
|
|
|
768
823
|
}
|
|
769
824
|
return all;
|
|
770
825
|
}
|
|
826
|
+
// ─── Property Extraction ───
|
|
771
827
|
extractTitle(page) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
828
|
+
const props = page.properties;
|
|
829
|
+
for (const prop of Object.values(props)) {
|
|
830
|
+
if (prop.type === "title" && prop.title) {
|
|
831
|
+
const joined = prop.title.map((t) => t.plain_text ?? "").join("");
|
|
832
|
+
if (joined) return joined;
|
|
776
833
|
}
|
|
777
834
|
}
|
|
778
835
|
return "\uC81C\uBAA9 \uC5C6\uC74C";
|
|
@@ -780,6 +837,7 @@ var NotionClient = class {
|
|
|
780
837
|
extractProperties(page) {
|
|
781
838
|
return this.propertyMapper.fromNotionProperties(page.properties);
|
|
782
839
|
}
|
|
840
|
+
// ─── Internal ───
|
|
783
841
|
async withRateLimit(fn) {
|
|
784
842
|
await this.sema.acquire();
|
|
785
843
|
try {
|
|
@@ -792,7 +850,7 @@ var NotionClient = class {
|
|
|
792
850
|
try {
|
|
793
851
|
return await fn();
|
|
794
852
|
} catch (error) {
|
|
795
|
-
if (
|
|
853
|
+
if (isRetryable(error) && attempt < 5) {
|
|
796
854
|
const baseDelay = extractRetryAfter(error) ?? 1e3 * Math.pow(2, attempt);
|
|
797
855
|
const jitter = baseDelay * (0.5 + Math.random() * 0.5);
|
|
798
856
|
await sleep(jitter);
|
|
@@ -802,13 +860,20 @@ var NotionClient = class {
|
|
|
802
860
|
}
|
|
803
861
|
}
|
|
804
862
|
};
|
|
805
|
-
function
|
|
806
|
-
|
|
863
|
+
function isRetryable(error) {
|
|
864
|
+
if (typeof error !== "object" || error === null) return false;
|
|
865
|
+
const code = error.code;
|
|
866
|
+
if (code === "notionhq_client_request_timeout" || code === "ECONNRESET" || code === "ETIMEDOUT") {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
if (!("status" in error)) return false;
|
|
870
|
+
const status = error.status;
|
|
871
|
+
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
807
872
|
}
|
|
808
873
|
function extractRetryAfter(error) {
|
|
809
874
|
if (typeof error === "object" && error !== null && "headers" in error) {
|
|
810
875
|
const headers = error.headers;
|
|
811
|
-
const retryAfter = headers["retry-after"];
|
|
876
|
+
const retryAfter = headers?.["retry-after"];
|
|
812
877
|
if (retryAfter) return Number(retryAfter) * 1e3;
|
|
813
878
|
}
|
|
814
879
|
return null;
|
|
@@ -1187,6 +1252,9 @@ var ChangeDetector = class {
|
|
|
1187
1252
|
const syncedRecords = this.stateDb.getByStatus("synced");
|
|
1188
1253
|
for (const record of syncedRecords) {
|
|
1189
1254
|
if (!existingPaths.has(record.obsidianPath)) {
|
|
1255
|
+
if (record.fileType === "folder-note" || record.fileType === "folder-only") {
|
|
1256
|
+
if (!record.obsidianPath.endsWith(".md")) continue;
|
|
1257
|
+
}
|
|
1190
1258
|
changes.push({
|
|
1191
1259
|
path: record.obsidianPath,
|
|
1192
1260
|
type: "deleted",
|
|
@@ -1314,17 +1382,11 @@ var ConversionPipeline = class {
|
|
|
1314
1382
|
return input.content;
|
|
1315
1383
|
}
|
|
1316
1384
|
};
|
|
1317
|
-
var FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
1318
|
-
var WIKILINK_IN_YAML = /\[\[([^\]]+)\]\]/g;
|
|
1319
1385
|
var FrontmatterExtractor = class {
|
|
1320
1386
|
name = "FrontmatterExtractor";
|
|
1321
1387
|
order = 10;
|
|
1322
1388
|
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);
|
|
1389
|
+
const { data, content } = matter(input.content);
|
|
1328
1390
|
return {
|
|
1329
1391
|
content: content.trim(),
|
|
1330
1392
|
metadata: {
|
|
@@ -1336,21 +1398,41 @@ var FrontmatterExtractor = class {
|
|
|
1336
1398
|
};
|
|
1337
1399
|
|
|
1338
1400
|
// src/converter/pre-processors/wikilink.ts
|
|
1339
|
-
var WIKILINK_REGEX =
|
|
1401
|
+
var WIKILINK_REGEX = /(?<!!)\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
1340
1402
|
var WikilinkResolver = class {
|
|
1403
|
+
constructor(resolve) {
|
|
1404
|
+
this.resolve = resolve;
|
|
1405
|
+
}
|
|
1406
|
+
resolve;
|
|
1341
1407
|
name = "WikilinkResolver";
|
|
1342
1408
|
order = 20;
|
|
1343
1409
|
process(input) {
|
|
1410
|
+
if (input.context.direction !== "push") {
|
|
1411
|
+
return { content: input.content, metadata: input.metadata };
|
|
1412
|
+
}
|
|
1413
|
+
const preserveMarkers = input.metadata.preserveMarkers ? [...input.metadata.preserveMarkers] : [];
|
|
1344
1414
|
const resolved = input.content.replace(
|
|
1345
1415
|
WIKILINK_REGEX,
|
|
1346
1416
|
(_match, target, display) => {
|
|
1347
1417
|
const label = display ?? target;
|
|
1348
|
-
|
|
1418
|
+
if (this.resolve) {
|
|
1419
|
+
const entry = this.resolve(target);
|
|
1420
|
+
if (entry) {
|
|
1421
|
+
return `<mention-page id="${entry.notionPageId}">${label}</mention-page>`;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
preserveMarkers.push({
|
|
1425
|
+
type: "wikilink",
|
|
1426
|
+
params: display ? { text: target, display } : { text: target },
|
|
1427
|
+
startIndex: 0
|
|
1428
|
+
});
|
|
1429
|
+
const encodedTarget = encodeURIComponent(target);
|
|
1430
|
+
return `[${label}](im-nobsidian://wikilink/${encodedTarget})`;
|
|
1349
1431
|
}
|
|
1350
1432
|
);
|
|
1351
1433
|
return {
|
|
1352
1434
|
content: resolved,
|
|
1353
|
-
metadata: input.metadata
|
|
1435
|
+
metadata: { ...input.metadata, preserveMarkers }
|
|
1354
1436
|
};
|
|
1355
1437
|
}
|
|
1356
1438
|
};
|
|
@@ -1383,13 +1465,17 @@ var CalloutTransformer = class {
|
|
|
1383
1465
|
name = "CalloutTransformer";
|
|
1384
1466
|
order = 30;
|
|
1385
1467
|
process(input) {
|
|
1468
|
+
if (input.context.path === "markdown-api") {
|
|
1469
|
+
return { content: input.content, metadata: input.metadata };
|
|
1470
|
+
}
|
|
1386
1471
|
const content = input.content.replace(
|
|
1387
1472
|
CALLOUT_REGEX,
|
|
1388
1473
|
(_match, type, foldable, title) => {
|
|
1389
1474
|
const emoji = CALLOUT_EMOJI_MAP[type.toLowerCase()] ?? "\u{1F4DD}";
|
|
1390
1475
|
const titleText = title?.trim() || type;
|
|
1391
1476
|
const foldMeta = foldable ? `
|
|
1392
|
-
%% im-nobsidian:callout:type=${type}&foldable=${foldable === "+" ? "open" : "closed"} %%` :
|
|
1477
|
+
%% im-nobsidian:callout:type=${type}&foldable=${foldable === "+" ? "open" : "closed"} %%` : `
|
|
1478
|
+
%% im-nobsidian:callout:type=${type} %%`;
|
|
1393
1479
|
return `> ${emoji} **${titleText}**${foldMeta}`;
|
|
1394
1480
|
}
|
|
1395
1481
|
);
|
|
@@ -1438,12 +1524,13 @@ var EmbedResolver = class {
|
|
|
1438
1524
|
images.push({ url: target, localPath: target, isExternal: false });
|
|
1439
1525
|
if (isPush && !isExternalUrl(target)) {
|
|
1440
1526
|
const fileName = target.split("/").pop() ?? target;
|
|
1441
|
-
return `> \u{1F4CE} ${fileName}
|
|
1527
|
+
return `> \u{1F4CE} ${fileName}
|
|
1442
1528
|
> %% im-nobsidian:local-image:${target} %%`;
|
|
1443
1529
|
}
|
|
1444
1530
|
return `})`;
|
|
1445
1531
|
}
|
|
1446
|
-
|
|
1532
|
+
const encoded = encodeURIComponent(target);
|
|
1533
|
+
return `[${target}](im-nobsidian://embed/${encoded})`;
|
|
1447
1534
|
});
|
|
1448
1535
|
content = content.replace(MARKDOWN_IMAGE_REGEX, (_match, alt, url) => {
|
|
1449
1536
|
if (isVideoUrl(url)) {
|
|
@@ -1519,8 +1606,6 @@ var UnsupportedBlockStripper = class {
|
|
|
1519
1606
|
};
|
|
1520
1607
|
}
|
|
1521
1608
|
};
|
|
1522
|
-
|
|
1523
|
-
// src/converter/pre-processors/properties-table.ts
|
|
1524
1609
|
var PropertiesTableInjector = class {
|
|
1525
1610
|
name = "PropertiesTableInjector";
|
|
1526
1611
|
order = 15;
|
|
@@ -1535,32 +1620,20 @@ var PropertiesTableInjector = class {
|
|
|
1535
1620
|
if (!properties || Object.keys(properties).length === 0) {
|
|
1536
1621
|
return { content: input.content, metadata: input.metadata };
|
|
1537
1622
|
}
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1623
|
+
const filteredProps = {};
|
|
1624
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
1625
|
+
filteredProps[key] = value;
|
|
1626
|
+
}
|
|
1627
|
+
if (Object.keys(filteredProps).length === 0) {
|
|
1540
1628
|
return { content: input.content, metadata: input.metadata };
|
|
1541
1629
|
}
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
const table = ["| Property | Value |", "| --- | --- |", ...rows].join("\n");
|
|
1547
|
-
const content = table + "\n\n---\n\n" + input.content;
|
|
1630
|
+
const yamlStr = matter.stringify("", filteredProps).trim();
|
|
1631
|
+
const yamlBody = yamlStr.slice(4, -3).trim();
|
|
1632
|
+
const codeBlock = "```yaml\n# im-nobsidian:properties\n" + yamlBody + "\n```";
|
|
1633
|
+
const content = codeBlock + "\n\n---\n\n" + input.content;
|
|
1548
1634
|
return { content, metadata: input.metadata };
|
|
1549
1635
|
}
|
|
1550
1636
|
};
|
|
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
1637
|
|
|
1565
1638
|
// src/converter/pre-processors/html-annotation.ts
|
|
1566
1639
|
var UNDERLINE_REGEX = /<u>([\s\S]*?)<\/u>/g;
|
|
@@ -1582,21 +1655,30 @@ var HtmlAnnotationStripper = class {
|
|
|
1582
1655
|
};
|
|
1583
1656
|
|
|
1584
1657
|
// src/converter/post-processors/mention-to-wikilink.ts
|
|
1585
|
-
var NOTION_LINK_REGEX = /\[([^\]]+)\]\((?:https:\/\/www\.notion\.so\/|notion:\/\/)([a-f0-9-]+)\)/g;
|
|
1658
|
+
var NOTION_LINK_REGEX = /\[([^\]]+)\]\((?:https:\/\/(?:www\.)?notion\.so\/|notion:\/\/)([a-f0-9-]+)\)/g;
|
|
1659
|
+
var WIKILINK_PRESERVE_REGEX = /\[([^\]]+)\]\(im-nobsidian:\/\/wikilink\/([^)]+)\)/g;
|
|
1660
|
+
var EMBED_PRESERVE_REGEX = /\[([^\]]+)\]\(im-nobsidian:\/\/embed\/([^)]+)\)/g;
|
|
1586
1661
|
var MentionToWikilink = class {
|
|
1587
1662
|
name = "MentionToWikilink";
|
|
1588
1663
|
order = 10;
|
|
1589
1664
|
process(input) {
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1665
|
+
let content = input.content;
|
|
1666
|
+
content = content.replace(NOTION_LINK_REGEX, (_match, display) => `[[${display}]]`);
|
|
1667
|
+
content = content.replace(
|
|
1668
|
+
WIKILINK_PRESERVE_REGEX,
|
|
1669
|
+
(_match, display, encodedTarget) => {
|
|
1670
|
+
const target = decodeURIComponent(encodedTarget);
|
|
1671
|
+
return target === display ? `[[${target}]]` : `[[${target}|${display}]]`;
|
|
1594
1672
|
}
|
|
1595
1673
|
);
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1674
|
+
content = content.replace(
|
|
1675
|
+
EMBED_PRESERVE_REGEX,
|
|
1676
|
+
(_match, _display, encodedTarget) => {
|
|
1677
|
+
const target = decodeURIComponent(encodedTarget);
|
|
1678
|
+
return `![[${target}]]`;
|
|
1679
|
+
}
|
|
1680
|
+
);
|
|
1681
|
+
return { content, metadata: input.metadata };
|
|
1600
1682
|
}
|
|
1601
1683
|
};
|
|
1602
1684
|
|
|
@@ -1643,15 +1725,18 @@ var EMOJI_TYPE_MAP = {
|
|
|
1643
1725
|
"\u{1F4CC}": "example",
|
|
1644
1726
|
"\u{1F4AC}": "quote"
|
|
1645
1727
|
};
|
|
1646
|
-
var CALLOUT_PRESERVE_REGEX = /%% im-nobsidian:callout:type=(\w+)
|
|
1728
|
+
var CALLOUT_PRESERVE_REGEX = /%% im-nobsidian:callout:type=(\w+)(?:&foldable=(open|closed))? %%/g;
|
|
1647
1729
|
var CalloutRestorer = class {
|
|
1648
1730
|
name = "CalloutRestorer";
|
|
1649
1731
|
order = 20;
|
|
1650
1732
|
process(input) {
|
|
1651
1733
|
let content = input.content;
|
|
1652
1734
|
content = content.replace(CALLOUT_PRESERVE_REGEX, (_match, type, foldable) => {
|
|
1653
|
-
|
|
1654
|
-
|
|
1735
|
+
if (foldable) {
|
|
1736
|
+
const foldChar = foldable === "open" ? "+" : "-";
|
|
1737
|
+
return `[!${type}]${foldChar}`;
|
|
1738
|
+
}
|
|
1739
|
+
return `[!${type}]`;
|
|
1655
1740
|
});
|
|
1656
1741
|
for (const [emoji, type] of Object.entries(EMOJI_TYPE_MAP)) {
|
|
1657
1742
|
const pattern = new RegExp(`> ${escapeRegex(emoji)} \\*\\*(.+?)\\*\\*`, "g");
|
|
@@ -1722,9 +1807,8 @@ var LocalImageRestorer = class {
|
|
|
1722
1807
|
return { content, metadata: input.metadata };
|
|
1723
1808
|
}
|
|
1724
1809
|
};
|
|
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*/;
|
|
1810
|
+
var YAML_PROPERTIES_REGEX = /```yaml\n# im-nobsidian:properties\n([\s\S]*?)```\n*(?:---\n*)?/;
|
|
1811
|
+
var LEGACY_TABLE_REGEX = /^\s*\| Property\s*\| Value\s*\|\n\s*\|\s*-{3,}\s*\|\s*-{3,}\s*\|\n((?:\|[^\n]+\|\n?)+)\n*---\n*/;
|
|
1728
1812
|
var PropertiesTableRestorer = class {
|
|
1729
1813
|
name = "PropertiesTableRestorer";
|
|
1730
1814
|
order = 5;
|
|
@@ -1732,10 +1816,39 @@ var PropertiesTableRestorer = class {
|
|
|
1732
1816
|
if (input.context.direction !== "pull") {
|
|
1733
1817
|
return { content: input.content, metadata: input.metadata };
|
|
1734
1818
|
}
|
|
1735
|
-
const
|
|
1736
|
-
if (
|
|
1819
|
+
const yamlMatch = input.content.match(YAML_PROPERTIES_REGEX);
|
|
1820
|
+
if (yamlMatch) {
|
|
1821
|
+
return this.restoreFromYaml(input, yamlMatch);
|
|
1822
|
+
}
|
|
1823
|
+
const tableMatch = input.content.match(LEGACY_TABLE_REGEX);
|
|
1824
|
+
if (tableMatch) {
|
|
1825
|
+
return this.restoreFromTable(input, tableMatch);
|
|
1826
|
+
}
|
|
1827
|
+
return { content: input.content, metadata: input.metadata };
|
|
1828
|
+
}
|
|
1829
|
+
restoreFromYaml(input, match) {
|
|
1830
|
+
const yamlContent = match[1];
|
|
1831
|
+
const fakeDocument = `---
|
|
1832
|
+
${yamlContent}---
|
|
1833
|
+
`;
|
|
1834
|
+
let parsed;
|
|
1835
|
+
try {
|
|
1836
|
+
const result = matter(fakeDocument);
|
|
1837
|
+
parsed = result.data;
|
|
1838
|
+
} catch {
|
|
1737
1839
|
return { content: input.content, metadata: input.metadata };
|
|
1738
1840
|
}
|
|
1841
|
+
const properties = {
|
|
1842
|
+
...input.metadata.properties,
|
|
1843
|
+
...parsed
|
|
1844
|
+
};
|
|
1845
|
+
const content = input.content.replace(YAML_PROPERTIES_REGEX, "").trimStart();
|
|
1846
|
+
return {
|
|
1847
|
+
content,
|
|
1848
|
+
metadata: { ...input.metadata, properties }
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
restoreFromTable(input, match) {
|
|
1739
1852
|
const rows = match[1].trim().split("\n");
|
|
1740
1853
|
const properties = {
|
|
1741
1854
|
...input.metadata.properties
|
|
@@ -1745,31 +1858,21 @@ var PropertiesTableRestorer = class {
|
|
|
1745
1858
|
if (cells.length >= 2) {
|
|
1746
1859
|
const key = cells[0];
|
|
1747
1860
|
const rawValue = cells[1];
|
|
1748
|
-
properties[key] =
|
|
1861
|
+
properties[key] = parseLegacyValue(rawValue);
|
|
1749
1862
|
}
|
|
1750
1863
|
}
|
|
1751
|
-
const content = input.content.replace(
|
|
1864
|
+
const content = input.content.replace(LEGACY_TABLE_REGEX, "").trimStart();
|
|
1752
1865
|
return {
|
|
1753
1866
|
content,
|
|
1754
1867
|
metadata: { ...input.metadata, properties }
|
|
1755
1868
|
};
|
|
1756
1869
|
}
|
|
1757
1870
|
};
|
|
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) {
|
|
1871
|
+
function parseLegacyValue(raw) {
|
|
1766
1872
|
if (raw === "" || raw === "null") return null;
|
|
1767
1873
|
if (raw === "true") return true;
|
|
1768
1874
|
if (raw === "false") return false;
|
|
1769
1875
|
const unquoted = stripQuotes(raw);
|
|
1770
|
-
if (ISO_DATE_REGEX.test(unquoted)) {
|
|
1771
|
-
return unquoted.replace(/T00:00:00(?:\.000)?Z?$/, "");
|
|
1772
|
-
}
|
|
1773
1876
|
const num = Number(unquoted);
|
|
1774
1877
|
if (!isNaN(num) && unquoted.trim() !== "") return num;
|
|
1775
1878
|
if (unquoted.includes(", ")) {
|
|
@@ -1791,15 +1894,21 @@ function parseValue(raw) {
|
|
|
1791
1894
|
}
|
|
1792
1895
|
return unquoted;
|
|
1793
1896
|
}
|
|
1897
|
+
function stripQuotes(s) {
|
|
1898
|
+
if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
|
|
1899
|
+
return s.slice(1, -1);
|
|
1900
|
+
}
|
|
1901
|
+
return s;
|
|
1902
|
+
}
|
|
1794
1903
|
|
|
1795
1904
|
// src/converter/pipeline-factory.ts
|
|
1796
|
-
function createDefaultPipeline() {
|
|
1905
|
+
function createDefaultPipeline(options) {
|
|
1797
1906
|
const pipeline = new ConversionPipeline();
|
|
1798
1907
|
pipeline.registerPreProcessor(new HtmlAnnotationStripper());
|
|
1799
1908
|
pipeline.registerPreProcessor(new UnsupportedBlockStripper());
|
|
1800
1909
|
pipeline.registerPreProcessor(new FrontmatterExtractor());
|
|
1801
1910
|
pipeline.registerPreProcessor(new PropertiesTableInjector());
|
|
1802
|
-
pipeline.registerPreProcessor(new WikilinkResolver());
|
|
1911
|
+
pipeline.registerPreProcessor(new WikilinkResolver(options?.wikilinkResolver));
|
|
1803
1912
|
pipeline.registerPreProcessor(new CalloutTransformer());
|
|
1804
1913
|
pipeline.registerPreProcessor(new MathNormalizer());
|
|
1805
1914
|
pipeline.registerPreProcessor(new EmbedResolver());
|
|
@@ -2127,7 +2236,8 @@ ${COLUMN_LIST_END}`;
|
|
|
2127
2236
|
markdownToNotionBlocks(markdown) {
|
|
2128
2237
|
const preprocessed = this.preProcessMarkdown(markdown);
|
|
2129
2238
|
const blocks = markdownToBlocks(preprocessed);
|
|
2130
|
-
|
|
2239
|
+
const processed = this.postProcessBlocks(blocks);
|
|
2240
|
+
return normalizeBlocksForNotion(processed);
|
|
2131
2241
|
}
|
|
2132
2242
|
toggleContents = /* @__PURE__ */ new Map();
|
|
2133
2243
|
columnContents = /* @__PURE__ */ new Map();
|
|
@@ -2345,6 +2455,111 @@ ${COLUMN_LIST_END}`;
|
|
|
2345
2455
|
return result.parent ?? "";
|
|
2346
2456
|
}
|
|
2347
2457
|
};
|
|
2458
|
+
var NOTION_MAX_TABLE_ROWS = 100;
|
|
2459
|
+
var NOTION_MAX_LIST_DEPTH = 3;
|
|
2460
|
+
function normalizeBlocksForNotion(blocks) {
|
|
2461
|
+
const result = [];
|
|
2462
|
+
for (const block of blocks) {
|
|
2463
|
+
if (block.type === "table") {
|
|
2464
|
+
result.push(...normalizeTable(block));
|
|
2465
|
+
} else if (isListBlock(block)) {
|
|
2466
|
+
result.push(flattenListDepth(block, 0));
|
|
2467
|
+
} else {
|
|
2468
|
+
result.push(block);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
return result;
|
|
2472
|
+
}
|
|
2473
|
+
function normalizeTable(block) {
|
|
2474
|
+
const table = block.table;
|
|
2475
|
+
if (!table?.children) return [block];
|
|
2476
|
+
const rows = table.children;
|
|
2477
|
+
if (rows.length === 0) return [block];
|
|
2478
|
+
let maxCells = table.table_width;
|
|
2479
|
+
for (const row of rows) {
|
|
2480
|
+
const tr = row.table_row;
|
|
2481
|
+
if (tr?.cells && tr.cells.length > maxCells) {
|
|
2482
|
+
maxCells = tr.cells.length;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
const normalizedRows = rows.map((row) => {
|
|
2486
|
+
const tr = row.table_row;
|
|
2487
|
+
if (!tr?.cells) return row;
|
|
2488
|
+
const cells = [...tr.cells];
|
|
2489
|
+
while (cells.length < maxCells) {
|
|
2490
|
+
cells.push([{ type: "text", text: { content: "" } }]);
|
|
2491
|
+
}
|
|
2492
|
+
if (cells.length > maxCells) {
|
|
2493
|
+
cells.length = maxCells;
|
|
2494
|
+
}
|
|
2495
|
+
return { ...row, table_row: { ...tr, cells } };
|
|
2496
|
+
});
|
|
2497
|
+
if (normalizedRows.length <= NOTION_MAX_TABLE_ROWS) {
|
|
2498
|
+
return [
|
|
2499
|
+
{
|
|
2500
|
+
...block,
|
|
2501
|
+
table: { ...table, table_width: maxCells, children: normalizedRows }
|
|
2502
|
+
}
|
|
2503
|
+
];
|
|
2504
|
+
}
|
|
2505
|
+
const tables = [];
|
|
2506
|
+
const headerRow = table.has_column_header ? normalizedRows[0] : null;
|
|
2507
|
+
const dataRows = table.has_column_header ? normalizedRows.slice(1) : normalizedRows;
|
|
2508
|
+
const chunkSize = headerRow ? NOTION_MAX_TABLE_ROWS - 1 : NOTION_MAX_TABLE_ROWS;
|
|
2509
|
+
for (let i = 0; i < dataRows.length; i += chunkSize) {
|
|
2510
|
+
const chunk = dataRows.slice(i, i + chunkSize);
|
|
2511
|
+
const children = headerRow ? [headerRow, ...chunk] : chunk;
|
|
2512
|
+
tables.push({
|
|
2513
|
+
type: "table",
|
|
2514
|
+
table: {
|
|
2515
|
+
table_width: maxCells,
|
|
2516
|
+
has_column_header: !!headerRow,
|
|
2517
|
+
has_row_header: table.has_row_header,
|
|
2518
|
+
children
|
|
2519
|
+
}
|
|
2520
|
+
});
|
|
2521
|
+
}
|
|
2522
|
+
return tables;
|
|
2523
|
+
}
|
|
2524
|
+
function isListBlock(block) {
|
|
2525
|
+
return block.type === "bulleted_list_item" || block.type === "numbered_list_item" || block.type === "to_do";
|
|
2526
|
+
}
|
|
2527
|
+
function flattenListDepth(block, depth) {
|
|
2528
|
+
const blockType = block.type;
|
|
2529
|
+
const blockData = block[blockType];
|
|
2530
|
+
if (!blockData?.children || blockData.children.length === 0) return block;
|
|
2531
|
+
if (depth >= NOTION_MAX_LIST_DEPTH - 1) {
|
|
2532
|
+
const flattened = collectAllDescendants(blockData.children);
|
|
2533
|
+
return {
|
|
2534
|
+
...block,
|
|
2535
|
+
[blockType]: { ...blockData, children: flattened }
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
const normalizedChildren = blockData.children.map((child) => {
|
|
2539
|
+
if (isListBlock(child)) {
|
|
2540
|
+
return flattenListDepth(child, depth + 1);
|
|
2541
|
+
}
|
|
2542
|
+
return child;
|
|
2543
|
+
});
|
|
2544
|
+
return {
|
|
2545
|
+
...block,
|
|
2546
|
+
[blockType]: { ...blockData, children: normalizedChildren }
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
function collectAllDescendants(blocks) {
|
|
2550
|
+
const result = [];
|
|
2551
|
+
for (const block of blocks) {
|
|
2552
|
+
const blockType = block.type;
|
|
2553
|
+
const blockData = block[blockType];
|
|
2554
|
+
const children = blockData?.children;
|
|
2555
|
+
const blockWithoutChildren = children ? { ...block, [blockType]: { ...blockData, children: void 0 } } : block;
|
|
2556
|
+
result.push(blockWithoutChildren);
|
|
2557
|
+
if (children && children.length > 0) {
|
|
2558
|
+
result.push(...collectAllDescendants(children));
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return result;
|
|
2562
|
+
}
|
|
2348
2563
|
var IMAGE_EXTENSIONS = {
|
|
2349
2564
|
"image/png": ".png",
|
|
2350
2565
|
"image/jpeg": ".jpg",
|
|
@@ -2354,13 +2569,26 @@ var IMAGE_EXTENSIONS = {
|
|
|
2354
2569
|
"image/bmp": ".bmp",
|
|
2355
2570
|
"image/tiff": ".tiff"
|
|
2356
2571
|
};
|
|
2572
|
+
var EXTENSION_TO_MIME = {
|
|
2573
|
+
".png": "image/png",
|
|
2574
|
+
".jpg": "image/jpeg",
|
|
2575
|
+
".jpeg": "image/jpeg",
|
|
2576
|
+
".gif": "image/gif",
|
|
2577
|
+
".webp": "image/webp",
|
|
2578
|
+
".svg": "image/svg+xml",
|
|
2579
|
+
".bmp": "image/bmp",
|
|
2580
|
+
".ico": "image/x-icon",
|
|
2581
|
+
".tiff": "image/tiff"
|
|
2582
|
+
};
|
|
2357
2583
|
var ImageHandler = class {
|
|
2358
|
-
constructor(vaultFs, attachmentFolder = "attachments") {
|
|
2584
|
+
constructor(vaultFs, attachmentFolder = "attachments", notionClient) {
|
|
2359
2585
|
this.vaultFs = vaultFs;
|
|
2360
2586
|
this.attachmentFolder = attachmentFolder;
|
|
2587
|
+
this.notionClient = notionClient;
|
|
2361
2588
|
}
|
|
2362
2589
|
vaultFs;
|
|
2363
2590
|
attachmentFolder;
|
|
2591
|
+
notionClient;
|
|
2364
2592
|
async downloadImage(url, pageTitle) {
|
|
2365
2593
|
const response = await fetch(url);
|
|
2366
2594
|
if (!response.ok) {
|
|
@@ -2414,6 +2642,54 @@ var ImageHandler = class {
|
|
|
2414
2642
|
}
|
|
2415
2643
|
return { content: result, downloads };
|
|
2416
2644
|
}
|
|
2645
|
+
async uploadLocalImage(localPath) {
|
|
2646
|
+
if (!this.notionClient) {
|
|
2647
|
+
throw new Error("NotionClient required for image upload");
|
|
2648
|
+
}
|
|
2649
|
+
const buffer = await this.readImageFromVault(localPath);
|
|
2650
|
+
const filename = localPath.split("/").pop() ?? "image.png";
|
|
2651
|
+
const contentType = this.getContentTypeFromPath(filename);
|
|
2652
|
+
const blob = new Blob([buffer], { type: contentType });
|
|
2653
|
+
const fileUploadId = await this.notionClient.uploadFile(blob, filename, contentType);
|
|
2654
|
+
return { localPath, fileUploadId };
|
|
2655
|
+
}
|
|
2656
|
+
async uploadAndAppendImages(pageId, images) {
|
|
2657
|
+
if (!this.notionClient) return [];
|
|
2658
|
+
const localImages = images.filter((img) => !img.isExternal && img.localPath);
|
|
2659
|
+
if (localImages.length === 0) return [];
|
|
2660
|
+
const results = [];
|
|
2661
|
+
const imageBlocks = [];
|
|
2662
|
+
for (const img of localImages) {
|
|
2663
|
+
try {
|
|
2664
|
+
const result = await this.uploadLocalImage(img.localPath);
|
|
2665
|
+
results.push(result);
|
|
2666
|
+
imageBlocks.push({
|
|
2667
|
+
type: "image",
|
|
2668
|
+
image: {
|
|
2669
|
+
type: "file_upload",
|
|
2670
|
+
file_upload: { id: result.fileUploadId }
|
|
2671
|
+
}
|
|
2672
|
+
});
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
getLogger().warn(`\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC \uC2E4\uD328 (${img.localPath}):`, error);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
if (imageBlocks.length > 0) {
|
|
2678
|
+
await this.notionClient.appendChildren(pageId, imageBlocks);
|
|
2679
|
+
}
|
|
2680
|
+
return results;
|
|
2681
|
+
}
|
|
2682
|
+
async readImageFromVault(localPath) {
|
|
2683
|
+
try {
|
|
2684
|
+
return await this.vaultFs.readBinary(localPath);
|
|
2685
|
+
} catch {
|
|
2686
|
+
return await this.vaultFs.readBinary(`${this.attachmentFolder}/${localPath}`);
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
getContentTypeFromPath(filename) {
|
|
2690
|
+
const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
|
|
2691
|
+
return (ext && EXTENSION_TO_MIME[ext]) ?? "application/octet-stream";
|
|
2692
|
+
}
|
|
2417
2693
|
isNotionImageUrl(url) {
|
|
2418
2694
|
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
2695
|
}
|
|
@@ -2462,6 +2738,257 @@ function sanitizeFileName(name) {
|
|
|
2462
2738
|
return sanitized;
|
|
2463
2739
|
}
|
|
2464
2740
|
|
|
2741
|
+
// src/converter/enhanced-md-converter.ts
|
|
2742
|
+
var NOTION_CALLOUT_RE = /^::: callout\n([\s\S]*?)\n:::/gm;
|
|
2743
|
+
var NOTION_TOGGLE_RE = /<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/gs;
|
|
2744
|
+
var NOTION_PAGE_MENTION_RE = /<mention-page id="([^"]+)">([\s\S]*?)<\/mention-page>/g;
|
|
2745
|
+
var NOTION_USER_MENTION_RE = /<mention-user id="[^"]*">([^<]*)<\/mention-user>/g;
|
|
2746
|
+
var NOTION_DATE_MENTION_RE = /<mention-date start="([^"]*)"(?: end="([^"]*)")?[^>]*\/>/g;
|
|
2747
|
+
var NOTION_UNKNOWN_RE = /<unknown id="[^"]*"[^>]*\/>/g;
|
|
2748
|
+
var TOGGLE_START2 = "%%im-nobsidian:toggle:start%%";
|
|
2749
|
+
var TOGGLE_END2 = "%%im-nobsidian:toggle:end%%";
|
|
2750
|
+
function notionEnhancedToObsidian(enhanced) {
|
|
2751
|
+
let result = enhanced;
|
|
2752
|
+
result = convertToggles(result);
|
|
2753
|
+
result = convertCallouts(result);
|
|
2754
|
+
result = convertPageMentions(result);
|
|
2755
|
+
result = convertPageLinks(result);
|
|
2756
|
+
result = convertUserMentions(result);
|
|
2757
|
+
result = convertDateMentions(result);
|
|
2758
|
+
result = removeUnknownBlocks(result);
|
|
2759
|
+
result = convertNotionMath(result);
|
|
2760
|
+
result = convertNotionTables(result);
|
|
2761
|
+
result = convertColorSpans(result);
|
|
2762
|
+
result = removeEmptyBlocks(result);
|
|
2763
|
+
result = unescapePipes(result);
|
|
2764
|
+
return result;
|
|
2765
|
+
}
|
|
2766
|
+
function obsidianToNotionEnhanced(obsidian) {
|
|
2767
|
+
let result = obsidian;
|
|
2768
|
+
result = convertTogglesToHtml(result);
|
|
2769
|
+
result = convertObsidianCallouts(result);
|
|
2770
|
+
return result;
|
|
2771
|
+
}
|
|
2772
|
+
function convertToggles(content) {
|
|
2773
|
+
function replaceToggle(match) {
|
|
2774
|
+
const m = /<details>\s*<summary>([\s\S]*?)<\/summary>([\s\S]*?)<\/details>/s.exec(match);
|
|
2775
|
+
if (!m) return match;
|
|
2776
|
+
const title = m[1].trim();
|
|
2777
|
+
let body = m[2].trim();
|
|
2778
|
+
body = convertToggles(body);
|
|
2779
|
+
const indented = body.split("\n").map((line) => line ? ` ${line}` : "").join("\n").trimEnd();
|
|
2780
|
+
return `${TOGGLE_START2}
|
|
2781
|
+
- ${title}
|
|
2782
|
+
${indented}
|
|
2783
|
+
${TOGGLE_END2}`;
|
|
2784
|
+
}
|
|
2785
|
+
return content.replace(NOTION_TOGGLE_RE, replaceToggle);
|
|
2786
|
+
}
|
|
2787
|
+
function convertCallouts(content) {
|
|
2788
|
+
return content.replace(NOTION_CALLOUT_RE, (_match, body) => {
|
|
2789
|
+
const lines = body.trim().split("\n");
|
|
2790
|
+
const firstLine = lines[0] ?? "";
|
|
2791
|
+
const emojiMatch = /^([\p{Emoji}️]+)\s*(.*)/u.exec(firstLine);
|
|
2792
|
+
const type = emojiMatch ? emojiToCalloutType(emojiMatch[1]) : "note";
|
|
2793
|
+
const title = emojiMatch ? emojiMatch[2] : firstLine;
|
|
2794
|
+
const rest = lines.slice(1).join("\n").trim();
|
|
2795
|
+
const calloutTitle = title ? `> [!${type}] ${title}` : `> [!${type}]`;
|
|
2796
|
+
const calloutBody = rest ? "\n" + rest.split("\n").map((line) => `> ${line}`).join("\n") : "";
|
|
2797
|
+
return calloutTitle + calloutBody;
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
function convertPageMentions(content) {
|
|
2801
|
+
return content.replace(NOTION_PAGE_MENTION_RE, (_match, _id, text) => {
|
|
2802
|
+
const cleaned = text.trim();
|
|
2803
|
+
return `[[${cleaned}]]`;
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2806
|
+
function convertUserMentions(content) {
|
|
2807
|
+
return content.replace(NOTION_USER_MENTION_RE, (_match, name) => `@${name}`);
|
|
2808
|
+
}
|
|
2809
|
+
function convertDateMentions(content) {
|
|
2810
|
+
return content.replace(NOTION_DATE_MENTION_RE, (_match, start, end) => {
|
|
2811
|
+
return end ? `${start} \u2192 ${end}` : start;
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
function removeUnknownBlocks(content) {
|
|
2815
|
+
return content.replace(NOTION_UNKNOWN_RE, "");
|
|
2816
|
+
}
|
|
2817
|
+
function convertTogglesToHtml(content) {
|
|
2818
|
+
const startRe = new RegExp(escapeRegex3(TOGGLE_START2), "g");
|
|
2819
|
+
const endRe = new RegExp(escapeRegex3(TOGGLE_END2), "g");
|
|
2820
|
+
let result = content;
|
|
2821
|
+
let safety = 0;
|
|
2822
|
+
while (result.includes(TOGGLE_START2) && safety++ < 100) {
|
|
2823
|
+
const startIdx = result.indexOf(TOGGLE_START2);
|
|
2824
|
+
const endIdx = findMatchingEnd(result, startIdx + TOGGLE_START2.length);
|
|
2825
|
+
if (endIdx === -1) break;
|
|
2826
|
+
const inner = result.slice(startIdx + TOGGLE_START2.length, endIdx).trim();
|
|
2827
|
+
const titleMatch = /^- (.+)$/m.exec(inner);
|
|
2828
|
+
const title = titleMatch ? titleMatch[1] : "";
|
|
2829
|
+
const body = inner.split("\n").slice(1).map((line) => line.replace(/^ {2}/, "")).join("\n").trim();
|
|
2830
|
+
const html = `<details>
|
|
2831
|
+
<summary>${title}</summary>
|
|
2832
|
+
|
|
2833
|
+
${body}
|
|
2834
|
+
|
|
2835
|
+
</details>`;
|
|
2836
|
+
result = result.slice(0, startIdx) + html + result.slice(endIdx + TOGGLE_END2.length);
|
|
2837
|
+
}
|
|
2838
|
+
result = result.replace(startRe, "").replace(endRe, "");
|
|
2839
|
+
return result;
|
|
2840
|
+
}
|
|
2841
|
+
function findMatchingEnd(content, startFrom) {
|
|
2842
|
+
let depth = 1;
|
|
2843
|
+
let pos = startFrom;
|
|
2844
|
+
while (pos < content.length && depth > 0) {
|
|
2845
|
+
const nextStart = content.indexOf(TOGGLE_START2, pos);
|
|
2846
|
+
const nextEnd = content.indexOf(TOGGLE_END2, pos);
|
|
2847
|
+
if (nextEnd === -1) return -1;
|
|
2848
|
+
if (nextStart !== -1 && nextStart < nextEnd) {
|
|
2849
|
+
depth++;
|
|
2850
|
+
pos = nextStart + TOGGLE_START2.length;
|
|
2851
|
+
} else {
|
|
2852
|
+
depth--;
|
|
2853
|
+
if (depth === 0) return nextEnd;
|
|
2854
|
+
pos = nextEnd + TOGGLE_END2.length;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return -1;
|
|
2858
|
+
}
|
|
2859
|
+
function convertObsidianCallouts(content) {
|
|
2860
|
+
const lines = content.split("\n");
|
|
2861
|
+
const result = [];
|
|
2862
|
+
let i = 0;
|
|
2863
|
+
while (i < lines.length) {
|
|
2864
|
+
const headerMatch = /^> \[!(\w+)\]([-+])?\s*(.*)/.exec(lines[i]);
|
|
2865
|
+
if (headerMatch) {
|
|
2866
|
+
const type = headerMatch[1];
|
|
2867
|
+
const title = headerMatch[3];
|
|
2868
|
+
const emoji = calloutTypeToEmoji(type) ?? "\u{1F4A1}";
|
|
2869
|
+
const calloutTitle = title ? `${emoji} ${title}` : emoji;
|
|
2870
|
+
const bodyLines = [];
|
|
2871
|
+
i++;
|
|
2872
|
+
while (i < lines.length && lines[i].startsWith("> ")) {
|
|
2873
|
+
bodyLines.push(lines[i].slice(2));
|
|
2874
|
+
i++;
|
|
2875
|
+
}
|
|
2876
|
+
result.push("::: callout");
|
|
2877
|
+
result.push(calloutTitle);
|
|
2878
|
+
if (bodyLines.length > 0) {
|
|
2879
|
+
result.push(...bodyLines);
|
|
2880
|
+
}
|
|
2881
|
+
result.push(":::");
|
|
2882
|
+
} else {
|
|
2883
|
+
result.push(lines[i]);
|
|
2884
|
+
i++;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
return result.join("\n");
|
|
2888
|
+
}
|
|
2889
|
+
var EMOJI_TYPE_MAP2 = {
|
|
2890
|
+
"\u{1F4A1}": "tip",
|
|
2891
|
+
"\u2139\uFE0F": "info",
|
|
2892
|
+
"\u26A0\uFE0F": "warning",
|
|
2893
|
+
"\u{1F525}": "danger",
|
|
2894
|
+
"\u2705": "success",
|
|
2895
|
+
"\u274C": "failure",
|
|
2896
|
+
"\u2753": "question",
|
|
2897
|
+
"\u{1F4DD}": "note",
|
|
2898
|
+
"\u{1F4CC}": "abstract",
|
|
2899
|
+
"\u{1F41B}": "bug",
|
|
2900
|
+
"\u{1F4AC}": "quote",
|
|
2901
|
+
"\u{1F4CB}": "example"
|
|
2902
|
+
};
|
|
2903
|
+
var TYPE_EMOJI_MAP = Object.fromEntries(
|
|
2904
|
+
Object.entries(EMOJI_TYPE_MAP2).map(([k, v]) => [v, k])
|
|
2905
|
+
);
|
|
2906
|
+
function emojiToCalloutType(emoji) {
|
|
2907
|
+
return EMOJI_TYPE_MAP2[emoji] ?? "note";
|
|
2908
|
+
}
|
|
2909
|
+
function calloutTypeToEmoji(type) {
|
|
2910
|
+
return TYPE_EMOJI_MAP[type.toLowerCase()];
|
|
2911
|
+
}
|
|
2912
|
+
function escapeRegex3(str) {
|
|
2913
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2914
|
+
}
|
|
2915
|
+
var NOTION_PAGE_LINK_RE = /<page url="[^"]*">([\s\S]*?)<\/page>/g;
|
|
2916
|
+
var NOTION_COLOR_SPAN_RE = /<span color="([^"]+)">([\s\S]*?)<\/span>/g;
|
|
2917
|
+
var NOTION_EMPTY_BLOCK_RE = /^<empty-block\/>\n?/gm;
|
|
2918
|
+
function convertPageLinks(content) {
|
|
2919
|
+
return content.replace(NOTION_PAGE_LINK_RE, (_match, text) => {
|
|
2920
|
+
const cleaned = text.replace(/\*\*/g, "").trim();
|
|
2921
|
+
return `[[${cleaned}]]`;
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
function convertColorSpans(content) {
|
|
2925
|
+
return content.replace(NOTION_COLOR_SPAN_RE, (_match, _color, text) => text);
|
|
2926
|
+
}
|
|
2927
|
+
function removeEmptyBlocks(content) {
|
|
2928
|
+
return content.replace(NOTION_EMPTY_BLOCK_RE, "\n");
|
|
2929
|
+
}
|
|
2930
|
+
var NOTION_INLINE_MATH_RE = /\$`([^`]+)`\$/g;
|
|
2931
|
+
var NOTION_BLOCK_MATH_RE = /\$\$\n```\n([\s\S]*?)\n```\n\$\$/g;
|
|
2932
|
+
function convertNotionMath(content) {
|
|
2933
|
+
let result = content.replace(NOTION_BLOCK_MATH_RE, (_match, eq) => {
|
|
2934
|
+
return `$$
|
|
2935
|
+
${eq.trim()}
|
|
2936
|
+
$$`;
|
|
2937
|
+
});
|
|
2938
|
+
result = result.replace(NOTION_INLINE_MATH_RE, (_match, eq) => `$${eq}$`);
|
|
2939
|
+
return result;
|
|
2940
|
+
}
|
|
2941
|
+
var NOTION_TABLE_RE = /<table[^>]*>([\s\S]*?)<\/table>/g;
|
|
2942
|
+
var TABLE_ROW_RE = /<tr>([\s\S]*?)<\/tr>/g;
|
|
2943
|
+
var TABLE_CELL_RE = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/g;
|
|
2944
|
+
function convertNotionTables(content) {
|
|
2945
|
+
return content.replace(NOTION_TABLE_RE, (_match, tableBody) => {
|
|
2946
|
+
const rows = [];
|
|
2947
|
+
let rowMatch;
|
|
2948
|
+
const rowRe = new RegExp(TABLE_ROW_RE.source, TABLE_ROW_RE.flags);
|
|
2949
|
+
while ((rowMatch = rowRe.exec(tableBody)) !== null) {
|
|
2950
|
+
const cells = [];
|
|
2951
|
+
let cellMatch;
|
|
2952
|
+
const cellRe = new RegExp(TABLE_CELL_RE.source, TABLE_CELL_RE.flags);
|
|
2953
|
+
while ((cellMatch = cellRe.exec(rowMatch[1])) !== null) {
|
|
2954
|
+
cells.push(cellMatch[1].trim());
|
|
2955
|
+
}
|
|
2956
|
+
rows.push(cells);
|
|
2957
|
+
}
|
|
2958
|
+
if (rows.length === 0) return _match;
|
|
2959
|
+
const colCount = Math.max(...rows.map((r) => r.length));
|
|
2960
|
+
const lines = [];
|
|
2961
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2962
|
+
const padded = rows[i];
|
|
2963
|
+
while (padded.length < colCount) padded.push("");
|
|
2964
|
+
lines.push(`| ${padded.join(" | ")} |`);
|
|
2965
|
+
if (i === 0) {
|
|
2966
|
+
lines.push(`| ${padded.map(() => "---").join(" | ")} |`);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
return lines.join("\n");
|
|
2970
|
+
});
|
|
2971
|
+
}
|
|
2972
|
+
function unescapePipes(content) {
|
|
2973
|
+
const lines = content.split("\n");
|
|
2974
|
+
const result = [];
|
|
2975
|
+
let inTable = false;
|
|
2976
|
+
for (const line of lines) {
|
|
2977
|
+
if (/^\|.*\|$/.test(line.trim()) || /^\|[\s-|]+\|$/.test(line.trim())) {
|
|
2978
|
+
inTable = true;
|
|
2979
|
+
result.push(line);
|
|
2980
|
+
} else {
|
|
2981
|
+
if (inTable && line.trim() === "") {
|
|
2982
|
+
inTable = false;
|
|
2983
|
+
} else if (!/^\|/.test(line.trim())) {
|
|
2984
|
+
inTable = false;
|
|
2985
|
+
}
|
|
2986
|
+
result.push(inTable ? line : line.replace(/\\\|/g, "|"));
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
return result.join("\n");
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2465
2992
|
// src/sync/orchestrator.ts
|
|
2466
2993
|
var SyncOrchestrator = class {
|
|
2467
2994
|
constructor(config, stateDb, notionClient, vaultFs) {
|
|
@@ -2470,9 +2997,11 @@ var SyncOrchestrator = class {
|
|
|
2470
2997
|
this.notionClient = notionClient;
|
|
2471
2998
|
this.vaultFs = vaultFs;
|
|
2472
2999
|
this.changeDetector = new ChangeDetector(stateDb);
|
|
2473
|
-
this.pipeline = createDefaultPipeline(
|
|
3000
|
+
this.pipeline = createDefaultPipeline({
|
|
3001
|
+
wikilinkResolver: (text) => stateDb.resolveWikilink(text)
|
|
3002
|
+
});
|
|
2474
3003
|
this.blockConverter = new BlockConverter();
|
|
2475
|
-
this.imageHandler = new ImageHandler(vaultFs, config.paths.attachments);
|
|
3004
|
+
this.imageHandler = new ImageHandler(vaultFs, config.paths.attachments, notionClient);
|
|
2476
3005
|
this.propertyMapper = new PropertyMapper();
|
|
2477
3006
|
this.blockConverter.initNotionToMd(this.notionClient.getInternalClient());
|
|
2478
3007
|
}
|
|
@@ -2488,16 +3017,29 @@ var SyncOrchestrator = class {
|
|
|
2488
3017
|
dbSchemaLoaded = false;
|
|
2489
3018
|
async push(options) {
|
|
2490
3019
|
const startTime = Date.now();
|
|
3020
|
+
if (this.config.sync.direction === "pull") {
|
|
3021
|
+
return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
|
|
3022
|
+
}
|
|
3023
|
+
this.cleanupInterruptedSync();
|
|
2491
3024
|
const files = await this.vaultFs.listMarkdownFiles();
|
|
2492
3025
|
const changes = this.changeDetector.detectLocalChanges(files);
|
|
2493
3026
|
const conflictPaths = new Set(this.stateDb.getByStatus("conflict").map((r) => r.obsidianPath));
|
|
2494
|
-
const
|
|
2495
|
-
const filtered = options?.paths ?
|
|
3027
|
+
const eligible = options?.force ? changes : changes.filter((c) => !conflictPaths.has(c.path));
|
|
3028
|
+
const filtered = options?.paths ? eligible.filter((c) => options.paths.some((p) => c.path.startsWith(p))) : eligible;
|
|
2496
3029
|
if (filtered.length === 0) {
|
|
2497
3030
|
return { created: 0, updated: 0, deleted: 0, failed: [], duration: Date.now() - startTime };
|
|
2498
3031
|
}
|
|
2499
3032
|
if (options?.dryRun) {
|
|
2500
|
-
|
|
3033
|
+
const dryCreated = filtered.filter((c) => c.type === "created").length;
|
|
3034
|
+
const dryUpdated = filtered.filter((c) => c.type === "modified" || c.type === "moved").length;
|
|
3035
|
+
const dryDeleted = filtered.filter((c) => c.type === "deleted").length;
|
|
3036
|
+
return {
|
|
3037
|
+
created: dryCreated,
|
|
3038
|
+
updated: dryUpdated,
|
|
3039
|
+
deleted: dryDeleted,
|
|
3040
|
+
failed: [],
|
|
3041
|
+
duration: Date.now() - startTime
|
|
3042
|
+
};
|
|
2501
3043
|
}
|
|
2502
3044
|
this.stateDb.setMeta("push_in_progress", "true");
|
|
2503
3045
|
const folderPaths = /* @__PURE__ */ new Set();
|
|
@@ -2559,6 +3101,19 @@ var SyncOrchestrator = class {
|
|
|
2559
3101
|
}
|
|
2560
3102
|
async pull(options) {
|
|
2561
3103
|
const startTime = Date.now();
|
|
3104
|
+
const emptyResult = {
|
|
3105
|
+
created: 0,
|
|
3106
|
+
updated: 0,
|
|
3107
|
+
deleted: 0,
|
|
3108
|
+
conflicts: [],
|
|
3109
|
+
writtenPaths: [],
|
|
3110
|
+
failed: [],
|
|
3111
|
+
duration: Date.now() - startTime
|
|
3112
|
+
};
|
|
3113
|
+
if (this.config.sync.direction === "push") {
|
|
3114
|
+
return emptyResult;
|
|
3115
|
+
}
|
|
3116
|
+
this.cleanupInterruptedSync();
|
|
2562
3117
|
let created = 0;
|
|
2563
3118
|
let updated = 0;
|
|
2564
3119
|
let deleted = 0;
|
|
@@ -2570,11 +3125,17 @@ var SyncOrchestrator = class {
|
|
|
2570
3125
|
const record = this.stateDb.getByNotionId(c.pageId);
|
|
2571
3126
|
return record && options.paths.some((p) => record.obsidianPath.startsWith(p));
|
|
2572
3127
|
}) : remoteChanges;
|
|
2573
|
-
if (
|
|
3128
|
+
if (filtered.length === 0) {
|
|
3129
|
+
return emptyResult;
|
|
3130
|
+
}
|
|
3131
|
+
if (options?.dryRun) {
|
|
3132
|
+
const dryCreated = filtered.filter((c) => c.type === "created").length;
|
|
3133
|
+
const dryUpdated = filtered.filter((c) => c.type === "modified").length;
|
|
3134
|
+
const dryDeleted = filtered.filter((c) => c.type === "deleted").length;
|
|
2574
3135
|
return {
|
|
2575
|
-
created:
|
|
2576
|
-
updated:
|
|
2577
|
-
deleted:
|
|
3136
|
+
created: dryCreated,
|
|
3137
|
+
updated: dryUpdated,
|
|
3138
|
+
deleted: dryDeleted,
|
|
2578
3139
|
conflicts: [],
|
|
2579
3140
|
writtenPaths: [],
|
|
2580
3141
|
failed: [],
|
|
@@ -2729,7 +3290,6 @@ var SyncOrchestrator = class {
|
|
|
2729
3290
|
filePath: path,
|
|
2730
3291
|
parentMode: this.config.notion.parentMode
|
|
2731
3292
|
});
|
|
2732
|
-
const blocks = this.blockConverter.markdownToNotionBlocks(conversionResult.content);
|
|
2733
3293
|
let effectiveParentId = parentId;
|
|
2734
3294
|
let effectiveParentType = "page";
|
|
2735
3295
|
let effectiveProperties = conversionResult.properties;
|
|
@@ -2742,15 +3302,14 @@ var SyncOrchestrator = class {
|
|
|
2742
3302
|
title
|
|
2743
3303
|
);
|
|
2744
3304
|
}
|
|
2745
|
-
const page = await this.
|
|
2746
|
-
|
|
2747
|
-
|
|
3305
|
+
const page = await this.pushCreatePage(
|
|
3306
|
+
effectiveParentId,
|
|
3307
|
+
effectiveParentType,
|
|
2748
3308
|
title,
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
}
|
|
3309
|
+
conversionResult.content,
|
|
3310
|
+
effectiveProperties
|
|
3311
|
+
);
|
|
3312
|
+
await this.imageHandler.uploadAndAppendImages(page.id, conversionResult.images);
|
|
2754
3313
|
const hash = computeHash(content);
|
|
2755
3314
|
this.stateDb.transaction(() => {
|
|
2756
3315
|
this.stateDb.upsert({
|
|
@@ -2765,11 +3324,12 @@ var SyncOrchestrator = class {
|
|
|
2765
3324
|
status: "synced",
|
|
2766
3325
|
baseSnapshot: Buffer.from(content, "utf-8")
|
|
2767
3326
|
});
|
|
3327
|
+
const aliases = extractAliases(conversionResult.properties);
|
|
2768
3328
|
this.stateDb.upsertWikilink({
|
|
2769
3329
|
obsidianPath: path,
|
|
2770
3330
|
notionPageId: page.id,
|
|
2771
3331
|
title,
|
|
2772
|
-
aliases
|
|
3332
|
+
aliases
|
|
2773
3333
|
});
|
|
2774
3334
|
this.stateDb.storePreserveMarkers(path, conversionResult.preserveMarkers);
|
|
2775
3335
|
});
|
|
@@ -2790,28 +3350,32 @@ var SyncOrchestrator = class {
|
|
|
2790
3350
|
filePath: path,
|
|
2791
3351
|
parentMode: this.config.notion.parentMode
|
|
2792
3352
|
});
|
|
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
|
-
}
|
|
3353
|
+
await this.pushUpdatePage(record.notionPageId, conversionResult.content);
|
|
3354
|
+
await this.imageHandler.uploadAndAppendImages(record.notionPageId, conversionResult.images);
|
|
2801
3355
|
let propsToUpdate = conversionResult.properties;
|
|
2802
3356
|
if (this.isDatabaseMode && propsToUpdate && Object.keys(propsToUpdate).length > 0) {
|
|
2803
3357
|
await this.ensureDbSchema();
|
|
2804
|
-
const
|
|
2805
|
-
propsToUpdate = this.propertyMapper.toNotionProperties(propsToUpdate,
|
|
3358
|
+
const title2 = extractTitle(path);
|
|
3359
|
+
propsToUpdate = this.propertyMapper.toNotionProperties(propsToUpdate, title2);
|
|
2806
3360
|
}
|
|
2807
3361
|
if (propsToUpdate && Object.keys(propsToUpdate).length > 0) {
|
|
2808
3362
|
await this.notionClient.updatePageProperties(record.notionPageId, propsToUpdate);
|
|
2809
3363
|
}
|
|
3364
|
+
const updatedPage = await this.notionClient.getPage(record.notionPageId);
|
|
2810
3365
|
const hash = computeHash(content);
|
|
3366
|
+
const title = extractTitle(path);
|
|
3367
|
+
const aliases = extractAliases(conversionResult.properties);
|
|
2811
3368
|
this.stateDb.transaction(() => {
|
|
2812
3369
|
this.stateDb.updateHash(record.id, hash, Buffer.from(content, "utf-8"));
|
|
2813
3370
|
this.stateDb.updateStatus(record.id, "synced");
|
|
3371
|
+
this.stateDb.setNotionLastEdited(record.id, updatedPage.last_edited_time);
|
|
2814
3372
|
this.stateDb.storePreserveMarkers(path, conversionResult.preserveMarkers);
|
|
3373
|
+
this.stateDb.upsertWikilink({
|
|
3374
|
+
obsidianPath: path,
|
|
3375
|
+
notionPageId: record.notionPageId,
|
|
3376
|
+
title,
|
|
3377
|
+
aliases
|
|
3378
|
+
});
|
|
2815
3379
|
});
|
|
2816
3380
|
}
|
|
2817
3381
|
async pushDelete(path) {
|
|
@@ -2819,8 +3383,10 @@ var SyncOrchestrator = class {
|
|
|
2819
3383
|
if (!record?.notionPageId) return;
|
|
2820
3384
|
if (this.config.sync.deleteSync) {
|
|
2821
3385
|
await this.notionClient.archivePage(record.notionPageId);
|
|
3386
|
+
this.stateDb.delete(record.id);
|
|
3387
|
+
} else {
|
|
3388
|
+
this.stateDb.updateStatus(record.id, "pending");
|
|
2822
3389
|
}
|
|
2823
|
-
this.stateDb.delete(record.id);
|
|
2824
3390
|
}
|
|
2825
3391
|
async detectRemoteChanges() {
|
|
2826
3392
|
const changes = [];
|
|
@@ -2885,24 +3451,25 @@ var SyncOrchestrator = class {
|
|
|
2885
3451
|
const title = this.notionClient.extractTitle(page);
|
|
2886
3452
|
const safeName = sanitizeFileName(title);
|
|
2887
3453
|
const parentPath = await this.resolveParentPath(page);
|
|
2888
|
-
const
|
|
2889
|
-
const hasChildPages =
|
|
2890
|
-
const markdown = await this.
|
|
3454
|
+
const allChildren = await this.notionClient.fetchAllChildren(pageId);
|
|
3455
|
+
const hasChildPages = allChildren.some((b) => "type" in b && b.type === "child_page");
|
|
3456
|
+
const markdown = await this.fetchPageMarkdown(pageId);
|
|
2891
3457
|
const hasContent = markdown.trim().length > 0;
|
|
2892
3458
|
let filePath;
|
|
2893
3459
|
let fileType;
|
|
2894
3460
|
if (hasChildPages && hasContent) {
|
|
2895
3461
|
const folderPath = parentPath ? `${parentPath}/${safeName}` : safeName;
|
|
2896
|
-
filePath = `${folderPath}/${safeName}.md
|
|
3462
|
+
filePath = await this.resolveUniqueFilePath(`${folderPath}/${safeName}.md`);
|
|
2897
3463
|
fileType = "folder-note";
|
|
2898
3464
|
await this.vaultFs.ensureFolder(folderPath);
|
|
2899
3465
|
} else if (hasChildPages && !hasContent) {
|
|
2900
3466
|
const folderPath = parentPath ? `${parentPath}/${safeName}` : safeName;
|
|
2901
|
-
filePath = `${folderPath}/${safeName}.md
|
|
3467
|
+
filePath = await this.resolveUniqueFilePath(`${folderPath}/${safeName}.md`);
|
|
2902
3468
|
fileType = "folder-only";
|
|
2903
3469
|
await this.vaultFs.ensureFolder(folderPath);
|
|
2904
3470
|
} else {
|
|
2905
|
-
|
|
3471
|
+
const basePath = parentPath ? `${parentPath}/${safeName}.md` : `${safeName}.md`;
|
|
3472
|
+
filePath = await this.resolveUniqueFilePath(basePath);
|
|
2906
3473
|
fileType = "file";
|
|
2907
3474
|
}
|
|
2908
3475
|
let properties;
|
|
@@ -2914,6 +3481,7 @@ var SyncOrchestrator = class {
|
|
|
2914
3481
|
} else {
|
|
2915
3482
|
properties = this.notionClient.extractProperties(page);
|
|
2916
3483
|
}
|
|
3484
|
+
properties.title = title;
|
|
2917
3485
|
let processedMarkdown = markdown;
|
|
2918
3486
|
if (this.config.conversion.imageDownload === "immediate") {
|
|
2919
3487
|
const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
|
|
@@ -2944,11 +3512,12 @@ var SyncOrchestrator = class {
|
|
|
2944
3512
|
status: "synced",
|
|
2945
3513
|
baseSnapshot: Buffer.from(finalContent, "utf-8")
|
|
2946
3514
|
});
|
|
3515
|
+
const aliases = extractAliases(properties);
|
|
2947
3516
|
this.stateDb.upsertWikilink({
|
|
2948
3517
|
obsidianPath: filePath,
|
|
2949
3518
|
notionPageId: pageId,
|
|
2950
3519
|
title,
|
|
2951
|
-
aliases
|
|
3520
|
+
aliases
|
|
2952
3521
|
});
|
|
2953
3522
|
});
|
|
2954
3523
|
return filePath;
|
|
@@ -2957,7 +3526,8 @@ var SyncOrchestrator = class {
|
|
|
2957
3526
|
const record = this.stateDb.getByNotionId(change.pageId);
|
|
2958
3527
|
if (!record) return {};
|
|
2959
3528
|
const page = await this.notionClient.getPage(change.pageId);
|
|
2960
|
-
let markdown = await this.
|
|
3529
|
+
let markdown = await this.fetchPageMarkdown(change.pageId);
|
|
3530
|
+
const title = this.notionClient.extractTitle(page);
|
|
2961
3531
|
let properties;
|
|
2962
3532
|
if (this.isDatabaseMode) {
|
|
2963
3533
|
await this.ensureDbSchema();
|
|
@@ -2967,8 +3537,8 @@ var SyncOrchestrator = class {
|
|
|
2967
3537
|
} else {
|
|
2968
3538
|
properties = this.notionClient.extractProperties(page);
|
|
2969
3539
|
}
|
|
3540
|
+
properties.title = title;
|
|
2970
3541
|
if (this.config.conversion.imageDownload === "immediate") {
|
|
2971
|
-
const title = this.notionClient.extractTitle(page);
|
|
2972
3542
|
const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
|
|
2973
3543
|
markdown = imageResult.content;
|
|
2974
3544
|
}
|
|
@@ -2992,21 +3562,26 @@ var SyncOrchestrator = class {
|
|
|
2992
3562
|
const localHash = computeHash(localContent);
|
|
2993
3563
|
const localModified = localHash !== record.contentHash;
|
|
2994
3564
|
if (localModified) {
|
|
2995
|
-
const
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3565
|
+
const strategy = this.config.sync.conflictStrategy;
|
|
3566
|
+
if (strategy === "remote-first") ; else if (strategy === "local-first") {
|
|
3567
|
+
return { path: record.obsidianPath };
|
|
3568
|
+
} else {
|
|
3569
|
+
const conflict = {
|
|
3570
|
+
syncRecord: record,
|
|
3571
|
+
localChange: {
|
|
3572
|
+
path: record.obsidianPath,
|
|
3573
|
+
type: "modified",
|
|
3574
|
+
currentHash: localHash,
|
|
3575
|
+
previousHash: record.contentHash
|
|
3576
|
+
},
|
|
3577
|
+
remoteChange: change,
|
|
3578
|
+
baseContent: record.baseSnapshot?.toString("utf-8") ?? null,
|
|
3579
|
+
localContent,
|
|
3580
|
+
remoteContent
|
|
3581
|
+
};
|
|
3582
|
+
this.stateDb.updateStatus(record.id, "conflict");
|
|
3583
|
+
return { conflict };
|
|
3584
|
+
}
|
|
3010
3585
|
}
|
|
3011
3586
|
await this.vaultFs.writeFile(record.obsidianPath, remoteContent);
|
|
3012
3587
|
const newHash = computeHash(remoteContent);
|
|
@@ -3039,6 +3614,22 @@ var SyncOrchestrator = class {
|
|
|
3039
3614
|
async ensureFolderPage(folderPath) {
|
|
3040
3615
|
const existing = this.stateDb.getByPath(folderPath);
|
|
3041
3616
|
if (existing?.notionPageId) return;
|
|
3617
|
+
const folderNotePath = `${folderPath}/${folderPath.split("/").pop()}.md`;
|
|
3618
|
+
const folderNoteRecord = this.stateDb.getByPath(folderNotePath);
|
|
3619
|
+
if (folderNoteRecord?.notionParentId) {
|
|
3620
|
+
this.stateDb.upsert({
|
|
3621
|
+
obsidianPath: folderPath,
|
|
3622
|
+
notionPageId: folderNoteRecord.notionParentId,
|
|
3623
|
+
notionParentId: "",
|
|
3624
|
+
contentHash: "",
|
|
3625
|
+
notionLastEdited: folderNoteRecord.notionLastEdited,
|
|
3626
|
+
localLastModified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3627
|
+
syncDirection: "both",
|
|
3628
|
+
fileType: "folder-note",
|
|
3629
|
+
status: "synced"
|
|
3630
|
+
});
|
|
3631
|
+
return;
|
|
3632
|
+
}
|
|
3042
3633
|
const parts = folderPath.split("/");
|
|
3043
3634
|
const folderName = parts[parts.length - 1];
|
|
3044
3635
|
let parentId = this.config.notion.rootPageId;
|
|
@@ -3099,23 +3690,118 @@ var SyncOrchestrator = class {
|
|
|
3099
3690
|
try {
|
|
3100
3691
|
const parentPage = await this.notionClient.getPage(parentId);
|
|
3101
3692
|
const parentTitle = this.notionClient.extractTitle(parentPage);
|
|
3102
|
-
|
|
3693
|
+
const grandparentPath = await this.resolveParentPath(parentPage);
|
|
3694
|
+
const safeName = sanitizeFileName(parentTitle);
|
|
3695
|
+
return grandparentPath ? `${grandparentPath}/${safeName}` : safeName;
|
|
3103
3696
|
} catch {
|
|
3104
3697
|
return "";
|
|
3105
3698
|
}
|
|
3106
3699
|
}
|
|
3700
|
+
async pushCreatePage(parentId, parentType, title, markdownContent, properties) {
|
|
3701
|
+
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
3702
|
+
try {
|
|
3703
|
+
const enhanced = obsidianToNotionEnhanced(markdownContent);
|
|
3704
|
+
return await this.notionClient.createPageWithMarkdown({
|
|
3705
|
+
parentId,
|
|
3706
|
+
parentType,
|
|
3707
|
+
title,
|
|
3708
|
+
markdown: enhanced,
|
|
3709
|
+
properties
|
|
3710
|
+
});
|
|
3711
|
+
} catch {
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
|
|
3715
|
+
const page = await this.notionClient.createPage({
|
|
3716
|
+
parentId,
|
|
3717
|
+
parentType,
|
|
3718
|
+
title,
|
|
3719
|
+
properties
|
|
3720
|
+
});
|
|
3721
|
+
if (blocks.length > 0) {
|
|
3722
|
+
await this.notionClient.appendChildren(page.id, blocks);
|
|
3723
|
+
}
|
|
3724
|
+
return page;
|
|
3725
|
+
}
|
|
3726
|
+
async pushUpdatePage(pageId, markdownContent) {
|
|
3727
|
+
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
3728
|
+
try {
|
|
3729
|
+
const enhanced = obsidianToNotionEnhanced(markdownContent);
|
|
3730
|
+
await this.notionClient.replacePageMarkdown(pageId, enhanced);
|
|
3731
|
+
return;
|
|
3732
|
+
} catch {
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
const blocks = this.blockConverter.markdownToNotionBlocks(markdownContent);
|
|
3736
|
+
const existingBlocks = await this.notionClient.fetchAllChildren(pageId);
|
|
3737
|
+
if (blocks.length > 0) {
|
|
3738
|
+
await this.notionClient.appendChildren(pageId, blocks);
|
|
3739
|
+
}
|
|
3740
|
+
const deleteSema = new Sema(this.config.advanced.concurrency);
|
|
3741
|
+
await Promise.all(
|
|
3742
|
+
existingBlocks.map(async (block) => {
|
|
3743
|
+
await deleteSema.acquire();
|
|
3744
|
+
try {
|
|
3745
|
+
await this.notionClient.deleteBlock(block.id);
|
|
3746
|
+
} finally {
|
|
3747
|
+
deleteSema.release();
|
|
3748
|
+
}
|
|
3749
|
+
})
|
|
3750
|
+
);
|
|
3751
|
+
}
|
|
3752
|
+
async fetchPageMarkdown(pageId) {
|
|
3753
|
+
if (this.config.conversion.preferMarkdownApi !== false) {
|
|
3754
|
+
try {
|
|
3755
|
+
const result = await this.notionClient.getPageMarkdown(pageId);
|
|
3756
|
+
return notionEnhancedToObsidian(result.markdown);
|
|
3757
|
+
} catch {
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
return this.blockConverter.notionBlocksToMarkdown(pageId);
|
|
3761
|
+
}
|
|
3107
3762
|
extractParentId(page) {
|
|
3108
3763
|
const parent = page.parent;
|
|
3109
3764
|
if (parent.type === "page_id") return parent.page_id ?? null;
|
|
3110
3765
|
if (parent.type === "database_id") return parent.database_id ?? null;
|
|
3111
3766
|
return null;
|
|
3112
3767
|
}
|
|
3768
|
+
cleanupInterruptedSync() {
|
|
3769
|
+
const pushInProgress = this.stateDb.getMeta("push_in_progress");
|
|
3770
|
+
const pullInProgress = this.stateDb.getMeta("pull_in_progress");
|
|
3771
|
+
if (pushInProgress === "true") {
|
|
3772
|
+
getLogger().warn("[Im-Nobsidian] \uC774\uC804 push\uAC00 \uBE44\uC815\uC0C1 \uC885\uB8CC\uB428 \u2014 \uD50C\uB798\uADF8 \uC815\uB9AC");
|
|
3773
|
+
this.stateDb.setMeta("push_in_progress", "");
|
|
3774
|
+
}
|
|
3775
|
+
if (pullInProgress === "true") {
|
|
3776
|
+
getLogger().warn("[Im-Nobsidian] \uC774\uC804 pull\uC774 \uBE44\uC815\uC0C1 \uC885\uB8CC\uB428 \u2014 \uD50C\uB798\uADF8 \uC815\uB9AC");
|
|
3777
|
+
this.stateDb.setMeta("pull_in_progress", "");
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
async resolveUniqueFilePath(basePath) {
|
|
3781
|
+
if (!await this.vaultFs.exists(basePath)) return basePath;
|
|
3782
|
+
const dir = basePath.lastIndexOf("/") >= 0 ? basePath.slice(0, basePath.lastIndexOf("/")) : "";
|
|
3783
|
+
const ext = ".md";
|
|
3784
|
+
const name = basePath.slice(dir ? dir.length + 1 : 0, -ext.length);
|
|
3785
|
+
for (let i = 1; i <= 99; i++) {
|
|
3786
|
+
const candidate = dir ? `${dir}/${name} (${i})${ext}` : `${name} (${i})${ext}`;
|
|
3787
|
+
if (!await this.vaultFs.exists(candidate)) return candidate;
|
|
3788
|
+
}
|
|
3789
|
+
return basePath;
|
|
3790
|
+
}
|
|
3113
3791
|
};
|
|
3114
3792
|
function extractTitle(filePath) {
|
|
3115
3793
|
const parts = filePath.split("/");
|
|
3116
3794
|
const filename = parts[parts.length - 1] ?? "";
|
|
3117
3795
|
return filename.replace(/\.md$/, "");
|
|
3118
3796
|
}
|
|
3797
|
+
function extractAliases(properties) {
|
|
3798
|
+
const raw = properties.aliases ?? properties.alias;
|
|
3799
|
+
if (!raw) return [];
|
|
3800
|
+
if (Array.isArray(raw)) return raw.filter((a) => typeof a === "string");
|
|
3801
|
+
if (typeof raw === "string")
|
|
3802
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3803
|
+
return [];
|
|
3804
|
+
}
|
|
3119
3805
|
var NodeVaultFS = class {
|
|
3120
3806
|
constructor(rootPath, pathConfig) {
|
|
3121
3807
|
this.rootPath = rootPath;
|
|
@@ -3140,6 +3826,10 @@ var NodeVaultFS = class {
|
|
|
3140
3826
|
const fullPath = join(this.rootPath, path);
|
|
3141
3827
|
return readFile(fullPath, "utf-8");
|
|
3142
3828
|
}
|
|
3829
|
+
async readBinary(path) {
|
|
3830
|
+
const fullPath = join(this.rootPath, path);
|
|
3831
|
+
return readFile(fullPath);
|
|
3832
|
+
}
|
|
3143
3833
|
async writeFile(path, content) {
|
|
3144
3834
|
const fullPath = join(this.rootPath, path);
|
|
3145
3835
|
await mkdir(dirname(fullPath), { recursive: true });
|