@im-nobsidian/core 0.2.0 → 0.3.0
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 +43 -7
- package/dist/index.js +772 -98
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -2312,6 +2312,50 @@ var FrontmatterExtractor = class {
|
|
|
2312
2312
|
}
|
|
2313
2313
|
};
|
|
2314
2314
|
|
|
2315
|
+
// src/utils/md-regions.ts
|
|
2316
|
+
function computeAnchor(content, idx) {
|
|
2317
|
+
const before = content.slice(Math.max(0, idx - 32), idx);
|
|
2318
|
+
const sameLine = before.split("\n").pop() ?? "";
|
|
2319
|
+
if (sameLine.trim().length >= 4) return sameLine;
|
|
2320
|
+
const prevLines = content.slice(0, idx).split("\n");
|
|
2321
|
+
for (let i = prevLines.length - 2; i >= 0; i--) {
|
|
2322
|
+
const line = prevLines[i].trim();
|
|
2323
|
+
if (line.length > 0) return line.slice(0, 48);
|
|
2324
|
+
}
|
|
2325
|
+
return "";
|
|
2326
|
+
}
|
|
2327
|
+
function mapOutsideCodeFences(content, fn) {
|
|
2328
|
+
const lines = content.split("\n");
|
|
2329
|
+
const out = [];
|
|
2330
|
+
let buffer = [];
|
|
2331
|
+
let fence = null;
|
|
2332
|
+
const flush = () => {
|
|
2333
|
+
if (buffer.length > 0) {
|
|
2334
|
+
out.push(fn(buffer.join("\n")));
|
|
2335
|
+
buffer = [];
|
|
2336
|
+
}
|
|
2337
|
+
};
|
|
2338
|
+
for (const line of lines) {
|
|
2339
|
+
const fenceMatch = line.match(/^\s*(```+|~~~+)/);
|
|
2340
|
+
if (fence === null && fenceMatch) {
|
|
2341
|
+
flush();
|
|
2342
|
+
fence = fenceMatch[1][0].repeat(3);
|
|
2343
|
+
out.push(line);
|
|
2344
|
+
continue;
|
|
2345
|
+
}
|
|
2346
|
+
if (fence !== null) {
|
|
2347
|
+
out.push(line);
|
|
2348
|
+
if (fenceMatch && fenceMatch[1].startsWith(fence)) {
|
|
2349
|
+
fence = null;
|
|
2350
|
+
}
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
buffer.push(line);
|
|
2354
|
+
}
|
|
2355
|
+
flush();
|
|
2356
|
+
return out.join("\n");
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2315
2359
|
// src/converter/pre-processors/wikilink.ts
|
|
2316
2360
|
var WIKILINK_REGEX2 = /(?<!!)\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
2317
2361
|
var WikilinkResolver = class {
|
|
@@ -2328,7 +2372,7 @@ var WikilinkResolver = class {
|
|
|
2328
2372
|
const preserveMarkers = input.metadata.preserveMarkers ? [...input.metadata.preserveMarkers] : [];
|
|
2329
2373
|
const resolved = input.content.replace(
|
|
2330
2374
|
WIKILINK_REGEX2,
|
|
2331
|
-
(_match, target, display) => {
|
|
2375
|
+
(_match, target, display, offset) => {
|
|
2332
2376
|
const label = display ?? target;
|
|
2333
2377
|
if (this.resolve) {
|
|
2334
2378
|
const entry = this.resolve(target);
|
|
@@ -2336,10 +2380,12 @@ var WikilinkResolver = class {
|
|
|
2336
2380
|
return `<mention-page id="${entry.notionPageId}">${label}</mention-page>`;
|
|
2337
2381
|
}
|
|
2338
2382
|
}
|
|
2383
|
+
const params = display ? { text: target, display } : { text: target };
|
|
2384
|
+
params.__anchor = computeAnchor(input.content, offset);
|
|
2339
2385
|
preserveMarkers.push({
|
|
2340
2386
|
type: "wikilink",
|
|
2341
|
-
params
|
|
2342
|
-
startIndex:
|
|
2387
|
+
params,
|
|
2388
|
+
startIndex: offset
|
|
2343
2389
|
});
|
|
2344
2390
|
const encodedTarget = encodeURIComponent(target);
|
|
2345
2391
|
return `[${label}](${WIKILINK_PROTOCOL}${encodedTarget})`;
|
|
@@ -2444,6 +2490,11 @@ var EmbedResolver = class {
|
|
|
2444
2490
|
}
|
|
2445
2491
|
return `})`;
|
|
2446
2492
|
}
|
|
2493
|
+
if (isPush && !isExternalUrl(target) && isAttachmentFile(target)) {
|
|
2494
|
+
const fileName = target.split("/").pop() ?? target;
|
|
2495
|
+
return `> \u{1F4CE} ${fileName}
|
|
2496
|
+
> ${spacedMarker(`local-file:${target}`)}`;
|
|
2497
|
+
}
|
|
2447
2498
|
const encoded = encodeURIComponent(target);
|
|
2448
2499
|
return `[${target}](${EMBED_PROTOCOL}${encoded})`;
|
|
2449
2500
|
});
|
|
@@ -2466,6 +2517,11 @@ var EmbedResolver = class {
|
|
|
2466
2517
|
function isImageFile(path) {
|
|
2467
2518
|
return /\.(png|jpg|jpeg|gif|svg|webp|bmp|ico)$/i.test(path);
|
|
2468
2519
|
}
|
|
2520
|
+
function isAttachmentFile(target) {
|
|
2521
|
+
const path = target.split("|")[0];
|
|
2522
|
+
const ext = /\.([a-z0-9]+)$/i.exec(path)?.[1]?.toLowerCase();
|
|
2523
|
+
return ext !== void 0 && ext !== "md" && ext !== "canvas";
|
|
2524
|
+
}
|
|
2469
2525
|
function isVideoUrl(url) {
|
|
2470
2526
|
return VIDEO_HOSTS.some((host) => url.includes(host));
|
|
2471
2527
|
}
|
|
@@ -2484,7 +2540,7 @@ var PreserveMarkerCollector = class {
|
|
|
2484
2540
|
for (const match of matches) {
|
|
2485
2541
|
markers.push({
|
|
2486
2542
|
type: match[1],
|
|
2487
|
-
params: parseMarkerParams(match[2]),
|
|
2543
|
+
params: parseMarkerParams(match[2], input.content, match.index),
|
|
2488
2544
|
startIndex: match.index
|
|
2489
2545
|
});
|
|
2490
2546
|
}
|
|
@@ -2494,14 +2550,19 @@ var PreserveMarkerCollector = class {
|
|
|
2494
2550
|
};
|
|
2495
2551
|
}
|
|
2496
2552
|
};
|
|
2497
|
-
function parseMarkerParams(paramString) {
|
|
2553
|
+
function parseMarkerParams(paramString, content, index) {
|
|
2498
2554
|
const params = {};
|
|
2499
|
-
|
|
2500
|
-
const
|
|
2501
|
-
|
|
2502
|
-
|
|
2555
|
+
if (paramString.includes("=")) {
|
|
2556
|
+
for (const pair of paramString.split("&")) {
|
|
2557
|
+
const eqIdx = pair.indexOf("=");
|
|
2558
|
+
if (eqIdx > 0) {
|
|
2559
|
+
params[pair.slice(0, eqIdx)] = decodeURIComponent(pair.slice(eqIdx + 1));
|
|
2560
|
+
}
|
|
2503
2561
|
}
|
|
2562
|
+
} else {
|
|
2563
|
+
params.__raw = paramString;
|
|
2504
2564
|
}
|
|
2565
|
+
params.__anchor = computeAnchor(content, index);
|
|
2505
2566
|
return params;
|
|
2506
2567
|
}
|
|
2507
2568
|
|
|
@@ -2522,7 +2583,20 @@ var UnsupportedBlockStripper = class {
|
|
|
2522
2583
|
}
|
|
2523
2584
|
};
|
|
2524
2585
|
function stringifyFrontmatter(content, data) {
|
|
2525
|
-
|
|
2586
|
+
const out = matter.stringify(
|
|
2587
|
+
{ content },
|
|
2588
|
+
data
|
|
2589
|
+
);
|
|
2590
|
+
return unquoteFrontmatterDates(out);
|
|
2591
|
+
}
|
|
2592
|
+
var QUOTED_DATE_LINE_RE = /^([ \t]*[^:\n]+:[ \t]*)'(\d{4}-\d{2}-\d{2})'([ \t]*)$/gm;
|
|
2593
|
+
function unquoteFrontmatterDates(out) {
|
|
2594
|
+
if (!out.startsWith("---\n")) return out;
|
|
2595
|
+
const close = out.indexOf("\n---\n", 3);
|
|
2596
|
+
if (close === -1) return out;
|
|
2597
|
+
const fmEnd = close + "\n---\n".length;
|
|
2598
|
+
const fm = out.slice(0, fmEnd).replace(QUOTED_DATE_LINE_RE, "$1$2$3");
|
|
2599
|
+
return fm + out.slice(fmEnd);
|
|
2526
2600
|
}
|
|
2527
2601
|
|
|
2528
2602
|
// src/converter/pre-processors/properties-table.ts
|
|
@@ -2557,6 +2631,7 @@ var PropertiesTableInjector = class {
|
|
|
2557
2631
|
|
|
2558
2632
|
// src/converter/pre-processors/html-annotation.ts
|
|
2559
2633
|
var UNDERLINE_REGEX = /<u>([\s\S]*?)<\/u>/g;
|
|
2634
|
+
var HIGHLIGHT_REGEX = /(?<!=)==([^=\n]+?)==(?!=)/g;
|
|
2560
2635
|
var COLOR_SPAN_REGEX = /<span class="notion-(\w+)(-bg)?">([\s\S]*?)<\/span>/g;
|
|
2561
2636
|
var SPACED_COLOR_REGEX = new RegExp(
|
|
2562
2637
|
`%% ${MARKER_BRAND_RE}:color:(\\w+(?:_background)?) %%([\\s\\S]*?)%% ${MARKER_BRAND_RE}:end %%`,
|
|
@@ -2585,10 +2660,136 @@ var InlineAnnotationPreserver = class {
|
|
|
2585
2660
|
return `${compactMarker(`color:${color}`)}${text}%%/color%%`;
|
|
2586
2661
|
}
|
|
2587
2662
|
);
|
|
2663
|
+
content = mapOutsideCodeFences(
|
|
2664
|
+
content,
|
|
2665
|
+
(segment) => segment.replace(
|
|
2666
|
+
HIGHLIGHT_REGEX,
|
|
2667
|
+
(_m, text) => `${compactMarker("color:yellow_bg")}${text}%%/color%%`
|
|
2668
|
+
)
|
|
2669
|
+
);
|
|
2670
|
+
return { content, metadata: input.metadata };
|
|
2671
|
+
}
|
|
2672
|
+
};
|
|
2673
|
+
|
|
2674
|
+
// src/converter/pre-processors/comment-stripper.ts
|
|
2675
|
+
var COMMENT_REGEX = /%%([\s\S]*?)%%/g;
|
|
2676
|
+
var CommentStripper = class {
|
|
2677
|
+
name = "CommentStripper";
|
|
2678
|
+
// FrontmatterExtractor(10) 뒤 — YAML 값 속 `%%` 를 본문 주석으로 오인하지 않는다.
|
|
2679
|
+
order = 11;
|
|
2680
|
+
process(input) {
|
|
2681
|
+
if (input.context.direction !== "push") {
|
|
2682
|
+
return { content: input.content, metadata: input.metadata };
|
|
2683
|
+
}
|
|
2684
|
+
const markers = input.metadata.preserveMarkers ? [...input.metadata.preserveMarkers] : [];
|
|
2685
|
+
const content = mapOutsideCodeFences(
|
|
2686
|
+
input.content,
|
|
2687
|
+
(segment) => segment.replace(COMMENT_REGEX, (match, body, offset) => {
|
|
2688
|
+
const trimmed = body.trimStart();
|
|
2689
|
+
if (trimmed.startsWith(`${MARKER_BRAND}:`) || trimmed.startsWith("/")) {
|
|
2690
|
+
return match;
|
|
2691
|
+
}
|
|
2692
|
+
markers.push({
|
|
2693
|
+
type: "comment",
|
|
2694
|
+
params: { text: body, __anchor: computeAnchor(segment, offset) },
|
|
2695
|
+
startIndex: offset
|
|
2696
|
+
});
|
|
2697
|
+
return "";
|
|
2698
|
+
})
|
|
2699
|
+
);
|
|
2700
|
+
return {
|
|
2701
|
+
content,
|
|
2702
|
+
metadata: { ...input.metadata, preserveMarkers: markers }
|
|
2703
|
+
};
|
|
2704
|
+
}
|
|
2705
|
+
};
|
|
2706
|
+
|
|
2707
|
+
// src/converter/pre-processors/footnote-guard.ts
|
|
2708
|
+
var FOOTNOTE_TOKEN_REGEX = /(?<!\\)\[\^([^\]\s]+)\]/g;
|
|
2709
|
+
var FootnoteGuard = class {
|
|
2710
|
+
name = "FootnoteGuard";
|
|
2711
|
+
order = 12;
|
|
2712
|
+
process(input) {
|
|
2713
|
+
if (input.context.direction !== "push") {
|
|
2714
|
+
return { content: input.content, metadata: input.metadata };
|
|
2715
|
+
}
|
|
2716
|
+
const content = mapOutsideCodeFences(
|
|
2717
|
+
input.content,
|
|
2718
|
+
(segment) => segment.replace(FOOTNOTE_TOKEN_REGEX, "\\[^$1]")
|
|
2719
|
+
);
|
|
2588
2720
|
return { content, metadata: input.metadata };
|
|
2589
2721
|
}
|
|
2590
2722
|
};
|
|
2591
2723
|
|
|
2724
|
+
// src/converter/pre-processors/table-alignment.ts
|
|
2725
|
+
var DELIMITER_CELL_RE = /^:?-{1,}:?$/;
|
|
2726
|
+
function parseDelimiterRow(line) {
|
|
2727
|
+
const trimmed = line.trim();
|
|
2728
|
+
if (!trimmed.includes("-") || !trimmed.includes("|")) return null;
|
|
2729
|
+
const inner = trimmed.replace(/^\|/, "").replace(/\|$/, "");
|
|
2730
|
+
const cells = inner.split("|").map((c) => c.trim());
|
|
2731
|
+
if (cells.length === 0) return null;
|
|
2732
|
+
return cells.every((c) => DELIMITER_CELL_RE.test(c)) ? cells : null;
|
|
2733
|
+
}
|
|
2734
|
+
function cellAlignment(cell) {
|
|
2735
|
+
const l = cell.startsWith(":");
|
|
2736
|
+
const r = cell.endsWith(":");
|
|
2737
|
+
if (l && r) return "center";
|
|
2738
|
+
if (r) return "right";
|
|
2739
|
+
if (l) return "left";
|
|
2740
|
+
return "none";
|
|
2741
|
+
}
|
|
2742
|
+
function alignmentsToDelimiter(alignments) {
|
|
2743
|
+
const cells = alignments.map((a) => {
|
|
2744
|
+
switch (a) {
|
|
2745
|
+
case "center":
|
|
2746
|
+
return ":---:";
|
|
2747
|
+
case "right":
|
|
2748
|
+
return "---:";
|
|
2749
|
+
case "left":
|
|
2750
|
+
return ":---";
|
|
2751
|
+
default:
|
|
2752
|
+
return "---";
|
|
2753
|
+
}
|
|
2754
|
+
});
|
|
2755
|
+
return `| ${cells.join(" | ")} |`;
|
|
2756
|
+
}
|
|
2757
|
+
var TableAlignmentGuard = class {
|
|
2758
|
+
name = "TableAlignmentGuard";
|
|
2759
|
+
order = 45;
|
|
2760
|
+
process(input) {
|
|
2761
|
+
if (input.context.direction !== "push") {
|
|
2762
|
+
return { content: input.content, metadata: input.metadata };
|
|
2763
|
+
}
|
|
2764
|
+
const markers = input.metadata.preserveMarkers ? [...input.metadata.preserveMarkers] : [];
|
|
2765
|
+
const lines = input.content.split("\n");
|
|
2766
|
+
let changed = false;
|
|
2767
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2768
|
+
const cells = parseDelimiterRow(lines[i]);
|
|
2769
|
+
if (!cells || !lines[i].includes(":")) continue;
|
|
2770
|
+
const prev = lines[i - 1];
|
|
2771
|
+
if (!prev.includes("|")) continue;
|
|
2772
|
+
markers.push({
|
|
2773
|
+
type: "table-align",
|
|
2774
|
+
params: {
|
|
2775
|
+
align: cells.map(cellAlignment).join(","),
|
|
2776
|
+
__anchor: prev.trim().slice(0, 64),
|
|
2777
|
+
// 원본 구분행 원문. 복원 시 정렬뿐 아니라 셀 폭 패딩까지 그대로 되살려
|
|
2778
|
+
// 라운드트립 delta-0 을 만족시킨다(__ 접두라 마커 렌더링에서 제외).
|
|
2779
|
+
__line: lines[i]
|
|
2780
|
+
},
|
|
2781
|
+
startIndex: 0
|
|
2782
|
+
});
|
|
2783
|
+
lines[i] = alignmentsToDelimiter(cells.map(() => "none"));
|
|
2784
|
+
changed = true;
|
|
2785
|
+
}
|
|
2786
|
+
return {
|
|
2787
|
+
content: changed ? lines.join("\n") : input.content,
|
|
2788
|
+
metadata: { ...input.metadata, preserveMarkers: markers }
|
|
2789
|
+
};
|
|
2790
|
+
}
|
|
2791
|
+
};
|
|
2792
|
+
|
|
2592
2793
|
// src/converter/link-restore.ts
|
|
2593
2794
|
var NOTION_LINK_REGEX = /\[([^\]]+)\]\((?:https:\/\/(?:www\.)?notion\.so\/|notion:\/\/)([a-f0-9-]+)\)/g;
|
|
2594
2795
|
var WIKILINK_PRESERVE_REGEX = new RegExp(`\\[([^\\]]+)\\]\\(${WIKILINK_PROTOCOL}([^)]+)\\)`, "g");
|
|
@@ -2620,6 +2821,91 @@ var MentionToWikilink = class {
|
|
|
2620
2821
|
}
|
|
2621
2822
|
};
|
|
2622
2823
|
|
|
2824
|
+
// src/converter/post-processors/escape-normalizer.ts
|
|
2825
|
+
var EscapeNormalizer = class {
|
|
2826
|
+
name = "EscapeNormalizer";
|
|
2827
|
+
order = 3;
|
|
2828
|
+
process(input) {
|
|
2829
|
+
if (input.context.direction !== "pull") {
|
|
2830
|
+
return { content: input.content, metadata: input.metadata };
|
|
2831
|
+
}
|
|
2832
|
+
const content = mapOutsideCodeFences(
|
|
2833
|
+
input.content,
|
|
2834
|
+
(segment) => segment.replace(/\\\[\^([^\]\\\n]+)\\\]/g, "[^$1]").replace(/\\\[\^/g, "[^").replace(/\\:/g, ":")
|
|
2835
|
+
);
|
|
2836
|
+
return { content, metadata: input.metadata };
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2839
|
+
|
|
2840
|
+
// src/converter/post-processors/highlight-restorer.ts
|
|
2841
|
+
var COMPACT_YELLOW_RE = new RegExp(
|
|
2842
|
+
`%%${MARKER_BRAND_RE}:color:yellow(?:_background|_bg)%%([^\\n]*?)%%\\/color%%`,
|
|
2843
|
+
"g"
|
|
2844
|
+
);
|
|
2845
|
+
var SPACED_YELLOW_RE = new RegExp(
|
|
2846
|
+
`%% ${MARKER_BRAND_RE}:color:yellow(?:_background|_bg) %%([^\\n]*?)%% ${MARKER_BRAND_RE}:end %%`,
|
|
2847
|
+
"g"
|
|
2848
|
+
);
|
|
2849
|
+
var HighlightRestorer = class {
|
|
2850
|
+
name = "HighlightRestorer";
|
|
2851
|
+
order = 25;
|
|
2852
|
+
process(input) {
|
|
2853
|
+
if (input.context.direction !== "pull") {
|
|
2854
|
+
return { content: input.content, metadata: input.metadata };
|
|
2855
|
+
}
|
|
2856
|
+
const content = input.content.replace(COMPACT_YELLOW_RE, (_m, text) => text ? `==${text}==` : "").replace(SPACED_YELLOW_RE, (_m, text) => text ? `==${text}==` : "");
|
|
2857
|
+
return { content, metadata: input.metadata };
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
|
|
2861
|
+
// src/converter/post-processors/table-alignment-restorer.ts
|
|
2862
|
+
var TableAlignmentRestorer = class {
|
|
2863
|
+
name = "TableAlignmentRestorer";
|
|
2864
|
+
order = 35;
|
|
2865
|
+
process(input) {
|
|
2866
|
+
if (input.context.direction !== "pull") {
|
|
2867
|
+
return { content: input.content, metadata: input.metadata };
|
|
2868
|
+
}
|
|
2869
|
+
const lines = input.content.split("\n");
|
|
2870
|
+
for (let i = 1; i < lines.length - 1; i++) {
|
|
2871
|
+
const delim = parseDelimiterRow(lines[i]);
|
|
2872
|
+
if (!delim || lines[i].includes(":")) continue;
|
|
2873
|
+
const phantom = parseDelimiterRow(lines[i + 1]);
|
|
2874
|
+
if (!phantom || !lines[i + 1].includes(":") || phantom.length !== delim.length) continue;
|
|
2875
|
+
if (!lines[i - 1].includes("|")) continue;
|
|
2876
|
+
lines[i] = alignmentsToDelimiter(phantom.map(cellAlignment));
|
|
2877
|
+
lines.splice(i + 1, 1);
|
|
2878
|
+
}
|
|
2879
|
+
const markers = input.metadata.preserveMarkers ?? [];
|
|
2880
|
+
const remaining = [];
|
|
2881
|
+
for (const marker of markers) {
|
|
2882
|
+
if (marker.type !== "table-align" || !marker.params.align || !marker.params.__anchor) {
|
|
2883
|
+
remaining.push(marker);
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
const anchor = marker.params.__anchor;
|
|
2887
|
+
const idx = lines.findIndex(
|
|
2888
|
+
(line, i) => i < lines.length - 1 && line.trim().startsWith(anchor) && parseDelimiterRow(lines[i + 1]) !== null
|
|
2889
|
+
);
|
|
2890
|
+
if (idx === -1) continue;
|
|
2891
|
+
const delimCells = parseDelimiterRow(lines[idx + 1]);
|
|
2892
|
+
if (lines[idx + 1].includes(":")) continue;
|
|
2893
|
+
const originalLine = marker.params.__line;
|
|
2894
|
+
if (originalLine && parseDelimiterRow(originalLine)?.length === delimCells.length) {
|
|
2895
|
+
lines[idx + 1] = originalLine;
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
const alignments = marker.params.align.split(",");
|
|
2899
|
+
while (alignments.length < delimCells.length) alignments.push("none");
|
|
2900
|
+
lines[idx + 1] = alignmentsToDelimiter(alignments.slice(0, delimCells.length));
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
content: lines.join("\n"),
|
|
2904
|
+
metadata: { ...input.metadata, preserveMarkers: remaining }
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
};
|
|
2908
|
+
|
|
2623
2909
|
// src/converter/post-processors/preserve-marker-injector.ts
|
|
2624
2910
|
var PreserveMarkerInjector = class {
|
|
2625
2911
|
name = "PreserveMarkerInjector";
|
|
@@ -2633,23 +2919,89 @@ var PreserveMarkerInjector = class {
|
|
|
2633
2919
|
return { content: input.content, metadata: input.metadata };
|
|
2634
2920
|
}
|
|
2635
2921
|
let content = input.content;
|
|
2636
|
-
const
|
|
2637
|
-
|
|
2922
|
+
const missing = markers.filter((m) => {
|
|
2923
|
+
const rendered = this.renderMarker(m);
|
|
2924
|
+
if (rendered === null) return false;
|
|
2925
|
+
if (content.includes(rendered)) return false;
|
|
2926
|
+
return !this.isSemanticallyRestored(m, content);
|
|
2927
|
+
});
|
|
2928
|
+
if (missing.length === 0) {
|
|
2638
2929
|
return { content, metadata: input.metadata };
|
|
2639
2930
|
}
|
|
2640
|
-
const sorted = [...
|
|
2641
|
-
for (const
|
|
2642
|
-
const
|
|
2643
|
-
|
|
2644
|
-
|
|
2931
|
+
const sorted = [...missing].sort((a, b) => a.startIndex - b.startIndex);
|
|
2932
|
+
for (const marker of sorted) {
|
|
2933
|
+
const rendered = this.renderMarker(marker);
|
|
2934
|
+
if (marker.params?.__anchor === void 0) {
|
|
2935
|
+
const at2 = Math.max(frontmatterEnd(content), Math.min(marker.startIndex, content.length));
|
|
2936
|
+
content = content.slice(0, at2) + rendered + content.slice(at2);
|
|
2937
|
+
continue;
|
|
2938
|
+
}
|
|
2939
|
+
const at = this.findInsertionPoint(marker, content);
|
|
2940
|
+
const before = content.slice(0, at);
|
|
2941
|
+
const needsNewline = before.length > 0 && !before.endsWith("\n");
|
|
2942
|
+
content = before + (needsNewline ? "\n" : "") + rendered + "\n" + content.slice(at);
|
|
2645
2943
|
}
|
|
2646
2944
|
return { content, metadata: input.metadata };
|
|
2647
2945
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2946
|
+
/** 마커의 본문 표현. 복원 불가능한(빈) 마커는 null — 삽입하지 않고 폐기한다. */
|
|
2947
|
+
renderMarker(marker) {
|
|
2948
|
+
const params = marker.params ?? {};
|
|
2949
|
+
if (marker.type === "comment") {
|
|
2950
|
+
return params.text ? `%%${params.text}%%` : null;
|
|
2951
|
+
}
|
|
2952
|
+
if (params.__raw !== void 0) {
|
|
2953
|
+
return spacedMarker(`${marker.type}:${params.__raw}`);
|
|
2954
|
+
}
|
|
2955
|
+
const entries = Object.entries(params).filter(([key]) => !key.startsWith("__"));
|
|
2956
|
+
if (entries.length === 0) return null;
|
|
2957
|
+
const body = entries.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
2958
|
+
return spacedMarker(`${marker.type}:${body}`);
|
|
2959
|
+
}
|
|
2960
|
+
/** 마커가 이미 원래 문법으로 복원돼 있으면 재삽입은 중복·오염이다. */
|
|
2961
|
+
isSemanticallyRestored(marker, content) {
|
|
2962
|
+
const params = marker.params ?? {};
|
|
2963
|
+
switch (marker.type) {
|
|
2964
|
+
case "wikilink": {
|
|
2965
|
+
const text = params.text;
|
|
2966
|
+
if (!text) return false;
|
|
2967
|
+
return content.includes(`[[${text}]]`) || content.includes(`[[${text}|`);
|
|
2968
|
+
}
|
|
2969
|
+
case "local-image":
|
|
2970
|
+
case "local-file": {
|
|
2971
|
+
const target = params.__raw ?? params.path;
|
|
2972
|
+
return target ? content.includes(`![[${target}]]`) : false;
|
|
2973
|
+
}
|
|
2974
|
+
case "table-align":
|
|
2975
|
+
return true;
|
|
2976
|
+
default:
|
|
2977
|
+
return false;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
/** 삽입 위치: 앵커 → startIndex(줄 경계 보정) 순. 항상 프론트매터 뒤. */
|
|
2981
|
+
findInsertionPoint(marker, content) {
|
|
2982
|
+
const fmEnd = frontmatterEnd(content);
|
|
2983
|
+
const anchor = marker.params?.__anchor;
|
|
2984
|
+
if (anchor && anchor.trim().length >= 4) {
|
|
2985
|
+
const idx = content.indexOf(anchor, fmEnd);
|
|
2986
|
+
if (idx !== -1) {
|
|
2987
|
+
return nextLineStart(content, idx + anchor.length);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
const clamped = Math.max(fmEnd, Math.min(marker.startIndex, content.length));
|
|
2991
|
+
return nextLineStart(content, clamped);
|
|
2651
2992
|
}
|
|
2652
2993
|
};
|
|
2994
|
+
function frontmatterEnd(content) {
|
|
2995
|
+
if (!content.startsWith("---\n")) return 0;
|
|
2996
|
+
const close = content.indexOf("\n---", 3);
|
|
2997
|
+
if (close === -1) return 0;
|
|
2998
|
+
const lineEnd = content.indexOf("\n", close + 4);
|
|
2999
|
+
return lineEnd === -1 ? content.length : lineEnd + 1;
|
|
3000
|
+
}
|
|
3001
|
+
function nextLineStart(content, pos) {
|
|
3002
|
+
const nl = content.indexOf("\n", pos);
|
|
3003
|
+
return nl === -1 ? content.length : nl + 1;
|
|
3004
|
+
}
|
|
2653
3005
|
|
|
2654
3006
|
// src/converter/post-processors/callout-restorer.ts
|
|
2655
3007
|
var EMOJI_TYPE_MAP = {
|
|
@@ -2785,7 +3137,7 @@ function normalizeDate(dateStr) {
|
|
|
2785
3137
|
|
|
2786
3138
|
// src/converter/post-processors/local-image-restorer.ts
|
|
2787
3139
|
var LOCAL_IMAGE_MARKER_REGEX = new RegExp(
|
|
2788
|
-
`>\\s*\u{1F4CE}\\s*[^\\n]*\\n>\\s*%%\\s*${MARKER_BRAND_RE}:local-image:([^\\s]+)\\s*%%`,
|
|
3140
|
+
`>\\s*\u{1F4CE}\\s*[^\\n]*\\n>\\s*%%\\s*${MARKER_BRAND_RE}:local-(?:image|file):([^\\s]+)\\s*%%`,
|
|
2789
3141
|
"g"
|
|
2790
3142
|
);
|
|
2791
3143
|
var LocalImageRestorer = class {
|
|
@@ -2897,25 +3249,178 @@ function stripQuotes(s) {
|
|
|
2897
3249
|
return s;
|
|
2898
3250
|
}
|
|
2899
3251
|
|
|
3252
|
+
// src/converter/post-processors/block-spacer.ts
|
|
3253
|
+
var BlockSpacer = class {
|
|
3254
|
+
name = "BlockSpacer";
|
|
3255
|
+
order = 110;
|
|
3256
|
+
process(input) {
|
|
3257
|
+
if (input.context.direction !== "pull") {
|
|
3258
|
+
return { content: input.content, metadata: input.metadata };
|
|
3259
|
+
}
|
|
3260
|
+
return {
|
|
3261
|
+
content: respace(input.content, input.metadata.notionExportCompact),
|
|
3262
|
+
metadata: input.metadata
|
|
3263
|
+
};
|
|
3264
|
+
}
|
|
3265
|
+
};
|
|
3266
|
+
var LIST_ITEM_RE = /^[ \t]*(?:[-*+]|\d+[.)])\s/;
|
|
3267
|
+
var QUOTE_RE = /^[ \t]*>/;
|
|
3268
|
+
var CALLOUT_START_RE = /^[ \t]*>\s*\[![^\]]*\]/;
|
|
3269
|
+
var TABLE_ROW_RE = /^[ \t]*\|/;
|
|
3270
|
+
var FOOTNOTE_DEF_RE = /^\[\^[^\]]+\]:/;
|
|
3271
|
+
var FENCE_RE = /^[ \t]*(```+|~~~+)/;
|
|
3272
|
+
var MATH_FENCE_RE = /^[ \t]*\$\$\s*$/;
|
|
3273
|
+
var BRAND_MARKER_LINE_RE = new RegExp(`^[ \\t]*%%\\s*${MARKER_BRAND}:`);
|
|
3274
|
+
var INDENTED_CONTINUATION_RE = /^(?:\t| {2,})\S/;
|
|
3275
|
+
function respace(content, sourceCompact) {
|
|
3276
|
+
if (sourceCompact === false) return content;
|
|
3277
|
+
const lines = content.split("\n");
|
|
3278
|
+
const head = [];
|
|
3279
|
+
let start = 0;
|
|
3280
|
+
if (lines[0]?.trim() === "---") {
|
|
3281
|
+
for (let i = 1; i < lines.length; i++) {
|
|
3282
|
+
if (lines[i].trim() === "---") {
|
|
3283
|
+
for (let j = 0; j <= i; j++) head.push(lines[j]);
|
|
3284
|
+
start = i + 1;
|
|
3285
|
+
break;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
if (sourceCompact === void 0 && hasBlankOutsideFences(lines, start)) return content;
|
|
3290
|
+
const blocks = [];
|
|
3291
|
+
let fence = null;
|
|
3292
|
+
let fenceToken = "";
|
|
3293
|
+
let boundary = false;
|
|
3294
|
+
const push = (kind, line) => {
|
|
3295
|
+
blocks.push({ kind, lines: [line] });
|
|
3296
|
+
};
|
|
3297
|
+
const appendToLast = (line) => {
|
|
3298
|
+
blocks[blocks.length - 1].lines.push(line);
|
|
3299
|
+
};
|
|
3300
|
+
const last = () => blocks[blocks.length - 1];
|
|
3301
|
+
for (let i = start; i < lines.length; i++) {
|
|
3302
|
+
const raw = lines[i];
|
|
3303
|
+
if (fence !== null) {
|
|
3304
|
+
appendToLast(raw);
|
|
3305
|
+
const closed = fence === "code" ? FENCE_RE.test(raw) && raw.trim().startsWith(fenceToken) : MATH_FENCE_RE.test(raw);
|
|
3306
|
+
if (closed) fence = null;
|
|
3307
|
+
continue;
|
|
3308
|
+
}
|
|
3309
|
+
if (raw.trim() === "") {
|
|
3310
|
+
boundary = true;
|
|
3311
|
+
continue;
|
|
3312
|
+
}
|
|
3313
|
+
const atBoundary = boundary;
|
|
3314
|
+
boundary = false;
|
|
3315
|
+
const fenceMatch = raw.match(FENCE_RE);
|
|
3316
|
+
if (fenceMatch) {
|
|
3317
|
+
push("fence", raw);
|
|
3318
|
+
fence = "code";
|
|
3319
|
+
fenceToken = fenceMatch[1][0].repeat(3);
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3322
|
+
if (MATH_FENCE_RE.test(raw)) {
|
|
3323
|
+
push("math", raw);
|
|
3324
|
+
fence = "math";
|
|
3325
|
+
continue;
|
|
3326
|
+
}
|
|
3327
|
+
const line = LIST_ITEM_RE.test(raw) || INDENTED_CONTINUATION_RE.test(raw) ? raw.replace(/^\t+/, (t) => " ".repeat(t.length)) : raw;
|
|
3328
|
+
if (BRAND_MARKER_LINE_RE.test(line) && blocks.length > 0) {
|
|
3329
|
+
appendToLast(line);
|
|
3330
|
+
continue;
|
|
3331
|
+
}
|
|
3332
|
+
if (LIST_ITEM_RE.test(line)) {
|
|
3333
|
+
if (last()?.kind === "list" && !atBoundary) appendToLast(line);
|
|
3334
|
+
else push("list", line);
|
|
3335
|
+
continue;
|
|
3336
|
+
}
|
|
3337
|
+
if (last()?.kind === "list" && INDENTED_CONTINUATION_RE.test(line)) {
|
|
3338
|
+
appendToLast(line);
|
|
3339
|
+
continue;
|
|
3340
|
+
}
|
|
3341
|
+
if (QUOTE_RE.test(line)) {
|
|
3342
|
+
if (last()?.kind === "quote" && !CALLOUT_START_RE.test(line) && !atBoundary) {
|
|
3343
|
+
appendToLast(line);
|
|
3344
|
+
} else {
|
|
3345
|
+
push("quote", line);
|
|
3346
|
+
}
|
|
3347
|
+
continue;
|
|
3348
|
+
}
|
|
3349
|
+
if (TABLE_ROW_RE.test(line)) {
|
|
3350
|
+
if (last()?.kind === "table" && !atBoundary) appendToLast(line);
|
|
3351
|
+
else push("table", line);
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
if (FOOTNOTE_DEF_RE.test(line)) {
|
|
3355
|
+
if (last()?.kind === "footnote" && !atBoundary) appendToLast(line);
|
|
3356
|
+
else push("footnote", line);
|
|
3357
|
+
continue;
|
|
3358
|
+
}
|
|
3359
|
+
push("other", line);
|
|
3360
|
+
}
|
|
3361
|
+
const body = blocks.map((b) => b.lines.join("\n")).join("\n\n");
|
|
3362
|
+
const tail = content.endsWith("\n") ? "\n" : "";
|
|
3363
|
+
if (head.length === 0) return body + tail;
|
|
3364
|
+
return `${head.join("\n")}
|
|
3365
|
+
|
|
3366
|
+
${body}${tail}`;
|
|
3367
|
+
}
|
|
3368
|
+
function isCompactExport(rawMarkdown) {
|
|
3369
|
+
return !hasBlankOutsideFences(rawMarkdown.split("\n"), 0);
|
|
3370
|
+
}
|
|
3371
|
+
function hasBlankOutsideFences(lines, start) {
|
|
3372
|
+
let end = lines.length - 1;
|
|
3373
|
+
while (end >= start && lines[end].trim() === "") end--;
|
|
3374
|
+
let fence = null;
|
|
3375
|
+
let fenceToken = "";
|
|
3376
|
+
for (let i = start; i <= end; i++) {
|
|
3377
|
+
const raw = lines[i];
|
|
3378
|
+
if (fence !== null) {
|
|
3379
|
+
const closed = fence === "code" ? FENCE_RE.test(raw) && raw.trim().startsWith(fenceToken) : MATH_FENCE_RE.test(raw);
|
|
3380
|
+
if (closed) fence = null;
|
|
3381
|
+
continue;
|
|
3382
|
+
}
|
|
3383
|
+
const fenceMatch = raw.match(FENCE_RE);
|
|
3384
|
+
if (fenceMatch) {
|
|
3385
|
+
fence = "code";
|
|
3386
|
+
fenceToken = fenceMatch[1][0].repeat(3);
|
|
3387
|
+
continue;
|
|
3388
|
+
}
|
|
3389
|
+
if (MATH_FENCE_RE.test(raw)) {
|
|
3390
|
+
fence = "math";
|
|
3391
|
+
continue;
|
|
3392
|
+
}
|
|
3393
|
+
if (raw.trim() === "") return true;
|
|
3394
|
+
}
|
|
3395
|
+
return false;
|
|
3396
|
+
}
|
|
3397
|
+
|
|
2900
3398
|
// src/converter/pipeline-factory.ts
|
|
2901
3399
|
function createDefaultPipeline(options) {
|
|
2902
3400
|
const pipeline = new ConversionPipeline();
|
|
2903
3401
|
pipeline.registerPreProcessor(new InlineAnnotationPreserver());
|
|
2904
3402
|
pipeline.registerPreProcessor(new UnsupportedBlockStripper());
|
|
3403
|
+
pipeline.registerPreProcessor(new CommentStripper());
|
|
2905
3404
|
pipeline.registerPreProcessor(new FrontmatterExtractor());
|
|
3405
|
+
pipeline.registerPreProcessor(new FootnoteGuard());
|
|
2906
3406
|
pipeline.registerPreProcessor(new PropertiesTableInjector());
|
|
2907
3407
|
pipeline.registerPreProcessor(new WikilinkResolver(options?.wikilinkResolver));
|
|
2908
3408
|
pipeline.registerPreProcessor(new CalloutTransformer());
|
|
3409
|
+
pipeline.registerPreProcessor(new TableAlignmentGuard());
|
|
2909
3410
|
pipeline.registerPreProcessor(new MathNormalizer());
|
|
2910
3411
|
pipeline.registerPreProcessor(new EmbedResolver());
|
|
2911
3412
|
pipeline.registerPreProcessor(new PreserveMarkerCollector());
|
|
3413
|
+
pipeline.registerPostProcessor(new EscapeNormalizer());
|
|
2912
3414
|
pipeline.registerPostProcessor(new PropertiesTableRestorer());
|
|
2913
3415
|
pipeline.registerPostProcessor(new LocalImageRestorer());
|
|
3416
|
+
pipeline.registerPostProcessor(new HighlightRestorer());
|
|
3417
|
+
pipeline.registerPostProcessor(new TableAlignmentRestorer());
|
|
2914
3418
|
pipeline.registerPostProcessor(new PreserveMarkerInjector());
|
|
2915
3419
|
pipeline.registerPostProcessor(new MentionToWikilink());
|
|
2916
3420
|
pipeline.registerPostProcessor(new CalloutRestorer());
|
|
2917
3421
|
pipeline.registerPostProcessor(new ColorAnnotator());
|
|
2918
3422
|
pipeline.registerPostProcessor(new FrontmatterGenerator());
|
|
3423
|
+
pipeline.registerPostProcessor(new BlockSpacer());
|
|
2919
3424
|
return pipeline;
|
|
2920
3425
|
}
|
|
2921
3426
|
|
|
@@ -3728,6 +4233,87 @@ var EXTENSION_TO_MIME = {
|
|
|
3728
4233
|
".ico": "image/x-icon",
|
|
3729
4234
|
".tiff": "image/tiff"
|
|
3730
4235
|
};
|
|
4236
|
+
function scanMarkdownLinks(markdown) {
|
|
4237
|
+
const spans = [];
|
|
4238
|
+
let i = 0;
|
|
4239
|
+
while (i < markdown.length) {
|
|
4240
|
+
const open = markdown.indexOf("[", i);
|
|
4241
|
+
if (open === -1) break;
|
|
4242
|
+
if (markdown[open - 1] === "\\") {
|
|
4243
|
+
i = open + 1;
|
|
4244
|
+
continue;
|
|
4245
|
+
}
|
|
4246
|
+
const isEmbed = markdown[open - 1] === "!" && markdown[open - 2] !== "\\";
|
|
4247
|
+
let depth = 1;
|
|
4248
|
+
let j = open + 1;
|
|
4249
|
+
let broken = false;
|
|
4250
|
+
while (j < markdown.length && depth > 0) {
|
|
4251
|
+
const ch = markdown[j];
|
|
4252
|
+
if (ch === "\\") {
|
|
4253
|
+
j += 2;
|
|
4254
|
+
continue;
|
|
4255
|
+
}
|
|
4256
|
+
if (ch === "\n" && markdown[j + 1] === "\n") {
|
|
4257
|
+
broken = true;
|
|
4258
|
+
break;
|
|
4259
|
+
}
|
|
4260
|
+
if (ch === "[") depth++;
|
|
4261
|
+
else if (ch === "]") depth--;
|
|
4262
|
+
j++;
|
|
4263
|
+
}
|
|
4264
|
+
if (broken || depth !== 0 || markdown[j] !== "(") {
|
|
4265
|
+
i = open + 1;
|
|
4266
|
+
continue;
|
|
4267
|
+
}
|
|
4268
|
+
const label = markdown.slice(open + 1, j - 1);
|
|
4269
|
+
let parenDepth = 1;
|
|
4270
|
+
let k = j + 1;
|
|
4271
|
+
while (k < markdown.length && parenDepth > 0) {
|
|
4272
|
+
const ch = markdown[k];
|
|
4273
|
+
if (ch === "\\") {
|
|
4274
|
+
k += 2;
|
|
4275
|
+
continue;
|
|
4276
|
+
}
|
|
4277
|
+
if (ch === "\n") {
|
|
4278
|
+
broken = true;
|
|
4279
|
+
break;
|
|
4280
|
+
}
|
|
4281
|
+
if (ch === "(") parenDepth++;
|
|
4282
|
+
else if (ch === ")") parenDepth--;
|
|
4283
|
+
k++;
|
|
4284
|
+
}
|
|
4285
|
+
if (broken || parenDepth !== 0) {
|
|
4286
|
+
i = open + 1;
|
|
4287
|
+
continue;
|
|
4288
|
+
}
|
|
4289
|
+
const start = isEmbed ? open - 1 : open;
|
|
4290
|
+
spans.push({
|
|
4291
|
+
full: markdown.slice(start, k),
|
|
4292
|
+
label,
|
|
4293
|
+
url: markdown.slice(j + 1, k - 1),
|
|
4294
|
+
isEmbed
|
|
4295
|
+
});
|
|
4296
|
+
i = k;
|
|
4297
|
+
}
|
|
4298
|
+
return spans;
|
|
4299
|
+
}
|
|
4300
|
+
function toWikilinkAlias(label) {
|
|
4301
|
+
return label.replace(/!?\[([^\]]*)\]\(([^)]*)\)/g, "$1").replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_m, t, d) => d ?? t).replace(/[\r\n]+/g, " ").replace(/\]\]/g, ")").replace(/\|/g, "-").trim();
|
|
4302
|
+
}
|
|
4303
|
+
var INTERNAL_FILE_URL_RE = /^file:\/\/%7B.*%7D%7D$/;
|
|
4304
|
+
var FILE_LABEL_PREFIX_RE = /^(?:📎|🎬|🎞|📄|🔊)\s*/u;
|
|
4305
|
+
var LOCAL_MARKER_RE = new RegExp(`${MARKER_BRAND_RE}:local-(?:image|file):([^\\s%]+)`, "g");
|
|
4306
|
+
function collectLocalMarkerBasenames(markdown) {
|
|
4307
|
+
const names = /* @__PURE__ */ new Set();
|
|
4308
|
+
const unescaped = markdown.replace(/\\/g, "");
|
|
4309
|
+
for (const m of unescaped.matchAll(LOCAL_MARKER_RE)) {
|
|
4310
|
+
names.add(baseName(m[1]));
|
|
4311
|
+
}
|
|
4312
|
+
return names;
|
|
4313
|
+
}
|
|
4314
|
+
function baseName(path) {
|
|
4315
|
+
return path.split("/").pop() ?? path;
|
|
4316
|
+
}
|
|
3731
4317
|
var ImageHandler = class {
|
|
3732
4318
|
constructor(vaultFs, attachmentFolder = "attachments", notionClient, customFetch, options, stateDb) {
|
|
3733
4319
|
this.vaultFs = vaultFs;
|
|
@@ -3753,6 +4339,9 @@ var ImageHandler = class {
|
|
|
3753
4339
|
return this.customFetch ?? globalThis.fetch;
|
|
3754
4340
|
}
|
|
3755
4341
|
async downloadImage(url, pageTitle) {
|
|
4342
|
+
return this.persistImage(await this.fetchImage(url, pageTitle));
|
|
4343
|
+
}
|
|
4344
|
+
async fetchImage(url, pageTitle) {
|
|
3756
4345
|
const maxRetries = this.maxRetries;
|
|
3757
4346
|
let lastError = null;
|
|
3758
4347
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
@@ -3770,38 +4359,60 @@ var ImageHandler = class {
|
|
|
3770
4359
|
const contentType = response.headers.get("content-type") ?? "image/png";
|
|
3771
4360
|
const extension = IMAGE_EXTENSIONS[contentType] ?? this.guessExtension(url) ?? ".png";
|
|
3772
4361
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3773
|
-
const
|
|
4362
|
+
const fullHash = createHash("sha256").update(buffer).digest("hex");
|
|
4363
|
+
const hash = fullHash.slice(0, 12);
|
|
3774
4364
|
const safeName = pageTitle.replace(/[^a-zA-Z0-9가-힣\s-]/g, "").replace(/\s+/g, "-").slice(0, 50);
|
|
3775
4365
|
const fileName = `${safeName}-${hash}${extension}`;
|
|
3776
4366
|
const localPath = `${this.attachmentFolder}/${fileName}`;
|
|
3777
|
-
|
|
3778
|
-
await this.vaultFs.writeBinary(localPath, buffer);
|
|
3779
|
-
return { originalUrl: url, localPath, hash, size: buffer.length };
|
|
4367
|
+
return { originalUrl: url, localPath, hash, fullHash, buffer };
|
|
3780
4368
|
} catch (error) {
|
|
3781
4369
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
3782
4370
|
}
|
|
3783
4371
|
}
|
|
3784
4372
|
throw lastError;
|
|
3785
4373
|
}
|
|
3786
|
-
async
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
4374
|
+
async persistImage(fetched) {
|
|
4375
|
+
await this.vaultFs.ensureFolder(this.attachmentFolder);
|
|
4376
|
+
await this.vaultFs.writeBinary(fetched.localPath, fetched.buffer);
|
|
4377
|
+
return {
|
|
4378
|
+
originalUrl: fetched.originalUrl,
|
|
4379
|
+
localPath: fetched.localPath,
|
|
4380
|
+
hash: fetched.hash,
|
|
4381
|
+
size: fetched.buffer.length
|
|
4382
|
+
};
|
|
4383
|
+
}
|
|
4384
|
+
async downloadAllImages(markdown, pageTitle, pageId) {
|
|
4385
|
+
const embeds = scanMarkdownLinks(markdown).filter((s) => s.isEmbed);
|
|
4386
|
+
const notionMatches = embeds.filter(
|
|
4387
|
+
(s) => /^https?:\/\//.test(s.url) && this.isNotionImageUrl(s.url)
|
|
4388
|
+
);
|
|
4389
|
+
const internalMatches = embeds.filter((s) => INTERNAL_FILE_URL_RE.test(s.url));
|
|
3792
4390
|
if (notionMatches.length === 0 && internalMatches.length === 0) {
|
|
3793
4391
|
return { content: markdown, downloads: [] };
|
|
3794
4392
|
}
|
|
4393
|
+
const registryByHash = /* @__PURE__ */ new Map();
|
|
4394
|
+
if (pageId && this.stateDb) {
|
|
4395
|
+
for (const entry of this.stateDb.getFilesByPageId(pageId)) {
|
|
4396
|
+
const list = registryByHash.get(entry.fileHash) ?? [];
|
|
4397
|
+
list.push(entry.localPath);
|
|
4398
|
+
registryByHash.set(entry.fileHash, list);
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
const markerBasenames = collectLocalMarkerBasenames(markdown);
|
|
4402
|
+
const isAppendedCopy = (fetched) => {
|
|
4403
|
+
const paths = registryByHash.get(fetched.fullHash);
|
|
4404
|
+
return paths !== void 0 && paths.some((p) => markerBasenames.has(baseName(p)));
|
|
4405
|
+
};
|
|
3795
4406
|
const sema = new Sema(this.concurrency);
|
|
3796
4407
|
const downloads = [];
|
|
3797
4408
|
let result = markdown;
|
|
3798
4409
|
const httpResults = await Promise.allSettled(
|
|
3799
|
-
notionMatches.map(async (
|
|
4410
|
+
notionMatches.map(async (span) => {
|
|
3800
4411
|
await sema.acquire();
|
|
3801
4412
|
try {
|
|
3802
4413
|
return {
|
|
3803
|
-
|
|
3804
|
-
|
|
4414
|
+
span,
|
|
4415
|
+
fetched: await this.fetchImage(span.url, pageTitle)
|
|
3805
4416
|
};
|
|
3806
4417
|
} finally {
|
|
3807
4418
|
sema.release();
|
|
@@ -3810,26 +4421,37 @@ var ImageHandler = class {
|
|
|
3810
4421
|
);
|
|
3811
4422
|
for (const settled of httpResults) {
|
|
3812
4423
|
if (settled.status !== "fulfilled") continue;
|
|
3813
|
-
const {
|
|
4424
|
+
const { span, fetched } = settled.value;
|
|
4425
|
+
if (isAppendedCopy(fetched)) {
|
|
4426
|
+
result = result.replace(span.full, "");
|
|
4427
|
+
continue;
|
|
4428
|
+
}
|
|
4429
|
+
const download = await this.persistImage(fetched);
|
|
3814
4430
|
downloads.push(download);
|
|
3815
|
-
const
|
|
3816
|
-
|
|
4431
|
+
const alias = toWikilinkAlias(span.label);
|
|
4432
|
+
const obsidianEmbed = `![[${download.localPath}${alias ? `|${alias}` : ""}]]`;
|
|
4433
|
+
result = result.replace(span.full, obsidianEmbed);
|
|
3817
4434
|
}
|
|
3818
|
-
for (const
|
|
4435
|
+
for (const span of internalMatches) {
|
|
3819
4436
|
await sema.acquire();
|
|
3820
4437
|
try {
|
|
3821
|
-
const
|
|
3822
|
-
const parsed = this.parseNotionFileUrl(fileUrl);
|
|
4438
|
+
const parsed = this.parseNotionFileUrl(span.url);
|
|
3823
4439
|
if (!parsed || !this.notionClient) continue;
|
|
3824
4440
|
const realUrl = await this.notionClient.getFileBlockUrl(parsed.blockId);
|
|
3825
4441
|
if (!realUrl) {
|
|
3826
4442
|
getLogger().warn(`[Im-Nobsidian] \uC774\uBBF8\uC9C0 Block URL \uC870\uD68C \uC2E4\uD328: ${parsed.fileName}`);
|
|
3827
4443
|
continue;
|
|
3828
4444
|
}
|
|
3829
|
-
const
|
|
4445
|
+
const fetched = await this.fetchImage(realUrl, pageTitle);
|
|
4446
|
+
if (isAppendedCopy(fetched)) {
|
|
4447
|
+
result = result.replace(span.full, "");
|
|
4448
|
+
continue;
|
|
4449
|
+
}
|
|
4450
|
+
const download = await this.persistImage(fetched);
|
|
3830
4451
|
downloads.push(download);
|
|
3831
|
-
const
|
|
3832
|
-
|
|
4452
|
+
const alias = toWikilinkAlias(span.label);
|
|
4453
|
+
const obsidianEmbed = `![[${download.localPath}${alias ? `|${alias}` : ""}]]`;
|
|
4454
|
+
result = result.replace(span.full, obsidianEmbed);
|
|
3833
4455
|
} catch (error) {
|
|
3834
4456
|
getLogger().warn(`\uB0B4\uBD80 \uC774\uBBF8\uC9C0 \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
|
|
3835
4457
|
} finally {
|
|
@@ -3838,27 +4460,28 @@ var ImageHandler = class {
|
|
|
3838
4460
|
}
|
|
3839
4461
|
return { content: result, downloads };
|
|
3840
4462
|
}
|
|
3841
|
-
async uploadLocalImage(localPath) {
|
|
4463
|
+
async uploadLocalImage(localPath, noteFolder) {
|
|
3842
4464
|
if (!this.notionClient) {
|
|
3843
4465
|
throw new Error("NotionClient required for image upload");
|
|
3844
4466
|
}
|
|
3845
|
-
const buffer = await this.readImageFromVault(localPath);
|
|
4467
|
+
const { buffer, resolvedPath } = await this.readImageFromVault(localPath, noteFolder);
|
|
3846
4468
|
const filename = localPath.split("/").pop() ?? "image.png";
|
|
3847
4469
|
const contentType = this.getContentTypeFromPath(filename);
|
|
3848
4470
|
const blob = new Blob([buffer], { type: contentType });
|
|
3849
4471
|
const fileUploadId = await this.notionClient.uploadFile(blob, filename, contentType);
|
|
3850
4472
|
const fileHash = createHash("sha256").update(buffer).digest("hex");
|
|
3851
|
-
return { localPath, fileUploadId, fileHash, fileSize: buffer.length };
|
|
4473
|
+
return { localPath: resolvedPath, fileUploadId, fileHash, fileSize: buffer.length };
|
|
3852
4474
|
}
|
|
3853
|
-
async uploadAndAppendImages(pageId, images) {
|
|
4475
|
+
async uploadAndAppendImages(pageId, images, notePath) {
|
|
3854
4476
|
if (!this.notionClient) return [];
|
|
3855
4477
|
const localImages = images.filter((img) => !img.isExternal && img.localPath);
|
|
3856
4478
|
if (localImages.length === 0) return [];
|
|
4479
|
+
const noteFolder = notePath?.includes("/") ? notePath.slice(0, notePath.lastIndexOf("/")) : void 0;
|
|
3857
4480
|
const results = [];
|
|
3858
4481
|
const imageBlocks = [];
|
|
3859
4482
|
for (const img of localImages) {
|
|
3860
4483
|
try {
|
|
3861
|
-
const result = await this.uploadLocalImage(img.localPath);
|
|
4484
|
+
const result = await this.uploadLocalImage(img.localPath, noteFolder);
|
|
3862
4485
|
results.push(result);
|
|
3863
4486
|
imageBlocks.push({
|
|
3864
4487
|
type: "image",
|
|
@@ -3868,7 +4491,11 @@ var ImageHandler = class {
|
|
|
3868
4491
|
}
|
|
3869
4492
|
});
|
|
3870
4493
|
} catch (error) {
|
|
3871
|
-
|
|
4494
|
+
if (error?.code === "ENOENT") {
|
|
4495
|
+
getLogger().warn(`\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC \uAC74\uB108\uB700 \u2014 \uBCFC\uD2B8\uC5D0\uC11C \uD30C\uC77C\uC744 \uCC3E\uC9C0 \uBABB\uD568: ${img.localPath}`);
|
|
4496
|
+
} else {
|
|
4497
|
+
getLogger().warn(`\uC774\uBBF8\uC9C0 \uC5C5\uB85C\uB4DC \uC2E4\uD328 (${img.localPath}):`, error);
|
|
4498
|
+
}
|
|
3872
4499
|
}
|
|
3873
4500
|
}
|
|
3874
4501
|
if (imageBlocks.length > 0) {
|
|
@@ -3892,23 +4519,39 @@ var ImageHandler = class {
|
|
|
3892
4519
|
}
|
|
3893
4520
|
return results;
|
|
3894
4521
|
}
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
4522
|
+
/**
|
|
4523
|
+
* 임베드 경로를 볼트 실제 파일로 해석한다(F23). Obsidian 위키링크 임베드는 노트 옆
|
|
4524
|
+
* 파일을 파일명만으로 참조하는 게 관례라, 노트 폴더 → 볼트 루트 → attachments/ 순으로
|
|
4525
|
+
* 시도한다. 폴백은 조용히 진행하고, 전부 실패했을 때만 마지막 오류를 던진다.
|
|
4526
|
+
*/
|
|
4527
|
+
async readImageFromVault(localPath, noteFolder) {
|
|
4528
|
+
const candidates = [];
|
|
4529
|
+
if (noteFolder && !localPath.includes("/")) {
|
|
4530
|
+
candidates.push(`${noteFolder}/${localPath}`);
|
|
4531
|
+
}
|
|
4532
|
+
candidates.push(localPath, `${this.attachmentFolder}/${localPath}`);
|
|
4533
|
+
let lastError;
|
|
4534
|
+
for (const candidate of candidates) {
|
|
4535
|
+
try {
|
|
4536
|
+
return { buffer: await this.vaultFs.readBinary(candidate), resolvedPath: candidate };
|
|
4537
|
+
} catch (error) {
|
|
4538
|
+
lastError = error;
|
|
4539
|
+
}
|
|
3900
4540
|
}
|
|
4541
|
+
throw lastError;
|
|
3901
4542
|
}
|
|
3902
4543
|
getContentTypeFromPath(filename) {
|
|
3903
4544
|
const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
|
|
3904
4545
|
return (ext && EXTENSION_TO_MIME[ext]) ?? "application/octet-stream";
|
|
3905
4546
|
}
|
|
3906
4547
|
async downloadAllFiles(markdown, pageTitle) {
|
|
3907
|
-
const
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
const
|
|
3911
|
-
|
|
4548
|
+
const fileLinks = scanMarkdownLinks(markdown).filter(
|
|
4549
|
+
(s) => !s.isEmbed && FILE_LABEL_PREFIX_RE.test(s.label)
|
|
4550
|
+
);
|
|
4551
|
+
const notionHttpMatches = fileLinks.filter(
|
|
4552
|
+
(s) => /^https?:\/\//.test(s.url) && this.isNotionFileUrl(s.url)
|
|
4553
|
+
);
|
|
4554
|
+
const mdInternalMatches = fileLinks.filter((s) => INTERNAL_FILE_URL_RE.test(s.url));
|
|
3912
4555
|
const xmlHttpRegex = /<(?:file|video|audio|pdf)\s+src="(https?:\/\/[^"]+)">([\s\S]*?)<\/(?:file|video|audio|pdf)>/g;
|
|
3913
4556
|
const xmlHttpMatches = [...markdown.matchAll(xmlHttpRegex)];
|
|
3914
4557
|
const notionXmlHttpMatches = xmlHttpMatches.filter((m) => m[1] && this.isNotionFileUrl(m[1]));
|
|
@@ -3921,38 +4564,36 @@ var ImageHandler = class {
|
|
|
3921
4564
|
const sema = new Sema(this.concurrency);
|
|
3922
4565
|
const downloads = [];
|
|
3923
4566
|
let result = markdown;
|
|
3924
|
-
for (const
|
|
4567
|
+
for (const span of notionHttpMatches) {
|
|
3925
4568
|
await sema.acquire();
|
|
3926
4569
|
try {
|
|
3927
|
-
const
|
|
3928
|
-
const
|
|
3929
|
-
const download = await this.downloadFile(url, pageTitle, caption);
|
|
4570
|
+
const caption = toWikilinkAlias(span.label.replace(FILE_LABEL_PREFIX_RE, "")) || "file";
|
|
4571
|
+
const download = await this.downloadFile(span.url, pageTitle, caption);
|
|
3930
4572
|
if (!download.localPath) continue;
|
|
3931
4573
|
downloads.push(download);
|
|
3932
|
-
const obsidianLink =
|
|
3933
|
-
result = result.replace(
|
|
4574
|
+
const obsidianLink = `![[${download.localPath}|${caption}]]`;
|
|
4575
|
+
result = result.replace(span.full, obsidianLink);
|
|
3934
4576
|
} catch (error) {
|
|
3935
4577
|
getLogger().warn(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
|
|
3936
4578
|
} finally {
|
|
3937
4579
|
sema.release();
|
|
3938
4580
|
}
|
|
3939
4581
|
}
|
|
3940
|
-
for (const
|
|
4582
|
+
for (const span of mdInternalMatches) {
|
|
3941
4583
|
await sema.acquire();
|
|
3942
4584
|
try {
|
|
3943
|
-
const
|
|
3944
|
-
const parsed = this.parseNotionFileUrl(fileUrl);
|
|
4585
|
+
const parsed = this.parseNotionFileUrl(span.url);
|
|
3945
4586
|
if (!parsed || !this.notionClient) continue;
|
|
3946
4587
|
const blockId = parsed.blockId;
|
|
3947
4588
|
const fileName = parsed.fileName;
|
|
3948
4589
|
const realUrl = await this.notionClient.getFileBlockUrl(blockId);
|
|
3949
4590
|
if (!realUrl) continue;
|
|
3950
|
-
const caption = fileName ||
|
|
4591
|
+
const caption = fileName || toWikilinkAlias(span.label.replace(FILE_LABEL_PREFIX_RE, "")) || "file";
|
|
3951
4592
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
3952
4593
|
if (!download.localPath) continue;
|
|
3953
4594
|
downloads.push(download);
|
|
3954
|
-
const obsidianLink =
|
|
3955
|
-
result = result.replace(
|
|
4595
|
+
const obsidianLink = `![[${download.localPath}|${caption}]]`;
|
|
4596
|
+
result = result.replace(span.full, obsidianLink);
|
|
3956
4597
|
} catch (error) {
|
|
3957
4598
|
getLogger().warn(`\uB0B4\uBD80 \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
|
|
3958
4599
|
} finally {
|
|
@@ -3963,11 +4604,11 @@ var ImageHandler = class {
|
|
|
3963
4604
|
await sema.acquire();
|
|
3964
4605
|
try {
|
|
3965
4606
|
const url = match[1];
|
|
3966
|
-
const caption = match[2]
|
|
4607
|
+
const caption = toWikilinkAlias(match[2] ?? "") || "file";
|
|
3967
4608
|
const download = await this.downloadFile(url, pageTitle, caption);
|
|
3968
4609
|
if (!download.localPath) continue;
|
|
3969
4610
|
downloads.push(download);
|
|
3970
|
-
const obsidianLink =
|
|
4611
|
+
const obsidianLink = `![[${download.localPath}|${caption}]]`;
|
|
3971
4612
|
result = result.replace(match[0], obsidianLink);
|
|
3972
4613
|
} catch (error) {
|
|
3973
4614
|
getLogger().warn(`\uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
|
|
@@ -3985,11 +4626,11 @@ var ImageHandler = class {
|
|
|
3985
4626
|
const fileName = parsed.fileName;
|
|
3986
4627
|
const realUrl = await this.notionClient.getFileBlockUrl(blockId);
|
|
3987
4628
|
if (!realUrl) continue;
|
|
3988
|
-
const caption = fileName || match[2]
|
|
4629
|
+
const caption = fileName || toWikilinkAlias(match[2] ?? "") || "file";
|
|
3989
4630
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
3990
4631
|
if (!download.localPath) continue;
|
|
3991
4632
|
downloads.push(download);
|
|
3992
|
-
const obsidianLink =
|
|
4633
|
+
const obsidianLink = `![[${download.localPath}|${caption}]]`;
|
|
3993
4634
|
result = result.replace(match[0], obsidianLink);
|
|
3994
4635
|
} catch (error) {
|
|
3995
4636
|
getLogger().warn(`\uB0B4\uBD80 \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uC2E4\uD328: ${error}`);
|
|
@@ -5257,7 +5898,7 @@ function unescapeNotionChars(content) {
|
|
|
5257
5898
|
return content.replace(/\\~/g, "~").replace(/\\\^/g, "^");
|
|
5258
5899
|
}
|
|
5259
5900
|
var NOTION_TABLE_RE = /<table[^>]*>([\s\S]*?)<\/table>/g;
|
|
5260
|
-
var
|
|
5901
|
+
var TABLE_ROW_RE2 = /<tr>([\s\S]*?)<\/tr>/g;
|
|
5261
5902
|
var TABLE_CELL_RE = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/g;
|
|
5262
5903
|
function isAlignmentRow(cells) {
|
|
5263
5904
|
return cells.every((c) => /^:?-{2,}:?$/.test(c.trim()));
|
|
@@ -5266,7 +5907,7 @@ function convertNotionTables(content) {
|
|
|
5266
5907
|
return content.replace(NOTION_TABLE_RE, (_match, tableBody) => {
|
|
5267
5908
|
const rows = [];
|
|
5268
5909
|
let rowMatch;
|
|
5269
|
-
const rowRe = new RegExp(
|
|
5910
|
+
const rowRe = new RegExp(TABLE_ROW_RE2.source, TABLE_ROW_RE2.flags);
|
|
5270
5911
|
while ((rowMatch = rowRe.exec(tableBody)) !== null) {
|
|
5271
5912
|
const cells = [];
|
|
5272
5913
|
let cellMatch;
|
|
@@ -5647,8 +6288,10 @@ var DatabaseSyncer = class {
|
|
|
5647
6288
|
properties.icon = icon.value;
|
|
5648
6289
|
}
|
|
5649
6290
|
let markdown = "";
|
|
6291
|
+
let exportCompact = false;
|
|
5650
6292
|
try {
|
|
5651
6293
|
const mdResult = await this.notionClient.getPageMarkdown(page.id);
|
|
6294
|
+
exportCompact = isCompactExport(mdResult.markdown);
|
|
5652
6295
|
markdown = notionEnhancedToObsidian(mdResult.markdown);
|
|
5653
6296
|
} catch {
|
|
5654
6297
|
}
|
|
@@ -5688,7 +6331,7 @@ var DatabaseSyncer = class {
|
|
|
5688
6331
|
filePath,
|
|
5689
6332
|
parentMode: "database"
|
|
5690
6333
|
},
|
|
5691
|
-
{ properties }
|
|
6334
|
+
{ properties, notionExportCompact: exportCompact }
|
|
5692
6335
|
);
|
|
5693
6336
|
if (existingRecord) {
|
|
5694
6337
|
let localContent = "";
|
|
@@ -6089,8 +6732,8 @@ var ConflictResolver = class {
|
|
|
6089
6732
|
async resolveDuplicate(conflict) {
|
|
6090
6733
|
const originalPath = conflict.syncRecord.obsidianPath;
|
|
6091
6734
|
const ext = originalPath.endsWith(".md") ? ".md" : "";
|
|
6092
|
-
const
|
|
6093
|
-
const conflictPath = `${
|
|
6735
|
+
const baseName2 = originalPath.replace(/\.md$/, "");
|
|
6736
|
+
const conflictPath = `${baseName2}.conflict${ext}`;
|
|
6094
6737
|
await this.vaultFs.writeFile(conflictPath, conflict.remoteContent);
|
|
6095
6738
|
const localHash = computeHash(conflict.localContent);
|
|
6096
6739
|
this.stateDb.updateHash(
|
|
@@ -6179,6 +6822,7 @@ function resolveFrontmatterRelations(content, idToTitle) {
|
|
|
6179
6822
|
|
|
6180
6823
|
// src/sync/orchestrator.ts
|
|
6181
6824
|
var INACCESSIBLE_DBS_META_KEY = "inaccessible_dbs";
|
|
6825
|
+
var INCREMENTAL_SAFETY_WINDOW_MS = 15 * 6e4;
|
|
6182
6826
|
function strategyToChoice(strategy) {
|
|
6183
6827
|
switch (strategy) {
|
|
6184
6828
|
case "local-first":
|
|
@@ -6479,7 +7123,12 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
6479
7123
|
return record && options.paths.some((p) => record.obsidianPath.startsWith(p));
|
|
6480
7124
|
}) : remoteChanges;
|
|
6481
7125
|
if (filtered.length === 0 && (this.config.notion.databases?.length ?? 0) === 0) {
|
|
6482
|
-
const dbDiscovery = await this.pullDiscoveredDatabases(
|
|
7126
|
+
const dbDiscovery = await this.pullDiscoveredDatabases(
|
|
7127
|
+
writtenPaths,
|
|
7128
|
+
failed,
|
|
7129
|
+
conflicts,
|
|
7130
|
+
options?.force === true
|
|
7131
|
+
);
|
|
6483
7132
|
return finalize(dbDiscovery.created, dbDiscovery.updated, 0);
|
|
6484
7133
|
}
|
|
6485
7134
|
if (filtered.length === 0) {
|
|
@@ -6606,7 +7255,12 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
6606
7255
|
}
|
|
6607
7256
|
}
|
|
6608
7257
|
{
|
|
6609
|
-
const dbDiscovery = await this.pullDiscoveredDatabases(
|
|
7258
|
+
const dbDiscovery = await this.pullDiscoveredDatabases(
|
|
7259
|
+
writtenPaths,
|
|
7260
|
+
failed,
|
|
7261
|
+
conflicts,
|
|
7262
|
+
options?.force === true
|
|
7263
|
+
);
|
|
6610
7264
|
counts.created += dbDiscovery.created;
|
|
6611
7265
|
counts.updated += dbDiscovery.updated;
|
|
6612
7266
|
}
|
|
@@ -6698,7 +7352,7 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
6698
7352
|
let remoteContent = "";
|
|
6699
7353
|
if (record.notionPageId) {
|
|
6700
7354
|
try {
|
|
6701
|
-
remoteContent = await this.fetchPageMarkdown(record.notionPageId);
|
|
7355
|
+
remoteContent = (await this.fetchPageMarkdown(record.notionPageId)).content;
|
|
6702
7356
|
} catch {
|
|
6703
7357
|
}
|
|
6704
7358
|
}
|
|
@@ -6795,7 +7449,7 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
6795
7449
|
return /* @__PURE__ */ new Set();
|
|
6796
7450
|
}
|
|
6797
7451
|
}
|
|
6798
|
-
async pullDiscoveredDatabases(writtenPaths, failed, conflicts) {
|
|
7452
|
+
async pullDiscoveredDatabases(writtenPaths, failed, conflicts, forceRediscovery = false) {
|
|
6799
7453
|
if (this.isDatabaseMode) return { created: 0, updated: 0 };
|
|
6800
7454
|
let created = 0;
|
|
6801
7455
|
let updated = 0;
|
|
@@ -6829,7 +7483,7 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
6829
7483
|
`[Im-Nobsidian] DB ${rawId}: \uC811\uADFC \uAC00\uB2A5\uD55C data source \uC5C6\uC74C(\uB9C1\uD06C\uB4DC/\uBBF8\uACF5\uC720/\uC0AD\uC81C \uCD94\uC815) \u2014 \uB3D9\uAE30\uD654 \uB300\uC0C1\uC5D0\uC11C \uC81C\uC678(\uC81C\uBAA9\uC740 \uBD80\uBAA8 \uD398\uC774\uC9C0\uC5D0 \uBCF4\uC874)`
|
|
6830
7484
|
);
|
|
6831
7485
|
};
|
|
6832
|
-
if (dbConfigs.length === 0) {
|
|
7486
|
+
if (dbConfigs.length === 0 || forceRediscovery) {
|
|
6833
7487
|
const discovered = await this.discoverChildDatabases();
|
|
6834
7488
|
for (const { dbId, parentPageId } of discovered) {
|
|
6835
7489
|
const nohyph = dbId.replace(/-/g, "");
|
|
@@ -7035,7 +7689,7 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7035
7689
|
localMtime: null,
|
|
7036
7690
|
localFileSize: null
|
|
7037
7691
|
});
|
|
7038
|
-
await this.imageHandler.uploadAndAppendImages(page.id, conversionResult.images);
|
|
7692
|
+
await this.imageHandler.uploadAndAppendImages(page.id, conversionResult.images, path);
|
|
7039
7693
|
const hash = computeHash(content);
|
|
7040
7694
|
const fileStat = await this.vaultFs.getFileStat(path);
|
|
7041
7695
|
this.stateDb.transaction(() => {
|
|
@@ -7081,7 +7735,11 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7081
7735
|
parentMode: this.config.notion.parentMode
|
|
7082
7736
|
});
|
|
7083
7737
|
await this.pushUpdatePage(record.notionPageId, conversionResult.content, record.baseSnapshot);
|
|
7084
|
-
await this.imageHandler.uploadAndAppendImages(
|
|
7738
|
+
await this.imageHandler.uploadAndAppendImages(
|
|
7739
|
+
record.notionPageId,
|
|
7740
|
+
conversionResult.images,
|
|
7741
|
+
path
|
|
7742
|
+
);
|
|
7085
7743
|
let propsToUpdate;
|
|
7086
7744
|
if (this.isDatabaseMode && conversionResult.properties && Object.keys(conversionResult.properties).length > 0) {
|
|
7087
7745
|
await this.ensureDbSchema();
|
|
@@ -7305,7 +7963,9 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7305
7963
|
async detectRemoteChangesIncremental(since) {
|
|
7306
7964
|
const changes = [];
|
|
7307
7965
|
this._childParentIds.clear();
|
|
7308
|
-
const
|
|
7966
|
+
const sinceMs = Date.parse(since);
|
|
7967
|
+
const safeSince = Number.isFinite(sinceMs) ? new Date(sinceMs - INCREMENTAL_SAFETY_WINDOW_MS).toISOString() : since;
|
|
7968
|
+
const recentPages = await this.notionClient.searchRecentPages(safeSince);
|
|
7309
7969
|
for (const page of recentPages) {
|
|
7310
7970
|
const record = this.stateDb.getByNotionId(page.id);
|
|
7311
7971
|
if (!record) {
|
|
@@ -7346,7 +8006,7 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7346
8006
|
const safeName = sanitizeFileName(title);
|
|
7347
8007
|
const parentPath = await this.resolveParentPath(page);
|
|
7348
8008
|
const hasChildPages = await this.pageHasChildContainers(pageId);
|
|
7349
|
-
const markdown = await this.fetchPageMarkdown(pageId);
|
|
8009
|
+
const { content: markdown, compact: exportCompact } = await this.fetchPageMarkdown(pageId);
|
|
7350
8010
|
const hasContent = markdown.trim().length > 0;
|
|
7351
8011
|
let filePath;
|
|
7352
8012
|
let fileType;
|
|
@@ -7374,10 +8034,12 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7374
8034
|
} else {
|
|
7375
8035
|
properties = this.notionClient.extractProperties(page);
|
|
7376
8036
|
}
|
|
7377
|
-
|
|
8037
|
+
if (this.isDatabaseMode || extractTitle(filePath) !== title) {
|
|
8038
|
+
properties.title = title;
|
|
8039
|
+
}
|
|
7378
8040
|
let processedMarkdown = markdown;
|
|
7379
8041
|
if (this.config.conversion.imageDownload === "immediate") {
|
|
7380
|
-
const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
|
|
8042
|
+
const imageResult = await this.imageHandler.downloadAllImages(markdown, title, pageId);
|
|
7381
8043
|
processedMarkdown = imageResult.content;
|
|
7382
8044
|
this._pullImageCount += imageResult.downloads.length;
|
|
7383
8045
|
}
|
|
@@ -7392,7 +8054,7 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7392
8054
|
filePath,
|
|
7393
8055
|
parentMode: this.config.notion.parentMode
|
|
7394
8056
|
},
|
|
7395
|
-
{ properties }
|
|
8057
|
+
{ properties, notionExportCompact: exportCompact }
|
|
7396
8058
|
);
|
|
7397
8059
|
const resolvedParentId = await this.extractParentId(page);
|
|
7398
8060
|
await this.vaultFs.writeFile(filePath, finalContent);
|
|
@@ -7449,7 +8111,8 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7449
8111
|
const record = this.stateDb.getByNotionId(change.pageId);
|
|
7450
8112
|
if (!record) return {};
|
|
7451
8113
|
const page = await this.notionClient.getPage(change.pageId);
|
|
7452
|
-
|
|
8114
|
+
const fetched = await this.fetchPageMarkdown(change.pageId);
|
|
8115
|
+
let markdown = fetched.content;
|
|
7453
8116
|
const title = this.notionClient.extractTitle(page);
|
|
7454
8117
|
let properties;
|
|
7455
8118
|
if (this.isDatabaseMode) {
|
|
@@ -7460,9 +8123,11 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7460
8123
|
} else {
|
|
7461
8124
|
properties = this.notionClient.extractProperties(page);
|
|
7462
8125
|
}
|
|
7463
|
-
|
|
8126
|
+
if (this.isDatabaseMode || extractTitle(record.obsidianPath) !== title) {
|
|
8127
|
+
properties.title = title;
|
|
8128
|
+
}
|
|
7464
8129
|
if (this.config.conversion.imageDownload === "immediate") {
|
|
7465
|
-
const imageResult = await this.imageHandler.downloadAllImages(markdown, title);
|
|
8130
|
+
const imageResult = await this.imageHandler.downloadAllImages(markdown, title, change.pageId);
|
|
7466
8131
|
markdown = imageResult.content;
|
|
7467
8132
|
this._pullImageCount += imageResult.downloads.length;
|
|
7468
8133
|
}
|
|
@@ -7478,7 +8143,11 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7478
8143
|
filePath: record.obsidianPath,
|
|
7479
8144
|
parentMode: this.config.notion.parentMode
|
|
7480
8145
|
},
|
|
7481
|
-
{
|
|
8146
|
+
{
|
|
8147
|
+
properties,
|
|
8148
|
+
preserveMarkers: savedMarkers.length > 0 ? savedMarkers : void 0,
|
|
8149
|
+
notionExportCompact: fetched.compact
|
|
8150
|
+
}
|
|
7482
8151
|
);
|
|
7483
8152
|
let localContent;
|
|
7484
8153
|
try {
|
|
@@ -7739,11 +8408,16 @@ var SyncOrchestrator = class _SyncOrchestrator {
|
|
|
7739
8408
|
try {
|
|
7740
8409
|
const result = await this.notionClient.getPageMarkdown(pageId);
|
|
7741
8410
|
this.collectInlineDbRefs(pageId, result.markdown);
|
|
7742
|
-
return
|
|
8411
|
+
return {
|
|
8412
|
+
content: this.resolveNotionIdWikilinks(notionEnhancedToObsidian(result.markdown)),
|
|
8413
|
+
// 압축형 판정은 반드시 원시 export 기준 — enhanced 변환이 <empty-block/> 을
|
|
8414
|
+
// 빈 줄로 바꾼 뒤에는 BlockSpacer 가 저작형과 구분할 수 없다(D1).
|
|
8415
|
+
compact: isCompactExport(result.markdown)
|
|
8416
|
+
};
|
|
7743
8417
|
} catch {
|
|
7744
8418
|
}
|
|
7745
8419
|
}
|
|
7746
|
-
return this.blockConverter.notionBlocksToMarkdown(pageId);
|
|
8420
|
+
return { content: await this.blockConverter.notionBlocksToMarkdown(pageId), compact: false };
|
|
7747
8421
|
}
|
|
7748
8422
|
// Markdown API는 인라인 데이터베이스를 다음처럼 렌더한다(컬럼/synced_block 내부 포함):
|
|
7749
8423
|
// <database url="https://www.notion.so/<id-nohyph>" inline="true"
|