@im-nobsidian/core 0.18.0 → 0.19.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 +10 -4
- package/dist/index.js +257 -111
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -597,8 +597,9 @@ interface PullResult {
|
|
|
597
597
|
*/
|
|
598
598
|
readonly relinkedPaths?: readonly string[];
|
|
599
599
|
/**
|
|
600
|
-
* dry-run 만 —
|
|
601
|
-
*
|
|
600
|
+
* dry-run 만 — {@link relinkedPaths} 는 이은 것이 아니라 실제 pull 이 다시 이을 것이다(P7). 미리 보기는 노트 글을 받아
|
|
601
|
+
* 견주지 않아 글이 다른 노트는 실제 pull 에서 충돌로 남는다. 자연 이름의 노트는 이은 노트가 없는 볼트에서만 이을 것으로
|
|
602
|
+
* 센다 — 이은 노트가 있는 볼트는 글이 같아야 잇는다. 이을 노트가 없으면 생략.
|
|
602
603
|
*/
|
|
603
604
|
readonly relinkPending?: true;
|
|
604
605
|
/**
|
|
@@ -2820,7 +2821,7 @@ declare class SyncOrchestrator {
|
|
|
2820
2821
|
* 적어 push 가 올린다 — dry-run 은 받지 않아 원격이 바뀐 그대로라, 원격을 보고 거절하지 않는다.
|
|
2821
2822
|
*/
|
|
2822
2823
|
/**
|
|
2823
|
-
* @param relinkedByPull 같은 sync 의 dry-run pull 이 다시 이을
|
|
2824
|
+
* @param relinkedByPull 같은 sync 의 dry-run pull 이 받거나 다시 이을 것이 있다 — 실제 sync 는 받으며 먼저 잇고 올리므로
|
|
2824
2825
|
* 상태 기록이 비었어도 막지 않는다({@link assertVaultLinked}).
|
|
2825
2826
|
*/
|
|
2826
2827
|
private executePush;
|
|
@@ -3643,6 +3644,8 @@ interface PlannedRow {
|
|
|
3643
3644
|
readonly moves?: readonly RemoteMove[];
|
|
3644
3645
|
/** 로컬도 고친 행을 실제 pull 이 받지 않는 까닭 — 없으면 받는다. */
|
|
3645
3646
|
readonly resolution?: PlannedResolution;
|
|
3647
|
+
/** `create` 만 — 상태 기록에 없던 같은 자리의 행 노트를 다시 잇는다(P7). 새로 받는 것으로 세지 않는다. */
|
|
3648
|
+
readonly relink?: true;
|
|
3646
3649
|
}
|
|
3647
3650
|
/**
|
|
3648
3651
|
* 로컬도 고친 노트를 실제 pull 이 어떻게 처리하나 — 충돌로 남김 · 합침(merge) · 받지 않음(원격이 그대로거나
|
|
@@ -3798,6 +3801,8 @@ declare class DatabaseSyncer {
|
|
|
3798
3801
|
* 볼트에서 옮긴 레코드 — dry-run 은 옮겨 적지 않아 이동 WAL 이 없다. 실제 pull 처럼 볼트가 먼저다.
|
|
3799
3802
|
*/
|
|
3800
3803
|
movingInVault?: ReadonlySet<string>;
|
|
3804
|
+
/** 이번 미리 보기가 다시 이을 것으로 고른 자리({@link pathKey}) — 고르면 더한다. */
|
|
3805
|
+
relinkTaken?: Set<string>;
|
|
3801
3806
|
}): Promise<PlannedRow[]>;
|
|
3802
3807
|
/**
|
|
3803
3808
|
* 받은 행이 지난 사본 그대로인가 — 렌더한 글이 달라도 받을 것이 없으면 그 사이 로컬 편집을 충돌로 올리지 않는다
|
|
@@ -4110,7 +4115,8 @@ declare class WikilinkResolver implements Processor {
|
|
|
4110
4115
|
process(input: ProcessorInput): ProcessorOutput;
|
|
4111
4116
|
/**
|
|
4112
4117
|
* `[[노트#제목]]` · `[[노트#^블록]]` · `[[#제목]]` → `[글자](https://www.notion.so/<페이지>#<블록>)`. 글자는 별칭,
|
|
4113
|
-
* 없으면 Obsidian 이 보이는 「노트 > 제목」 — pull 이 그 글자면 별칭을 붙이지 않는다.
|
|
4118
|
+
* 없으면 Obsidian 이 보이는 「노트 > 제목」 — pull 이 그 글자면 별칭을 붙이지 않는다. 앵커 링크가 아니면 null, 못 풀면
|
|
4119
|
+
* 그 까닭({@link AnchorMiss}).
|
|
4114
4120
|
*/
|
|
4115
4121
|
private anchorLink;
|
|
4116
4122
|
}
|
package/dist/index.js
CHANGED
|
@@ -5994,6 +5994,9 @@ function anchorBlockOf(block) {
|
|
|
5994
5994
|
text: (body?.rich_text ?? []).map((t) => t.plain_text ?? "").join("")
|
|
5995
5995
|
};
|
|
5996
5996
|
}
|
|
5997
|
+
function flattenBlockTree(blocks) {
|
|
5998
|
+
return blocks.flatMap((block) => [block, ...flattenBlockTree(block.children ?? [])]);
|
|
5999
|
+
}
|
|
5997
6000
|
var ANCHORED_WIKILINK_RE = new LookbehindRegExp(
|
|
5998
6001
|
/\[\[([^[\]|]*#[^[\]|]*?)\\?(?:\|[^[\]]+)?\]\]/g,
|
|
5999
6002
|
void 0,
|
|
@@ -6037,7 +6040,7 @@ var WikilinkResolver = class {
|
|
|
6037
6040
|
const target = display === void 0 ? written : withoutTableAliasEscape(written);
|
|
6038
6041
|
const label = display ?? target;
|
|
6039
6042
|
const anchored = this.anchorLink(target, display, input.context.anchors);
|
|
6040
|
-
if (anchored) return anchored;
|
|
6043
|
+
if (anchored?.link) return anchored.link;
|
|
6041
6044
|
if (this.resolve) {
|
|
6042
6045
|
const entry = this.resolve(target);
|
|
6043
6046
|
if (entry) {
|
|
@@ -6048,6 +6051,7 @@ var WikilinkResolver = class {
|
|
|
6048
6051
|
}
|
|
6049
6052
|
}
|
|
6050
6053
|
const params = display ? { text: target, display } : { text: target };
|
|
6054
|
+
if (anchored?.miss) params.anchor = anchored.miss;
|
|
6051
6055
|
params.__anchor = computeAnchor(input.content, base + offset);
|
|
6052
6056
|
preserveMarkers.push({
|
|
6053
6057
|
type: "wikilink",
|
|
@@ -6065,16 +6069,19 @@ var WikilinkResolver = class {
|
|
|
6065
6069
|
}
|
|
6066
6070
|
/**
|
|
6067
6071
|
* `[[노트#제목]]` · `[[노트#^블록]]` · `[[#제목]]` → `[글자](https://www.notion.so/<페이지>#<블록>)`. 글자는 별칭,
|
|
6068
|
-
* 없으면 Obsidian 이 보이는 「노트 > 제목」 — pull 이 그 글자면 별칭을 붙이지 않는다.
|
|
6072
|
+
* 없으면 Obsidian 이 보이는 「노트 > 제목」 — pull 이 그 글자면 별칭을 붙이지 않는다. 앵커 링크가 아니면 null, 못 풀면
|
|
6073
|
+
* 그 까닭({@link AnchorMiss}).
|
|
6069
6074
|
*/
|
|
6070
6075
|
anchorLink(target, display, anchors) {
|
|
6071
6076
|
const split = anchors ? splitAnchor(target) : null;
|
|
6072
6077
|
if (!anchors || !split) return null;
|
|
6073
6078
|
const pageId = split.note === "" ? anchors.selfPageId : this.resolve?.(split.note)?.notionPageId ?? null;
|
|
6074
|
-
if (!pageId) return
|
|
6079
|
+
if (!pageId) return { miss: "page" };
|
|
6075
6080
|
const blockId = anchors.blockOf(pageId, split.anchor);
|
|
6076
|
-
if (!blockId) return
|
|
6077
|
-
return
|
|
6081
|
+
if (!blockId) return { miss: "block" };
|
|
6082
|
+
return {
|
|
6083
|
+
link: `[${display ?? anchorDisplay(split.note, split.anchor)}](${notionBlockUrl(pageId, blockId)})`
|
|
6084
|
+
};
|
|
6078
6085
|
}
|
|
6079
6086
|
};
|
|
6080
6087
|
|
|
@@ -15686,6 +15693,77 @@ async function readLocalNote(vaultFs, path) {
|
|
|
15686
15693
|
}
|
|
15687
15694
|
}
|
|
15688
15695
|
|
|
15696
|
+
// src/utils/db-row-path.ts
|
|
15697
|
+
function selectDbRowFiles(files, localFolder) {
|
|
15698
|
+
const prefix = localFolder.endsWith("/") ? localFolder : `${localFolder}/`;
|
|
15699
|
+
return files.filter((file) => {
|
|
15700
|
+
if (!file.path.startsWith(prefix)) return false;
|
|
15701
|
+
const rel = file.path.slice(prefix.length);
|
|
15702
|
+
return rel.length > 0 && !rel.includes("/");
|
|
15703
|
+
});
|
|
15704
|
+
}
|
|
15705
|
+
var PAGE_ID_SUFFIX_LENGTHS = [8, 16, 32];
|
|
15706
|
+
var PAGE_ID_SUFFIX = new RegExp(
|
|
15707
|
+
` \\((?:${PAGE_ID_SUFFIX_LENGTHS.map((length) => `[0-9a-f]{${length}}`).join("|")})\\)$`,
|
|
15708
|
+
"i"
|
|
15709
|
+
);
|
|
15710
|
+
function withoutPageIdSuffix(name) {
|
|
15711
|
+
return name.replace(PAGE_ID_SUFFIX, "");
|
|
15712
|
+
}
|
|
15713
|
+
function pagePathCandidates(dir, safeName, pageId) {
|
|
15714
|
+
const base = dir ? `${dir}/${safeName}` : safeName;
|
|
15715
|
+
const raw = pageId.replace(/-/g, "");
|
|
15716
|
+
return [
|
|
15717
|
+
`${base}.md`,
|
|
15718
|
+
...PAGE_ID_SUFFIX_LENGTHS.map((len) => `${base} (${raw.slice(0, len)}).md`)
|
|
15719
|
+
];
|
|
15720
|
+
}
|
|
15721
|
+
function resolveDbRowPath(localFolder, safeName, pageId, lookupByPath) {
|
|
15722
|
+
const ownedBySelf = (candidate) => {
|
|
15723
|
+
const owner = lookupByPath(candidate);
|
|
15724
|
+
if (!owner) return true;
|
|
15725
|
+
return owner.notionPageId != null && notionIdsEqual(owner.notionPageId, pageId);
|
|
15726
|
+
};
|
|
15727
|
+
const candidates = pagePathCandidates(localFolder, safeName, pageId);
|
|
15728
|
+
return candidates.find(ownedBySelf) ?? candidates[candidates.length - 1];
|
|
15729
|
+
}
|
|
15730
|
+
function inClaimOrder(items, chosen, idOf2) {
|
|
15731
|
+
const slots = items.flatMap((item, index) => chosen(item) ? [index] : []);
|
|
15732
|
+
const key = (item) => compactNotionId(idOf2(item));
|
|
15733
|
+
const members = slots.map((index) => items[index]).sort((a, b) => key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
|
|
15734
|
+
const ordered = [...items];
|
|
15735
|
+
slots.forEach((slot, k) => ordered[slot] = members[k]);
|
|
15736
|
+
return ordered;
|
|
15737
|
+
}
|
|
15738
|
+
|
|
15739
|
+
// src/utils/path-key.ts
|
|
15740
|
+
function pathKey(path) {
|
|
15741
|
+
return path.normalize("NFC").toLowerCase();
|
|
15742
|
+
}
|
|
15743
|
+
function foldedOwnerOf(stateDb, candidate, pageId) {
|
|
15744
|
+
return stateDb.getByPathKey(candidate).some(
|
|
15745
|
+
(record) => record.obsidianPath !== candidate && !(record.notionPageId != null && notionIdsEqual(record.notionPageId, pageId))
|
|
15746
|
+
);
|
|
15747
|
+
}
|
|
15748
|
+
|
|
15749
|
+
// src/sync/untracked-note.ts
|
|
15750
|
+
async function findUntrackedNote(stateDb, vaultFs, dir, safeName, pageId, taken) {
|
|
15751
|
+
const [natural, ...suffixed] = pagePathCandidates(dir, safeName, pageId);
|
|
15752
|
+
const order = [
|
|
15753
|
+
...suffixed.map((path) => ({ path, existing: "own" })),
|
|
15754
|
+
{ path: natural, existing: "name" }
|
|
15755
|
+
];
|
|
15756
|
+
for (const candidate of order) {
|
|
15757
|
+
if (taken(pathKey(candidate.path)) || stateDb.getByPath(candidate.path)) continue;
|
|
15758
|
+
if (foldedOwnerOf(stateDb, candidate.path, pageId)) continue;
|
|
15759
|
+
if (await vaultFs.exists(candidate.path)) return candidate;
|
|
15760
|
+
}
|
|
15761
|
+
return null;
|
|
15762
|
+
}
|
|
15763
|
+
function relinksWithoutContent(note, unlinkedVault) {
|
|
15764
|
+
return note.existing === "own" || unlinkedVault;
|
|
15765
|
+
}
|
|
15766
|
+
|
|
15689
15767
|
// src/conflict/merger.ts
|
|
15690
15768
|
function threeWayMerge(base, local, remote) {
|
|
15691
15769
|
const baseLines = base.split("\n");
|
|
@@ -16151,49 +16229,6 @@ function withUnsentProperties(content, keys, before) {
|
|
|
16151
16229
|
return (lines && replaceFrontmatterLines(content, lines)) ?? content;
|
|
16152
16230
|
}
|
|
16153
16231
|
|
|
16154
|
-
// src/utils/db-row-path.ts
|
|
16155
|
-
function selectDbRowFiles(files, localFolder) {
|
|
16156
|
-
const prefix = localFolder.endsWith("/") ? localFolder : `${localFolder}/`;
|
|
16157
|
-
return files.filter((file) => {
|
|
16158
|
-
if (!file.path.startsWith(prefix)) return false;
|
|
16159
|
-
const rel = file.path.slice(prefix.length);
|
|
16160
|
-
return rel.length > 0 && !rel.includes("/");
|
|
16161
|
-
});
|
|
16162
|
-
}
|
|
16163
|
-
var PAGE_ID_SUFFIX_LENGTHS = [8, 16, 32];
|
|
16164
|
-
var PAGE_ID_SUFFIX = new RegExp(
|
|
16165
|
-
` \\((?:${PAGE_ID_SUFFIX_LENGTHS.map((length) => `[0-9a-f]{${length}}`).join("|")})\\)$`,
|
|
16166
|
-
"i"
|
|
16167
|
-
);
|
|
16168
|
-
function withoutPageIdSuffix(name) {
|
|
16169
|
-
return name.replace(PAGE_ID_SUFFIX, "");
|
|
16170
|
-
}
|
|
16171
|
-
function pagePathCandidates(dir, safeName, pageId) {
|
|
16172
|
-
const base = dir ? `${dir}/${safeName}` : safeName;
|
|
16173
|
-
const raw = pageId.replace(/-/g, "");
|
|
16174
|
-
return [
|
|
16175
|
-
`${base}.md`,
|
|
16176
|
-
...PAGE_ID_SUFFIX_LENGTHS.map((len) => `${base} (${raw.slice(0, len)}).md`)
|
|
16177
|
-
];
|
|
16178
|
-
}
|
|
16179
|
-
function resolveDbRowPath(localFolder, safeName, pageId, lookupByPath) {
|
|
16180
|
-
const ownedBySelf = (candidate) => {
|
|
16181
|
-
const owner = lookupByPath(candidate);
|
|
16182
|
-
if (!owner) return true;
|
|
16183
|
-
return owner.notionPageId != null && notionIdsEqual(owner.notionPageId, pageId);
|
|
16184
|
-
};
|
|
16185
|
-
const candidates = pagePathCandidates(localFolder, safeName, pageId);
|
|
16186
|
-
return candidates.find(ownedBySelf) ?? candidates[candidates.length - 1];
|
|
16187
|
-
}
|
|
16188
|
-
function inClaimOrder(items, chosen, idOf2) {
|
|
16189
|
-
const slots = items.flatMap((item, index) => chosen(item) ? [index] : []);
|
|
16190
|
-
const key = (item) => compactNotionId(idOf2(item));
|
|
16191
|
-
const members = slots.map((index) => items[index]).sort((a, b) => key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
|
|
16192
|
-
const ordered = [...items];
|
|
16193
|
-
slots.forEach((slot, k) => ordered[slot] = members[k]);
|
|
16194
|
-
return ordered;
|
|
16195
|
-
}
|
|
16196
|
-
|
|
16197
16232
|
// src/sync/note-title.ts
|
|
16198
16233
|
function explicitTitle(frontmatter) {
|
|
16199
16234
|
const title = frontmatter.title;
|
|
@@ -19161,16 +19196,6 @@ function joinPath(folder, name) {
|
|
|
19161
19196
|
return folder === "" ? name : `${folder}/${name}`;
|
|
19162
19197
|
}
|
|
19163
19198
|
|
|
19164
|
-
// src/utils/path-key.ts
|
|
19165
|
-
function pathKey(path) {
|
|
19166
|
-
return path.normalize("NFC").toLowerCase();
|
|
19167
|
-
}
|
|
19168
|
-
function foldedOwnerOf(stateDb, candidate, pageId) {
|
|
19169
|
-
return stateDb.getByPathKey(candidate).some(
|
|
19170
|
-
(record) => record.obsidianPath !== candidate && !(record.notionPageId != null && notionIdsEqual(record.notionPageId, pageId))
|
|
19171
|
-
);
|
|
19172
|
-
}
|
|
19173
|
-
|
|
19174
19199
|
// src/converter/unknown-tag-fallback.ts
|
|
19175
19200
|
var UNKNOWN_BLOCK_TAG_RE2 = /<unknown (url|id)="([^"]*)"[^>]*\/>/g;
|
|
19176
19201
|
var UNKNOWN_MENTION_TAG_RE = /<unknown_mention\s+url="([^"]*)"[^>]*?\/>/g;
|
|
@@ -19846,7 +19871,13 @@ var DatabaseSyncer = class {
|
|
|
19846
19871
|
const target = await this.queryRows(dbConfig, opts);
|
|
19847
19872
|
if (target.kind !== "rows") return [];
|
|
19848
19873
|
const folderMove = opts?.followPlacement && !opts.paths ? await this.unblocked(this.placementMoveOf(dbConfig, target.meta, opts.movingInVault)) : null;
|
|
19849
|
-
const planned = await this.planRows(
|
|
19874
|
+
const planned = await this.planRows(
|
|
19875
|
+
dbConfig,
|
|
19876
|
+
target,
|
|
19877
|
+
opts?.paths,
|
|
19878
|
+
opts?.movingInVault,
|
|
19879
|
+
opts?.relinkTaken
|
|
19880
|
+
);
|
|
19850
19881
|
if (!folderMove) return planned;
|
|
19851
19882
|
const moved = (path) => within(path, folderMove.from) ? folderMove.to + path.slice(folderMove.from.length) : path;
|
|
19852
19883
|
return [
|
|
@@ -19925,7 +19956,7 @@ var DatabaseSyncer = class {
|
|
|
19925
19956
|
return false;
|
|
19926
19957
|
}
|
|
19927
19958
|
}
|
|
19928
|
-
async planRows(dbConfig, target, paths, movingInVault = /* @__PURE__ */ new Set()) {
|
|
19959
|
+
async planRows(dbConfig, target, paths, movingInVault = /* @__PURE__ */ new Set(), relinkTaken = /* @__PURE__ */ new Set()) {
|
|
19929
19960
|
const opts = { paths };
|
|
19930
19961
|
const planned = [];
|
|
19931
19962
|
const claimed = /* @__PURE__ */ new Map();
|
|
@@ -19946,6 +19977,24 @@ var DatabaseSyncer = class {
|
|
|
19946
19977
|
if (action === "skip") continue;
|
|
19947
19978
|
if (!record) {
|
|
19948
19979
|
const name = sanitizeFileName(this.notionClient.extractTitle(page));
|
|
19980
|
+
const untracked = await findUntrackedNote(
|
|
19981
|
+
this.stateDb,
|
|
19982
|
+
this.vaultFs,
|
|
19983
|
+
dbConfig.localFolder,
|
|
19984
|
+
name,
|
|
19985
|
+
page.id,
|
|
19986
|
+
(key) => relinkTaken.has(key) || claimed.has(key)
|
|
19987
|
+
);
|
|
19988
|
+
if (untracked && relinksWithoutContent(untracked, this.unlinkedVault)) {
|
|
19989
|
+
relinkTaken.add(pathKey(untracked.path));
|
|
19990
|
+
planned.push({
|
|
19991
|
+
pageId: page.id,
|
|
19992
|
+
path: untracked.path,
|
|
19993
|
+
operation: "create",
|
|
19994
|
+
relink: true
|
|
19995
|
+
});
|
|
19996
|
+
continue;
|
|
19997
|
+
}
|
|
19949
19998
|
const ownerOf = await this.rowPathOwners(dbConfig.localFolder, name, page.id);
|
|
19950
19999
|
const path = resolveDbRowPath(dbConfig.localFolder, name, page.id, (p) => {
|
|
19951
20000
|
const owner = claimed.get(pathKey(p));
|
|
@@ -20761,20 +20810,16 @@ var DatabaseSyncer = class {
|
|
|
20761
20810
|
* `claimExistingNote` 와 같은 규칙이다.
|
|
20762
20811
|
*/
|
|
20763
20812
|
async claimExistingRow(localFolder, safeName, pageId) {
|
|
20764
|
-
const
|
|
20765
|
-
|
|
20766
|
-
|
|
20767
|
-
|
|
20768
|
-
|
|
20769
|
-
|
|
20770
|
-
|
|
20771
|
-
|
|
20772
|
-
|
|
20773
|
-
|
|
20774
|
-
this.relinkClaims.add(key);
|
|
20775
|
-
return { path: candidate, existing };
|
|
20776
|
-
}
|
|
20777
|
-
return null;
|
|
20813
|
+
const found2 = await findUntrackedNote(
|
|
20814
|
+
this.stateDb,
|
|
20815
|
+
this.vaultFs,
|
|
20816
|
+
localFolder,
|
|
20817
|
+
safeName,
|
|
20818
|
+
pageId,
|
|
20819
|
+
(key) => this.relinkClaims.has(key)
|
|
20820
|
+
);
|
|
20821
|
+
if (found2) this.relinkClaims.add(pathKey(found2.path));
|
|
20822
|
+
return found2;
|
|
20778
20823
|
}
|
|
20779
20824
|
/**
|
|
20780
20825
|
* 상태 기록에 없던 행 노트를 이 행과 다시 잇는다(P7) — 페이지의 `relinkExistingNote` 와 같은 규칙. 글이 같으면 잇고, 다르면
|
|
@@ -26099,23 +26144,32 @@ var PagePusher = class {
|
|
|
26099
26144
|
return uploads;
|
|
26100
26145
|
}
|
|
26101
26146
|
/**
|
|
26102
|
-
* 노트의 제목 · 블록 링크(`[[노트#제목]]`)가 가리키는 페이지의
|
|
26103
|
-
* (anchor-links).
|
|
26147
|
+
* 노트의 제목 · 블록 링크(`[[노트#제목]]`)가 가리키는 페이지의 블록을 읽어 둔다 — 변환은 기다리지 않는다
|
|
26148
|
+
* (anchor-links). 맨 위 블록에서 모두 찾으면 거기서 멈추고, 못 찾은 앵커가 있으면 콜아웃 · 토글 · 목록 자식까지 읽는다.
|
|
26149
|
+
* 그런 링크가 없으면 읽지 않는다. 읽지 못한 페이지의 링크는 예전처럼 간다.
|
|
26104
26150
|
*
|
|
26105
|
-
* @param selfPageId 이 노트의 페이지 — `[[#제목]]` 의 대상. 새로 만드는 노트는 아직 없다(그 링크는
|
|
26151
|
+
* @param selfPageId 이 노트의 페이지 — `[[#제목]]` 의 대상. 새로 만드는 노트는 아직 없다(그 링크는 push 뒤 패스가 다시 보낸다 — `push-link-pass`).
|
|
26106
26152
|
*/
|
|
26107
26153
|
async anchorLookup(content, selfPageId) {
|
|
26108
|
-
const pages = /* @__PURE__ */ new
|
|
26154
|
+
const pages = /* @__PURE__ */ new Map();
|
|
26109
26155
|
for (const target of anchoredWikilinkTargets(content)) {
|
|
26110
|
-
const { note } = splitAnchor(target);
|
|
26156
|
+
const { note, anchor } = splitAnchor(target);
|
|
26111
26157
|
const pageId = note === "" ? selfPageId : this.stateDb.resolveWikilink(note)?.notionPageId ?? null;
|
|
26112
|
-
if (pageId)
|
|
26158
|
+
if (!pageId) continue;
|
|
26159
|
+
const key = compactNotionId(pageId);
|
|
26160
|
+
pages.set(key, [...pages.get(key) ?? [], anchor]);
|
|
26113
26161
|
}
|
|
26114
26162
|
if (pages.size === 0) return void 0;
|
|
26115
26163
|
const blocks = /* @__PURE__ */ new Map();
|
|
26116
|
-
for (const pageId of pages) {
|
|
26164
|
+
for (const [pageId, anchors] of pages) {
|
|
26117
26165
|
try {
|
|
26118
|
-
|
|
26166
|
+
const top = (await this.notionClient.fetchAllChildren(pageId)).map(anchorBlockOf);
|
|
26167
|
+
blocks.set(pageId, top);
|
|
26168
|
+
if (anchors.every((anchor) => anchorBlockId(top, anchor) !== null)) continue;
|
|
26169
|
+
blocks.set(
|
|
26170
|
+
pageId,
|
|
26171
|
+
flattenBlockTree(await this.notionClient.getBlockTree(pageId)).map(anchorBlockOf)
|
|
26172
|
+
);
|
|
26119
26173
|
} catch (error) {
|
|
26120
26174
|
getLogger().debug(
|
|
26121
26175
|
`[Im-Nobsidian] \uC81C\uBAA9 \uB9C1\uD06C\uC758 \uB300\uC0C1 \uD398\uC774\uC9C0 \uBE14\uB85D\uC744 \uC77D\uC9C0 \uBABB\uD568 (${pageId}): ${error instanceof Error ? error.message : String(error)}`
|
|
@@ -27396,6 +27450,36 @@ var PagePuller = class {
|
|
|
27396
27450
|
sanitizeFileName(this.notionClient.extractTitle(page))
|
|
27397
27451
|
);
|
|
27398
27452
|
}
|
|
27453
|
+
/**
|
|
27454
|
+
* dry-run — 실제 pull 이 이 새 페이지를 새로 받지 않고 상태 기록에 없던 노트 · 폴더와 다시 잇는가(P7). 다시 이을 노트(`note`) ·
|
|
27455
|
+
* 이미 있는 폴더(`folder` — 이은 노트가 없는 볼트에서 폴더로만 받으면 만들지 않는다), 아니면 null. 받기 전에는 폴더 노트가 될지
|
|
27456
|
+
* 모른다 — 노트 자리 · 폴더 노트 자리를 차례로 본다. `taken` 은 이번 미리 보기가 이미 이은 자리({@link pathKey}) — 고르면
|
|
27457
|
+
* 더한다. 볼트도 상태 DB 도 바꾸지 않는다.
|
|
27458
|
+
*/
|
|
27459
|
+
async plannedRelink(pageId, taken) {
|
|
27460
|
+
const page = this.detector.discoveredPage(pageId) ?? await this.notionClient.getPage(pageId);
|
|
27461
|
+
const safeName = sanitizeFileName(this.notionClient.extractTitle(page));
|
|
27462
|
+
const parentPath = await this.resolveParentPath(page);
|
|
27463
|
+
const folder = parentPath ? `${parentPath}/${safeName}` : safeName;
|
|
27464
|
+
for (const dir of [parentPath, folder]) {
|
|
27465
|
+
const found2 = await findUntrackedNote(
|
|
27466
|
+
this.stateDb,
|
|
27467
|
+
this.vaultFs,
|
|
27468
|
+
dir,
|
|
27469
|
+
safeName,
|
|
27470
|
+
pageId,
|
|
27471
|
+
(key) => taken.has(key)
|
|
27472
|
+
);
|
|
27473
|
+
if (found2 && relinksWithoutContent(found2, this.unlinkedVault)) {
|
|
27474
|
+
taken.add(pathKey(found2.path));
|
|
27475
|
+
return { path: found2.path, kind: "note" };
|
|
27476
|
+
}
|
|
27477
|
+
}
|
|
27478
|
+
const folderExists = this.unlinkedVault && !taken.has(pathKey(folder)) && this.stateDb.getByPath(folder) === null && await this.vaultFs.exists(folder);
|
|
27479
|
+
if (!folderExists) return null;
|
|
27480
|
+
taken.add(pathKey(folder));
|
|
27481
|
+
return { path: folder, kind: "folder" };
|
|
27482
|
+
}
|
|
27399
27483
|
/**
|
|
27400
27484
|
* 새 페이지가 놓일 자리가 뺀 자리인가 — 부모 폴더 · 노트 이름 · 폴더 노트가 될 때의 폴더를 본다. 받기 전에는 폴더 노트가
|
|
27401
27485
|
* 될지 모른다(본문을 받아 봐야 안다) — 어느 쪽이든 빠지면 뺀다. 자연 이름으로 보고, 같은 이름이 있을 때 붙는 id 접미사는
|
|
@@ -28081,21 +28165,22 @@ var PagePuller = class {
|
|
|
28081
28165
|
* 이름. 다시 이을지는 받은 글과 견줘 호출측이 정한다({@link relinkExistingNote}) — 잇지 않으면 장부에서 지운다.
|
|
28082
28166
|
*/
|
|
28083
28167
|
async claimExistingNote(dir, safeName, pageId) {
|
|
28084
|
-
const
|
|
28085
|
-
|
|
28086
|
-
|
|
28087
|
-
|
|
28088
|
-
|
|
28089
|
-
|
|
28090
|
-
|
|
28091
|
-
|
|
28092
|
-
|
|
28093
|
-
|
|
28168
|
+
const taken = (key) => this.claimedPaths.has(key);
|
|
28169
|
+
for (; ; ) {
|
|
28170
|
+
const found2 = await findUntrackedNote(
|
|
28171
|
+
this.stateDb,
|
|
28172
|
+
this.vaultFs,
|
|
28173
|
+
dir,
|
|
28174
|
+
safeName,
|
|
28175
|
+
pageId,
|
|
28176
|
+
taken
|
|
28177
|
+
);
|
|
28178
|
+
if (!found2) return null;
|
|
28179
|
+
const key = pathKey(found2.path);
|
|
28094
28180
|
if (this.claimedPaths.has(key)) continue;
|
|
28095
|
-
this.claimedPaths.set(key,
|
|
28096
|
-
return
|
|
28181
|
+
this.claimedPaths.set(key, found2.path);
|
|
28182
|
+
return found2;
|
|
28097
28183
|
}
|
|
28098
|
-
return null;
|
|
28099
28184
|
}
|
|
28100
28185
|
async claimFilePath(dir, safeName, pageId) {
|
|
28101
28186
|
const candidates = pagePathCandidates(dir, safeName, pageId);
|
|
@@ -28297,17 +28382,31 @@ async function resolveNotionLinks(stateDb, vaultFs, paths, pageTitle, blockAncho
|
|
|
28297
28382
|
}
|
|
28298
28383
|
|
|
28299
28384
|
// src/sync/push-link-pass.ts
|
|
28300
|
-
function notesWithResolvableWikilinks(stateDb) {
|
|
28385
|
+
function notesWithResolvableWikilinks(stateDb, pushed = /* @__PURE__ */ new Set()) {
|
|
28301
28386
|
const paths = [];
|
|
28302
28387
|
for (const record of stateDb.getAll()) {
|
|
28303
28388
|
if (!record.notionPageId || record.status !== "synced") continue;
|
|
28304
|
-
const resolvable = stateDb.getPreserveMarkers(record.obsidianPath).some(
|
|
28305
|
-
(marker) => marker.type === "wikilink" && marker.params.text !== void 0 && stateDb.resolveWikilink(marker.params.text) !== null
|
|
28306
|
-
);
|
|
28389
|
+
const resolvable = stateDb.getPreserveMarkers(record.obsidianPath).some((marker) => marker.type === "wikilink" && resolvesNow(stateDb, record, marker, pushed));
|
|
28307
28390
|
if (resolvable) paths.push(record.obsidianPath);
|
|
28308
28391
|
}
|
|
28309
28392
|
return paths.sort();
|
|
28310
28393
|
}
|
|
28394
|
+
function resolvesNow(stateDb, record, marker, pushed) {
|
|
28395
|
+
const text = marker.params.text;
|
|
28396
|
+
if (text === void 0) return false;
|
|
28397
|
+
const split = splitAnchor(text);
|
|
28398
|
+
if (!split) return stateDb.resolveWikilink(text) !== null;
|
|
28399
|
+
const target = split.note === "" ? null : stateDb.resolveWikilink(split.note);
|
|
28400
|
+
if (split.note !== "" && target === null) return false;
|
|
28401
|
+
switch (marker.params.anchor) {
|
|
28402
|
+
case "page":
|
|
28403
|
+
return true;
|
|
28404
|
+
case "block":
|
|
28405
|
+
return target !== null && target.obsidianPath !== record.obsidianPath && pushed.has(target.obsidianPath);
|
|
28406
|
+
default:
|
|
28407
|
+
return true;
|
|
28408
|
+
}
|
|
28409
|
+
}
|
|
28311
28410
|
|
|
28312
28411
|
// src/sync/pull-planner.ts
|
|
28313
28412
|
var PullPlanner = class {
|
|
@@ -28356,12 +28455,19 @@ var PullPlanner = class {
|
|
|
28356
28455
|
}
|
|
28357
28456
|
planned.push(change);
|
|
28358
28457
|
}
|
|
28458
|
+
const relinks = await this.plannedRelinks(planned);
|
|
28359
28459
|
const queued = new Set(
|
|
28360
28460
|
[...filtered, ...restoreChanges].map((change) => compactNotionId(change.pageId))
|
|
28361
28461
|
);
|
|
28362
28462
|
const movingInVault = new Set(localPlan?.scan.adoptions.map((adoption) => adoption.record.id));
|
|
28363
28463
|
const rows = [];
|
|
28364
|
-
for (const row of await this.planDatabaseRows(
|
|
28464
|
+
for (const row of await this.planDatabaseRows(
|
|
28465
|
+
options.paths,
|
|
28466
|
+
failed,
|
|
28467
|
+
ledger,
|
|
28468
|
+
movingInVault,
|
|
28469
|
+
relinks.taken
|
|
28470
|
+
)) {
|
|
28365
28471
|
if (queued.has(compactNotionId(row.pageId))) continue;
|
|
28366
28472
|
if (row.resolution?.action === "conflict") conflicts.push(row.resolution.conflict);
|
|
28367
28473
|
else if (row.resolution?.action === "merge") mergedPaths.push(row.path);
|
|
@@ -28371,16 +28477,27 @@ var PullPlanner = class {
|
|
|
28371
28477
|
const items = [];
|
|
28372
28478
|
const plannedPath = this.planner.plannedPaths(localPlan);
|
|
28373
28479
|
for (const change of [...planned, ...restoreChanges]) {
|
|
28480
|
+
const relink = relinks.byPage.get(change.pageId);
|
|
28481
|
+
if (relink) {
|
|
28482
|
+
items.push({ path: relink.path, operation: "create", unchanged: true });
|
|
28483
|
+
continue;
|
|
28484
|
+
}
|
|
28374
28485
|
const record = this.stateDb.getByNotionId(change.pageId);
|
|
28375
28486
|
items.push({
|
|
28376
28487
|
path: (record && plannedPath.get(record.id)) ?? record?.obsidianPath ?? change.title ?? change.pageId,
|
|
28377
28488
|
operation: change.type === "created" ? "create" : change.type === "deleted" ? "delete" : "update"
|
|
28378
28489
|
});
|
|
28379
28490
|
}
|
|
28380
|
-
for (const row of rows)
|
|
28491
|
+
for (const row of rows) {
|
|
28492
|
+
items.push({
|
|
28493
|
+
path: row.path,
|
|
28494
|
+
operation: row.operation,
|
|
28495
|
+
...row.relink ? { unchanged: true } : {}
|
|
28496
|
+
});
|
|
28497
|
+
}
|
|
28381
28498
|
const movedRows = rows.filter((row) => row.moves !== void 0);
|
|
28382
28499
|
items.forEach((item, index) => options.onProgress?.(index + 1, items.length, item));
|
|
28383
|
-
const count = (type) => planned.filter((c) => c.type === type).length;
|
|
28500
|
+
const count = (type) => planned.filter((c) => c.type === type && !relinks.byPage.has(c.pageId)).length;
|
|
28384
28501
|
const restoredModified = planned.filter(
|
|
28385
28502
|
(c) => c.type === "modified" && restoreIds.has(c.pageId)
|
|
28386
28503
|
).length;
|
|
@@ -28391,7 +28508,13 @@ var PullPlanner = class {
|
|
|
28391
28508
|
].map((change) => this.stateDb.getByNotionId(change.pageId)?.obsidianPath).filter((path) => path !== void 0),
|
|
28392
28509
|
...rows.filter((row) => row.operation === "update" && row.restore).map((row) => row.path)
|
|
28393
28510
|
];
|
|
28394
|
-
const rowCount = (operation, restore = false) => rows.filter(
|
|
28511
|
+
const rowCount = (operation, restore = false) => rows.filter(
|
|
28512
|
+
(row) => row.operation === operation && (row.restore ?? false) === restore && !row.relink
|
|
28513
|
+
).length;
|
|
28514
|
+
const relinkedPaths = [
|
|
28515
|
+
...[...relinks.byPage.values()].filter((relink) => relink.kind === "note").map((relink) => relink.path),
|
|
28516
|
+
...rows.filter((row) => row.relink).map((row) => row.path)
|
|
28517
|
+
];
|
|
28395
28518
|
return {
|
|
28396
28519
|
created: count("created") + rowCount("create"),
|
|
28397
28520
|
updated: count("modified") - restoredModified + rowCount("update"),
|
|
@@ -28403,6 +28526,7 @@ var PullPlanner = class {
|
|
|
28403
28526
|
...movedRows.length > 0 ? { moved: movedRows.flatMap((row) => row.moves) } : {},
|
|
28404
28527
|
conflicts,
|
|
28405
28528
|
...mergedPaths.length > 0 ? { mergedPaths } : {},
|
|
28529
|
+
...relinkedPaths.length > 0 ? { relinkedPaths, relinkPending: true } : {},
|
|
28406
28530
|
writtenPaths: [],
|
|
28407
28531
|
failed,
|
|
28408
28532
|
duration: Date.now() - startTime,
|
|
@@ -28419,6 +28543,27 @@ var PullPlanner = class {
|
|
|
28419
28543
|
}
|
|
28420
28544
|
};
|
|
28421
28545
|
}
|
|
28546
|
+
/**
|
|
28547
|
+
* 새 페이지 가운데 실제 pull 이 상태 기록에 없던 노트 · 폴더와 다시 이을 것({@link PagePuller.plannedRelink}) — 페이지 id →
|
|
28548
|
+
* 자리. `taken` 은 고른 자리라 행도 피한다.
|
|
28549
|
+
*/
|
|
28550
|
+
async plannedRelinks(planned) {
|
|
28551
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
28552
|
+
const taken = /* @__PURE__ */ new Set();
|
|
28553
|
+
for (const change of planned) {
|
|
28554
|
+
if (change.type !== "created") continue;
|
|
28555
|
+
try {
|
|
28556
|
+
const relink = await this.puller.plannedRelink(change.pageId, taken);
|
|
28557
|
+
if (relink) byPage.set(change.pageId, relink);
|
|
28558
|
+
} catch (error) {
|
|
28559
|
+
getLogger().debug(
|
|
28560
|
+
`[Im-Nobsidian] \uB2E4\uC2DC \uC774\uC744 \uC790\uB9AC\uB97C \uBBF8\uB9AC \uBCF4\uC9C0 \uBABB\uD568 (${change.pageId}):`,
|
|
28561
|
+
error
|
|
28562
|
+
);
|
|
28563
|
+
}
|
|
28564
|
+
}
|
|
28565
|
+
return { byPage, taken };
|
|
28566
|
+
}
|
|
28422
28567
|
/**
|
|
28423
28568
|
* 고친 페이지를 실제 pull 이 받는가 — 폴더로만 추적하는 페이지는 본문이 생겼을 때만 폴더 노트로 받는다. 하위 페이지 ·
|
|
28424
28569
|
* DB 를 옮기기만 해도 그 페이지의 수정 시각이 바뀌어 고친 것으로 보였다. 확인하지 못하면 센다(실제 pull 이 받아 본다).
|
|
@@ -28467,7 +28612,7 @@ var PullPlanner = class {
|
|
|
28467
28612
|
* (`DatabaseDiscovery.pullDiscoveredDatabases`)를 같은 규칙으로 고른다. 세지 못한 DB 는 이유와
|
|
28468
28613
|
* 함께 `failed` 에 싣는다 — 세지 못한 것을 없다고 하지 않는다.
|
|
28469
28614
|
*/
|
|
28470
|
-
async planDatabaseRows(paths, failed, ledger, movingInVault) {
|
|
28615
|
+
async planDatabaseRows(paths, failed, ledger, movingInVault, relinkTaken) {
|
|
28471
28616
|
const configured = this.config.notion.databases ?? [];
|
|
28472
28617
|
const rows = [];
|
|
28473
28618
|
const plan = async (dbConfig, resolveDbFolder2) => {
|
|
@@ -28481,7 +28626,8 @@ var PullPlanner = class {
|
|
|
28481
28626
|
paths,
|
|
28482
28627
|
resolveDbFolder: resolveDbFolder2,
|
|
28483
28628
|
followPlacement: resolveDbFolder2 !== void 0,
|
|
28484
|
-
movingInVault
|
|
28629
|
+
movingInVault,
|
|
28630
|
+
relinkTaken
|
|
28485
28631
|
})
|
|
28486
28632
|
);
|
|
28487
28633
|
} catch (error) {
|
|
@@ -29518,7 +29664,7 @@ var SyncOrchestrator = class {
|
|
|
29518
29664
|
* 적어 push 가 올린다 — dry-run 은 받지 않아 원격이 바뀐 그대로라, 원격을 보고 거절하지 않는다.
|
|
29519
29665
|
*/
|
|
29520
29666
|
/**
|
|
29521
|
-
* @param relinkedByPull 같은 sync 의 dry-run pull 이 다시 이을
|
|
29667
|
+
* @param relinkedByPull 같은 sync 의 dry-run pull 이 받거나 다시 이을 것이 있다 — 실제 sync 는 받으며 먼저 잇고 올리므로
|
|
29522
29668
|
* 상태 기록이 비었어도 막지 않는다({@link assertVaultLinked}).
|
|
29523
29669
|
*/
|
|
29524
29670
|
async executePush(options, mergedByPull = /* @__PURE__ */ new Set(), relinkedByPull = false) {
|
|
@@ -29876,7 +30022,7 @@ var SyncOrchestrator = class {
|
|
|
29876
30022
|
);
|
|
29877
30023
|
}
|
|
29878
30024
|
};
|
|
29879
|
-
const relinkable = notesWithResolvableWikilinks(this.stateDb).filter(
|
|
30025
|
+
const relinkable = notesWithResolvableWikilinks(this.stateDb, pushedBodies).filter(
|
|
29880
30026
|
(path) => inAnyPathScope(path, options?.paths) && !excludeSet?.has(path)
|
|
29881
30027
|
);
|
|
29882
30028
|
await runPool(relinkable, relink, {
|
|
@@ -30190,8 +30336,7 @@ var SyncOrchestrator = class {
|
|
|
30190
30336
|
},
|
|
30191
30337
|
moved: [...relocated.moved, ...plan.moved ?? []],
|
|
30192
30338
|
moveSkipped: relocated.skipped,
|
|
30193
|
-
excluded: [...exclusions.values()]
|
|
30194
|
-
...this.puller.unlinkedVaultAtStart && plan.created > 0 ? { relinkPending: true } : {}
|
|
30339
|
+
excluded: [...exclusions.values()]
|
|
30195
30340
|
};
|
|
30196
30341
|
}
|
|
30197
30342
|
let pullTotal = workItems.length;
|
|
@@ -30403,15 +30548,16 @@ var SyncOrchestrator = class {
|
|
|
30403
30548
|
const tagged = (direction) => onProgress && ((current, total, item) => onProgress(current, total, { ...item, direction }));
|
|
30404
30549
|
const pullResult = await this.executePull({ ...options, onProgress: tagged("pull") });
|
|
30405
30550
|
const conflicted = options?.dryRun ? pullResult.conflicts.map((conflict) => conflict.syncRecord.obsidianPath) : [];
|
|
30551
|
+
const relinked = options?.dryRun ? pullResult.relinkedPaths ?? [] : [];
|
|
30406
30552
|
const pushResult = await this.executePush(
|
|
30407
30553
|
{
|
|
30408
30554
|
...options,
|
|
30409
30555
|
onProgress: tagged("push"),
|
|
30410
30556
|
paths: options?.paths,
|
|
30411
|
-
excludePaths: [...pullResult.writtenPaths, ...conflicted]
|
|
30557
|
+
excludePaths: [...pullResult.writtenPaths, ...conflicted, ...relinked]
|
|
30412
30558
|
},
|
|
30413
30559
|
new Set(options?.dryRun ? pullResult.mergedPaths ?? [] : []),
|
|
30414
|
-
pullResult.
|
|
30560
|
+
options?.dryRun === true && (pullResult.created > 0 || relinked.length > 0)
|
|
30415
30561
|
);
|
|
30416
30562
|
return {
|
|
30417
30563
|
pull: pullResult,
|