@im-nobsidian/core 0.9.1 → 0.10.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 +83 -6
- package/dist/index.js +504 -95
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LogLevel, Client } from '@notionhq/client';
|
|
3
3
|
import { readFile, mkdir, writeFile, unlink, rename, stat, readdir } from 'fs/promises';
|
|
4
|
-
import { join, dirname,
|
|
4
|
+
import { join, dirname, sep, relative } from 'path';
|
|
5
5
|
import { mkdirSync, unlinkSync, utimesSync, openSync, writeSync, closeSync, readFileSync, statSync, existsSync, readlinkSync } from 'fs';
|
|
6
6
|
import Database from 'better-sqlite3';
|
|
7
7
|
import { randomUUID, createHash } from 'crypto';
|
|
@@ -455,6 +455,22 @@ function compactNotionId(id) {
|
|
|
455
455
|
return id.replace(/-/g, "").toLowerCase();
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
// src/utils/logger.ts
|
|
459
|
+
var consoleLogger = {
|
|
460
|
+
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
461
|
+
error: (msg, ...args) => console.error(msg, ...args),
|
|
462
|
+
info: (msg, ...args) => console.info(msg, ...args),
|
|
463
|
+
debug: () => {
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
var currentLogger = consoleLogger;
|
|
467
|
+
function setLogger(logger) {
|
|
468
|
+
currentLogger = logger;
|
|
469
|
+
}
|
|
470
|
+
function getLogger() {
|
|
471
|
+
return currentLogger;
|
|
472
|
+
}
|
|
473
|
+
|
|
458
474
|
// src/state/migrations/001-initial.ts
|
|
459
475
|
var INITIAL_MIGRATION = `
|
|
460
476
|
CREATE TABLE IF NOT EXISTS sync_state (
|
|
@@ -649,24 +665,6 @@ function wikilinkLookupsIn(parentIds) {
|
|
|
649
665
|
}
|
|
650
666
|
];
|
|
651
667
|
}
|
|
652
|
-
|
|
653
|
-
// src/utils/logger.ts
|
|
654
|
-
var consoleLogger = {
|
|
655
|
-
warn: (msg, ...args) => console.warn(msg, ...args),
|
|
656
|
-
error: (msg, ...args) => console.error(msg, ...args),
|
|
657
|
-
info: (msg, ...args) => console.info(msg, ...args),
|
|
658
|
-
debug: () => {
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
var currentLogger = consoleLogger;
|
|
662
|
-
function setLogger(logger) {
|
|
663
|
-
currentLogger = logger;
|
|
664
|
-
}
|
|
665
|
-
function getLogger() {
|
|
666
|
-
return currentLogger;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// src/state/state-lock.ts
|
|
670
668
|
var STATE_LOCK_HEARTBEAT_MS = 3e4;
|
|
671
669
|
var STATE_LOCK_STALE_MS = STATE_LOCK_HEARTBEAT_MS * 4;
|
|
672
670
|
function stateDbLockPath(dbPath) {
|
|
@@ -903,7 +901,8 @@ var StateDB = class _StateDB {
|
|
|
903
901
|
}
|
|
904
902
|
}
|
|
905
903
|
static openLocked(dbPath, lock) {
|
|
906
|
-
const
|
|
904
|
+
const saved = existsSync(dbPath) ? statSync(dbPath) : null;
|
|
905
|
+
const savedBytes = saved?.isFile() ? saved.size : null;
|
|
907
906
|
if (savedBytes === 0) throw SavedStateDbError.noTables(0);
|
|
908
907
|
const db = new Database(dbPath);
|
|
909
908
|
try {
|
|
@@ -912,6 +911,7 @@ var StateDB = class _StateDB {
|
|
|
912
911
|
}
|
|
913
912
|
const stateDb = new _StateDB(db, lock);
|
|
914
913
|
stateDb.migrate();
|
|
914
|
+
if (process.platform === "win32") stateDb.normalizePathSeparators();
|
|
915
915
|
return stateDb;
|
|
916
916
|
} catch (error) {
|
|
917
917
|
db.close();
|
|
@@ -925,6 +925,57 @@ var StateDB = class _StateDB {
|
|
|
925
925
|
if (version < migration.version) this.db.exec(migration.sql);
|
|
926
926
|
}
|
|
927
927
|
}
|
|
928
|
+
/**
|
|
929
|
+
* `\` 로 이은 경로를 `/` 로 고쳐 적는다 — Windows 에서만 부른다(이슈 #8). 0.9.1 까지의 CLI 는 Windows 에서
|
|
930
|
+
* 볼트 경로를 `relative()` 그대로(`00 Me\a.md`) 적었다. 이제 목록은 `/` 로 이으므로 그대로 두면 그
|
|
931
|
+
* 노트들이 「볼트에서 지움」 과 「새 노트」 로 보인다.
|
|
932
|
+
*
|
|
933
|
+
* 같은 노트가 이미 `/` 경로로도 추적되고 있으면(pull 이 `/` 로 적은 노트를 `\` 목록이 새 노트로 보고 또
|
|
934
|
+
* 올렸다) `\` 쪽 기록을 버린다 — 그 기록의 Notion 페이지는 그때 생긴 사본이다. 지우지 않고 추적만 놓는다.
|
|
935
|
+
* Linux · macOS 에서는 `\` 가 파일 이름의 글자일 수 있어 부르지 않는다.
|
|
936
|
+
*
|
|
937
|
+
* @returns 고쳐 적은 경로 수와 버린 사본 기록 수
|
|
938
|
+
*/
|
|
939
|
+
normalizePathSeparators() {
|
|
940
|
+
let renamed = 0;
|
|
941
|
+
let dropped = 0;
|
|
942
|
+
const fix = (path) => path.replace(/\\/g, "/");
|
|
943
|
+
this.db.transaction(() => {
|
|
944
|
+
const unique = (table, column, key) => {
|
|
945
|
+
const rows = this.db.prepare(
|
|
946
|
+
`SELECT ${key} AS k, ${column} AS p FROM ${table} WHERE instr(${column}, '\\') > 0`
|
|
947
|
+
).all();
|
|
948
|
+
for (const row of rows) {
|
|
949
|
+
const taken = this.db.prepare(`SELECT 1 FROM ${table} WHERE ${column} = ?`).get(fix(row.p));
|
|
950
|
+
if (taken) {
|
|
951
|
+
this.db.prepare(`DELETE FROM ${table} WHERE ${key} = ?`).run(row.k);
|
|
952
|
+
if (table === "sync_state") dropped++;
|
|
953
|
+
} else {
|
|
954
|
+
this.db.prepare(`UPDATE ${table} SET ${column} = ? WHERE ${key} = ?`).run(fix(row.p), row.k);
|
|
955
|
+
if (table === "sync_state") renamed++;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
unique("sync_state", "obsidian_path", "id");
|
|
960
|
+
unique("wikilink_map", "obsidian_path", "obsidian_path");
|
|
961
|
+
unique("file_registry", "local_path", "id");
|
|
962
|
+
this.db.prepare("UPDATE image_registry SET local_path = replace(local_path, '\\', '/')").run();
|
|
963
|
+
const markers = this.db.prepare(
|
|
964
|
+
"SELECT key FROM sync_metadata WHERE key LIKE 'preserve_markers:%' AND instr(key, '\\') > 0"
|
|
965
|
+
).all();
|
|
966
|
+
for (const { key } of markers) {
|
|
967
|
+
const taken = this.db.prepare("SELECT 1 FROM sync_metadata WHERE key = ?").get(fix(key));
|
|
968
|
+
if (taken) this.db.prepare("DELETE FROM sync_metadata WHERE key = ?").run(key);
|
|
969
|
+
else this.db.prepare("UPDATE sync_metadata SET key = ? WHERE key = ?").run(fix(key), key);
|
|
970
|
+
}
|
|
971
|
+
})();
|
|
972
|
+
if (renamed + dropped > 0) {
|
|
973
|
+
getLogger().warn(
|
|
974
|
+
`[Im-Nobsidian] Windows \uACBD\uB85C ${renamed}\uAC1C\uB97C / \uB85C \uACE0\uCCD0 \uC801\uC74C` + (dropped > 0 ? ` \xB7 \uAC19\uC740 \uB178\uD2B8\uB97C \uB610 \uC62C\uB9B0 \uC0AC\uBCF8 \uAE30\uB85D ${dropped}\uAC1C\uB294 \uCD94\uC801\uC744 \uB193\uC74C(Notion \uD398\uC774\uC9C0\uB294 \uB0A8\uC74C)` : "")
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
return { renamed, dropped };
|
|
978
|
+
}
|
|
928
979
|
getSchemaVersion() {
|
|
929
980
|
try {
|
|
930
981
|
const row = this.db.prepare("SELECT value FROM sync_metadata WHERE key = 'schema_version'").get();
|
|
@@ -4605,6 +4656,15 @@ function folderNoteOf(folder) {
|
|
|
4605
4656
|
function isFolderRecord(record) {
|
|
4606
4657
|
return (record.fileType === "folder-note" || record.fileType === "folder-only") && !isMarkdownPath(record.obsidianPath);
|
|
4607
4658
|
}
|
|
4659
|
+
function childFolderOf(record, lookup) {
|
|
4660
|
+
if (isFolderRecord(record) && !lookup.databaseAt(record.obsidianPath)) {
|
|
4661
|
+
return record.obsidianPath;
|
|
4662
|
+
}
|
|
4663
|
+
if (record.fileType === "folder-note" || record.fileType === "folder-only") {
|
|
4664
|
+
return parentFolderOf(record.obsidianPath);
|
|
4665
|
+
}
|
|
4666
|
+
return stripMarkdownExtension(record.obsidianPath);
|
|
4667
|
+
}
|
|
4608
4668
|
function parentFolderOf(path) {
|
|
4609
4669
|
const slash = path.lastIndexOf("/");
|
|
4610
4670
|
return slash < 0 ? "" : path.slice(0, slash);
|
|
@@ -4835,10 +4895,16 @@ function hasRemotePage(record) {
|
|
|
4835
4895
|
return record !== null && Boolean(record.notionPageId);
|
|
4836
4896
|
}
|
|
4837
4897
|
var ChangeDetector = class {
|
|
4838
|
-
|
|
4898
|
+
/**
|
|
4899
|
+
* @param excludes 설정이 동기화에서 뺀 경로(이슈 #8) — 목록에 없어도 지운 것이 아니다. 예전에는 뺀 폴더의
|
|
4900
|
+
* 추적 노트를 「삭제」 로 보고, deleteSync 면 Notion 페이지를 지울 수 있었다.
|
|
4901
|
+
*/
|
|
4902
|
+
constructor(stateDb, excludes = () => false) {
|
|
4839
4903
|
this.stateDb = stateDb;
|
|
4904
|
+
this.excludes = excludes;
|
|
4840
4905
|
}
|
|
4841
4906
|
stateDb;
|
|
4907
|
+
excludes;
|
|
4842
4908
|
async detectLocalChangesFast(stats, readFile3, options) {
|
|
4843
4909
|
return (await this.scanLocalChangesFast(stats, readFile3, options)).changes;
|
|
4844
4910
|
}
|
|
@@ -4914,7 +4980,7 @@ var ChangeDetector = class {
|
|
|
4914
4980
|
}
|
|
4915
4981
|
const missing = this.stateDb.getByStatus("synced").filter(
|
|
4916
4982
|
(record) => hasRemotePage(record) && !existing.has(record.obsidianPath) && // 폴더 페이지는 파일이 아니다 — 폴더가 사라진 것은 파일 목록으로 알 수 없다.
|
|
4917
|
-
!isFolderRecord(record)
|
|
4983
|
+
!isFolderRecord(record) && !this.excludes(record.obsidianPath)
|
|
4918
4984
|
);
|
|
4919
4985
|
const pairs = pairLocalMoves(
|
|
4920
4986
|
missing.map((record) => ({
|
|
@@ -5160,6 +5226,7 @@ var FrontmatterExtractor = class {
|
|
|
5160
5226
|
};
|
|
5161
5227
|
|
|
5162
5228
|
// src/utils/md-regions.ts
|
|
5229
|
+
var LIST_MARKER_SOURCE = String.raw`(?:[-*+]|\d{1,9}[.)])`;
|
|
5163
5230
|
var BLANK_LINE_RE = /\n[ \t]*\n/;
|
|
5164
5231
|
function computeAnchor(content, idx) {
|
|
5165
5232
|
const before = content.slice(Math.max(0, idx - 32), idx);
|
|
@@ -5198,7 +5265,9 @@ function closingFence(line, open) {
|
|
|
5198
5265
|
}
|
|
5199
5266
|
return { lead: m[1], bar: m[2] };
|
|
5200
5267
|
}
|
|
5201
|
-
var QUOTED_FENCE_OPEN_RE =
|
|
5268
|
+
var QUOTED_FENCE_OPEN_RE = new RegExp(
|
|
5269
|
+
String.raw`^((?:[\t ]*>)*)([\t ]*)(${LIST_MARKER_SOURCE}[\t ]+)?(\`{3,}|~{3,})(.*)\r?$`
|
|
5270
|
+
);
|
|
5202
5271
|
var QUOTE_MARK_RE = /^[\t ]*>/;
|
|
5203
5272
|
var LEADING_SPACE_RE = /^[\t ]*/;
|
|
5204
5273
|
function stripQuoteMarks(line, depth2) {
|
|
@@ -5295,7 +5364,7 @@ function codeLineMask(content) {
|
|
|
5295
5364
|
}
|
|
5296
5365
|
return inCode;
|
|
5297
5366
|
}
|
|
5298
|
-
var TOP_LIST_ITEM_RE =
|
|
5367
|
+
var TOP_LIST_ITEM_RE = new RegExp(String.raw`^ {0,3}${LIST_MARKER_SOURCE}(?:[\t ]|$)`);
|
|
5299
5368
|
var INDENTED_CODE_WIDTH = 4;
|
|
5300
5369
|
function indentedCodeLineMask(content) {
|
|
5301
5370
|
const lines = content.split("\n");
|
|
@@ -5626,7 +5695,9 @@ function withoutPrefix(line, prefix) {
|
|
|
5626
5695
|
return null;
|
|
5627
5696
|
}
|
|
5628
5697
|
var DOUBLE_DOLLAR_RE = /(?<![\\$])\$\$(?!\$)/g;
|
|
5629
|
-
var OWN_LINE_DOLLARS_RE =
|
|
5698
|
+
var OWN_LINE_DOLLARS_RE = new RegExp(
|
|
5699
|
+
String.raw`^(?:[ \t]*>)*[ \t]*(?:\[![^\]]*\][+-]?[ \t]*|${LIST_MARKER_SOURCE}[ \t]+)?\$\$[ \t]*$`
|
|
5700
|
+
);
|
|
5630
5701
|
function settleDollars(content, final) {
|
|
5631
5702
|
return mapOutsideCode(
|
|
5632
5703
|
content,
|
|
@@ -6151,7 +6222,7 @@ function widenAmbiguousFences(lines, blocks) {
|
|
|
6151
6222
|
}
|
|
6152
6223
|
return widened;
|
|
6153
6224
|
}
|
|
6154
|
-
var LIST_ITEM_LINE_RE =
|
|
6225
|
+
var LIST_ITEM_LINE_RE = new RegExp(String.raw`^[\t ]*${LIST_MARKER_SOURCE}(?:[\t ]|$)`);
|
|
6155
6226
|
function alignNestedCodeBodies(text) {
|
|
6156
6227
|
const lines = text.split("\n");
|
|
6157
6228
|
const blocks = scanContainerBlocks(lines);
|
|
@@ -7995,7 +8066,7 @@ function clampCalloutIndent(content) {
|
|
|
7995
8066
|
}
|
|
7996
8067
|
return out.join("\n");
|
|
7997
8068
|
}
|
|
7998
|
-
var BODY_INDENT_RE =
|
|
8069
|
+
var BODY_INDENT_RE = new RegExp(String.raw`^\t+(?![ \t]*${LIST_MARKER_SOURCE}(?:\s|$))`);
|
|
7999
8070
|
function clampBodyIndent(line) {
|
|
8000
8071
|
return line.replace(BODY_INDENT_RE, CLAMPED_INDENT);
|
|
8001
8072
|
}
|
|
@@ -8196,10 +8267,10 @@ function stripQuotes(s) {
|
|
|
8196
8267
|
|
|
8197
8268
|
// src/converter/lazy-continuation.ts
|
|
8198
8269
|
var QUOTE_PREFIX_RE2 = /^[\t >]*>/;
|
|
8199
|
-
var LIST_ITEM_RE =
|
|
8270
|
+
var LIST_ITEM_RE = new RegExp(String.raw`^${LIST_MARKER_SOURCE}(?:[\t ]|$)`);
|
|
8200
8271
|
var NOT_LAZY_RE = /^(?:#{1,6}(?:[\t ]|$)|>|\[!|(?:[-*_][\t ]*){3,}$|\||<br\s*\/?>[\t ]*$|%%)/;
|
|
8201
8272
|
var NOT_PARAGRAPH_RE = /^(?:#{1,6}(?:[\t ]|$)|(?:[-*_][\t ]*){3,}$|\||<br\s*\/?>[\t ]*$|\$\$|%%)/;
|
|
8202
|
-
var LIST_PARAGRAPH_RE =
|
|
8273
|
+
var LIST_PARAGRAPH_RE = new RegExp(String.raw`^${LIST_MARKER_SOURCE}[\t ]+\S`);
|
|
8203
8274
|
var FENCE_LINE_RE2 = /^(?:`{3,}|~{3,})/;
|
|
8204
8275
|
function continuesListParagraph(previous, line) {
|
|
8205
8276
|
const prev = splitQuoted(previous);
|
|
@@ -8270,7 +8341,7 @@ var BlockSpacer = class {
|
|
|
8270
8341
|
};
|
|
8271
8342
|
}
|
|
8272
8343
|
};
|
|
8273
|
-
var LIST_ITEM_RE2 =
|
|
8344
|
+
var LIST_ITEM_RE2 = new RegExp(String.raw`^[ \t]*${LIST_MARKER_SOURCE}(?:\s|$)`);
|
|
8274
8345
|
var QUOTE_RE = /^[ \t]*>/;
|
|
8275
8346
|
var CALLOUT_START_RE = /^[ \t]*>\s*\[![^\]]*\]/;
|
|
8276
8347
|
var TABLE_ROW_RE = /^[ \t]*\|/;
|
|
@@ -8645,7 +8716,7 @@ var RENDER_LINE_RULES = [
|
|
|
8645
8716
|
var SEPARATOR_BODY = "[ \\t:|-]*-{3,}";
|
|
8646
8717
|
var SEPARATOR_RE = new RegExp(`^[\\t ]*>?[\\t ]*\\|${SEPARATOR_BODY}[ \\t:|-]*\\|`);
|
|
8647
8718
|
var DEEP_FENCE_RE = /^ {4,}```/;
|
|
8648
|
-
var LIST_ITEM_RE3 =
|
|
8719
|
+
var LIST_ITEM_RE3 = new RegExp(String.raw`^([\t ]*)(${LIST_MARKER_SOURCE})(?:[\t ]|$)`);
|
|
8649
8720
|
var CODE_INDENT = 4;
|
|
8650
8721
|
var FOOTNOTE_CONTENT_WIDTH = 4;
|
|
8651
8722
|
var ATX_HEADING_RE = /^ {0,3}#{1,6}(?:[\t ]|$)/;
|
|
@@ -10152,7 +10223,7 @@ function pipeTablesToNfmTables(content) {
|
|
|
10152
10223
|
// src/converter/empty-block.ts
|
|
10153
10224
|
var EMPTY_BLOCK_LINE_RE = new RegExp(`^(${CONTAINER_PREFIX_SOURCE})<empty-block\\/>[\\t ]*$`);
|
|
10154
10225
|
var BREAK_LINE_RE = new RegExp(`^(${CONTAINER_PREFIX_SOURCE})<br\\s*\\/?>[\\t ]*$`);
|
|
10155
|
-
var LIST_ITEM_RE4 =
|
|
10226
|
+
var LIST_ITEM_RE4 = new RegExp(String.raw`^${LIST_MARKER_SOURCE}(?:\s|$)`);
|
|
10156
10227
|
function mapProseLines(content, map) {
|
|
10157
10228
|
const lines = content.split("\n");
|
|
10158
10229
|
const kinds = classifyContainerLines(lines.map((line) => splitContainerPrefix(line).body));
|
|
@@ -11131,6 +11202,32 @@ function unquoteCode(lines, bodies) {
|
|
|
11131
11202
|
return out;
|
|
11132
11203
|
}
|
|
11133
11204
|
|
|
11205
|
+
// src/converter/literal-angles.ts
|
|
11206
|
+
function unescapeAngleBrackets(content) {
|
|
11207
|
+
if (!content.includes("\\<") && !content.includes("\\>")) return content;
|
|
11208
|
+
return mapOutsideCodeFences(
|
|
11209
|
+
content,
|
|
11210
|
+
(segment) => segment.split("\n").map(unescapeLine).join("\n")
|
|
11211
|
+
);
|
|
11212
|
+
}
|
|
11213
|
+
var BLOCK_PREFIX_RE = new RegExp(String.raw`^[ \t]*(?:(?:>|${LIST_MARKER_SOURCE})[ \t]*)*$`);
|
|
11214
|
+
var TAG_START_RE = /[A-Za-z/!?]/;
|
|
11215
|
+
function unescapeLine(line) {
|
|
11216
|
+
if (!line.includes("\\<") && !line.includes("\\>")) return line;
|
|
11217
|
+
const code = inlineCodeRanges(line);
|
|
11218
|
+
return line.replace(
|
|
11219
|
+
/(\\+)([<>])/g,
|
|
11220
|
+
(match, slashes, char, at) => {
|
|
11221
|
+
if (slashes.length % 2 === 0) return match;
|
|
11222
|
+
if (code.some(([start, end]) => at >= start && at < end)) return match;
|
|
11223
|
+
const escape = at + slashes.length - 1;
|
|
11224
|
+
if (char === ">" && BLOCK_PREFIX_RE.test(line.slice(0, escape))) return match;
|
|
11225
|
+
if (char === "<" && TAG_START_RE.test(line[at + match.length] ?? "")) return match;
|
|
11226
|
+
return `${slashes.slice(1)}${char}`;
|
|
11227
|
+
}
|
|
11228
|
+
);
|
|
11229
|
+
}
|
|
11230
|
+
|
|
11134
11231
|
// src/converter/enhanced-md-converter.ts
|
|
11135
11232
|
var NOTION_CALLOUT_RE = /^::: callout\n([\s\S]*?)\n:::/gm;
|
|
11136
11233
|
var NOTION_PAGE_MENTION_RE = /<mention-page id="([^"]+)">([\s\S]*?)<\/mention-page>/g;
|
|
@@ -11167,6 +11264,7 @@ function notionEnhancedToObsidian(enhanced, options) {
|
|
|
11167
11264
|
result = emptyBlocksToBreaks(result);
|
|
11168
11265
|
result = unescapePipes(result);
|
|
11169
11266
|
result = unescapeNotionChars(result);
|
|
11267
|
+
result = unescapeAngleBrackets(result);
|
|
11170
11268
|
result = unescapeBrackets(result);
|
|
11171
11269
|
result = receiveWikilinkText(result);
|
|
11172
11270
|
result = receiveLiteralBackslashes(result);
|
|
@@ -15125,16 +15223,16 @@ var PageDecorations = class {
|
|
|
15125
15223
|
*
|
|
15126
15224
|
* @param baseName 내려받은 파일 이름의 앞부분 — 보통 노트 이름
|
|
15127
15225
|
*/
|
|
15128
|
-
async pulled(page,
|
|
15226
|
+
async pulled(page, baseName3, download) {
|
|
15129
15227
|
const out = {};
|
|
15130
15228
|
const cover = this.notionClient.extractCover(page);
|
|
15131
15229
|
if (cover) {
|
|
15132
|
-
const local = download ? await this.imageHandler.localizeNotionFileUrl(cover.url, `${
|
|
15230
|
+
const local = download ? await this.imageHandler.localizeNotionFileUrl(cover.url, `${baseName3}-cover`) : null;
|
|
15133
15231
|
out.cover = local ? `[[${local}]]` : cover.url;
|
|
15134
15232
|
}
|
|
15135
15233
|
const icon = this.notionClient.extractIcon(page);
|
|
15136
15234
|
if (icon) {
|
|
15137
|
-
const local = download && icon.type === "file" ? await this.imageHandler.localizeNotionFileUrl(icon.value, `${
|
|
15235
|
+
const local = download && icon.type === "file" ? await this.imageHandler.localizeNotionFileUrl(icon.value, `${baseName3}-icon`) : null;
|
|
15138
15236
|
out.icon = local ? `[[${local}]]` : icon.value;
|
|
15139
15237
|
}
|
|
15140
15238
|
return out;
|
|
@@ -15579,6 +15677,8 @@ var DatabaseSyncer = class {
|
|
|
15579
15677
|
if (!wholeDbInScope && !scope.some((path) => matchesPathScope(path, dbConfig.localFolder))) {
|
|
15580
15678
|
return { kind: "out-of-scope" };
|
|
15581
15679
|
}
|
|
15680
|
+
if (this.vaultFs.excludesPath?.(dbConfig.localFolder, "folder"))
|
|
15681
|
+
return { kind: "out-of-scope" };
|
|
15582
15682
|
const meta = await this.notionClient.getDatabaseMeta(dbConfig.databaseId);
|
|
15583
15683
|
const pages = await this.notionClient.queryAllDatabasePages(
|
|
15584
15684
|
dbConfig.databaseId,
|
|
@@ -15615,6 +15715,7 @@ var DatabaseSyncer = class {
|
|
|
15615
15715
|
*/
|
|
15616
15716
|
async rowAction(dbConfig, page, record, wholeDbInScope, scope) {
|
|
15617
15717
|
if (!wholeDbInScope && !(record && inAnyPathScope(record.obsidianPath, scope))) return "skip";
|
|
15718
|
+
if (record && this.vaultFs.excludesPath?.(record.obsidianPath)) return "skip";
|
|
15618
15719
|
if (!record) return "pull";
|
|
15619
15720
|
const misplaced = !isDirectDbRowPath(dbConfig.localFolder, record.obsidianPath);
|
|
15620
15721
|
const changed = misplaced || this.remoteVerdict(record, page) !== "unchanged";
|
|
@@ -15678,7 +15779,7 @@ var DatabaseSyncer = class {
|
|
|
15678
15779
|
unlistedRows(dbConfig, pages, paths) {
|
|
15679
15780
|
const listed = new Set(pages.map((page) => normalizeNotionId(page.id)));
|
|
15680
15781
|
return this.stateDb.getAll().filter(
|
|
15681
|
-
(record) => record.fileType === "db-row" && record.notionPageId !== null && record.notionParentId !== null && notionIdsEqual(record.notionParentId, dbConfig.databaseId) && !listed.has(normalizeNotionId(record.notionPageId)) && inAnyPathScope(record.obsidianPath, paths)
|
|
15782
|
+
(record) => record.fileType === "db-row" && record.notionPageId !== null && record.notionParentId !== null && notionIdsEqual(record.notionParentId, dbConfig.databaseId) && !listed.has(normalizeNotionId(record.notionPageId)) && inAnyPathScope(record.obsidianPath, paths) && !(this.vaultFs.excludesPath?.(record.obsidianPath) ?? false)
|
|
15682
15783
|
);
|
|
15683
15784
|
}
|
|
15684
15785
|
/**
|
|
@@ -16631,6 +16732,14 @@ var ScopedVaultFS = class {
|
|
|
16631
16732
|
const joined = normalizeVaultPath(this.folder === "" ? path : `${this.folder}/${path}`);
|
|
16632
16733
|
return joined !== null && matchesPathScope(joined, this.folder) && connectionFolderOf(joined, this.nested) === null;
|
|
16633
16734
|
}
|
|
16735
|
+
holdsOtherConnection(folder) {
|
|
16736
|
+
const joined = normalizeVaultPath(this.folder === "" ? folder : `${this.folder}/${folder}`);
|
|
16737
|
+
return joined !== null && this.nested.some((c) => matchesPathScope(c, joined));
|
|
16738
|
+
}
|
|
16739
|
+
excludesPath(path, kind) {
|
|
16740
|
+
const joined = normalizeVaultPath(this.folder === "" ? path : `${this.folder}/${path}`);
|
|
16741
|
+
return joined !== null && (this.base.excludesPath?.(joined, kind) ?? false);
|
|
16742
|
+
}
|
|
16634
16743
|
/** 이 연결의 경로 → 볼트 경로. */
|
|
16635
16744
|
toBase(path, write) {
|
|
16636
16745
|
const joined = normalizeVaultPath(this.folder === "" ? path : `${this.folder}/${path}`);
|
|
@@ -18496,6 +18605,199 @@ var LocalPlanner = class {
|
|
|
18496
18605
|
}
|
|
18497
18606
|
};
|
|
18498
18607
|
|
|
18608
|
+
// src/sync/remote-moves.ts
|
|
18609
|
+
var MOVABLE_TYPES = /* @__PURE__ */ new Set([
|
|
18610
|
+
"file",
|
|
18611
|
+
"folder-note",
|
|
18612
|
+
"folder-only"
|
|
18613
|
+
]);
|
|
18614
|
+
function movedInNotion(record, parentId) {
|
|
18615
|
+
return MOVABLE_TYPES.has(record.fileType) && record.notionParentId !== null && !notionIdsEqual(record.notionParentId, parentId);
|
|
18616
|
+
}
|
|
18617
|
+
var RemoteMoveFollower = class {
|
|
18618
|
+
constructor(config, stateDb, vaultFs, folderLookup) {
|
|
18619
|
+
this.config = config;
|
|
18620
|
+
this.stateDb = stateDb;
|
|
18621
|
+
this.vaultFs = vaultFs;
|
|
18622
|
+
this.folderLookup = folderLookup;
|
|
18623
|
+
}
|
|
18624
|
+
config;
|
|
18625
|
+
stateDb;
|
|
18626
|
+
vaultFs;
|
|
18627
|
+
folderLookup;
|
|
18628
|
+
/**
|
|
18629
|
+
* @param moved 페이지 id → Notion 의 새 부모 페이지 id.
|
|
18630
|
+
* @param options.dryRun 옮길 자리만 정하고 쓰지 않는다.
|
|
18631
|
+
*/
|
|
18632
|
+
async follow(moved, options = {}) {
|
|
18633
|
+
const result = { moved: [], skipped: [] };
|
|
18634
|
+
for (const [pageId, parentId] of moved) {
|
|
18635
|
+
const record = this.stateDb.getByNotionId(pageId);
|
|
18636
|
+
if (!record || !movedInNotion(record, parentId)) continue;
|
|
18637
|
+
if (this.vaultFs.excludesPath?.(record.obsidianPath)) continue;
|
|
18638
|
+
let outcome;
|
|
18639
|
+
try {
|
|
18640
|
+
outcome = await this.followOne(record, parentId, options.dryRun === true);
|
|
18641
|
+
} catch (error) {
|
|
18642
|
+
outcome = {
|
|
18643
|
+
path: record.obsidianPath,
|
|
18644
|
+
reason: `\uC62E\uAE30\uC9C0 \uBABB\uD568: ${error instanceof Error ? error.message : String(error)}`
|
|
18645
|
+
};
|
|
18646
|
+
}
|
|
18647
|
+
if ("reason" in outcome) {
|
|
18648
|
+
result.skipped.push(outcome);
|
|
18649
|
+
getLogger().warn(
|
|
18650
|
+
`[Im-Nobsidian] Notion \uC5D0\uC11C \uC62E\uAE34 \uD398\uC774\uC9C0\uB97C \uBCFC\uD2B8\uC5D0\uC11C \uC62E\uAE30\uC9C0 \uC54A\uC74C (${outcome.path}): ${outcome.reason}`
|
|
18651
|
+
);
|
|
18652
|
+
} else if (outcome.moves.length > 0) {
|
|
18653
|
+
result.moved.push(...outcome.moves);
|
|
18654
|
+
}
|
|
18655
|
+
}
|
|
18656
|
+
return result;
|
|
18657
|
+
}
|
|
18658
|
+
async followOne(record, parentId, dryRun) {
|
|
18659
|
+
const skip = (reason) => ({ path: record.obsidianPath, reason });
|
|
18660
|
+
if (this.stateDb.getIncompleteOpByState(record.id, "move")) {
|
|
18661
|
+
return skip(
|
|
18662
|
+
"\uBCFC\uD2B8\uC5D0\uC11C \uC62E\uAE34 \uAC83\uC744 \uC544\uC9C1 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uB2E4\uC74C push \uAC00 Notion \uC744 \uBCFC\uD2B8\uC758 \uC790\uB9AC\uB85C \uC62E\uAE34\uB2E4"
|
|
18663
|
+
);
|
|
18664
|
+
}
|
|
18665
|
+
const folder = this.parentFolder(parentId);
|
|
18666
|
+
if (folder === null) return skip("Notion \uC758 \uC0C8 \uBD80\uBAA8\uAC00 \uBCFC\uD2B8\uC5D0 \uC5C6\uB2E4");
|
|
18667
|
+
const units = this.unitsOf(record);
|
|
18668
|
+
const moves = units.map((from) => ({ from, to: joinPath(folder, baseName2(from)) }));
|
|
18669
|
+
if (moves.every((move) => move.from === move.to)) {
|
|
18670
|
+
if (!dryRun) this.stateDb.setNotionParentId(record.id, parentId);
|
|
18671
|
+
return { moves: [] };
|
|
18672
|
+
}
|
|
18673
|
+
for (const move of moves) {
|
|
18674
|
+
const blocked = await this.blocked(move);
|
|
18675
|
+
if (blocked) return skip(blocked);
|
|
18676
|
+
}
|
|
18677
|
+
if (dryRun) return { moves };
|
|
18678
|
+
const done = [];
|
|
18679
|
+
try {
|
|
18680
|
+
for (const move of moves) {
|
|
18681
|
+
await this.vaultFs.moveFile(move.from, move.to);
|
|
18682
|
+
done.push(move);
|
|
18683
|
+
}
|
|
18684
|
+
} catch (error) {
|
|
18685
|
+
for (const move of done.reverse())
|
|
18686
|
+
await this.vaultFs.moveFile(move.to, move.from).catch(() => {
|
|
18687
|
+
});
|
|
18688
|
+
throw error;
|
|
18689
|
+
}
|
|
18690
|
+
this.stateDb.transaction(() => {
|
|
18691
|
+
for (const move of moves) this.rewritePaths(move);
|
|
18692
|
+
this.stateDb.setNotionParentId(record.id, parentId);
|
|
18693
|
+
});
|
|
18694
|
+
for (const move of moves) {
|
|
18695
|
+
getLogger().info(
|
|
18696
|
+
`[Im-Nobsidian] Notion \uC5D0\uC11C \uC62E\uAE34 \uD398\uC774\uC9C0\uB97C \uBCFC\uD2B8\uC5D0\uC11C \uC62E\uAE40: ${move.from} \u2192 ${move.to}`
|
|
18697
|
+
);
|
|
18698
|
+
}
|
|
18699
|
+
return { moves };
|
|
18700
|
+
}
|
|
18701
|
+
/** 새 부모 아래의 자식이 놓일 폴더 — 새 페이지를 받을 때와 같은 규칙. 루트면 `""`, 볼트에 없는 부모면 null. */
|
|
18702
|
+
parentFolder(parentId) {
|
|
18703
|
+
if (notionIdsEqual(parentId, this.config.notion.rootPageId)) return "";
|
|
18704
|
+
const parent = this.stateDb.getByNotionId(parentId);
|
|
18705
|
+
return parent ? childFolderOf(parent, this.folderLookup()) : null;
|
|
18706
|
+
}
|
|
18707
|
+
/**
|
|
18708
|
+
* 함께 옮길 것 — 폴더로 추적하는 페이지는 그 폴더, 폴더 노트(`X/X.md`)는 든 폴더, 노트는 그 파일과 옆의 자식
|
|
18709
|
+
* 폴더(`X/`, 있으면).
|
|
18710
|
+
*/
|
|
18711
|
+
unitsOf(record) {
|
|
18712
|
+
if (isFolderRecord(record)) return [record.obsidianPath];
|
|
18713
|
+
if (record.fileType !== "file") return [parentFolderOf(record.obsidianPath)];
|
|
18714
|
+
const children = stripMarkdownExtension(record.obsidianPath);
|
|
18715
|
+
const hasChildren = this.stateDb.getAll().some((r) => within(r.obsidianPath, children));
|
|
18716
|
+
return hasChildren ? [record.obsidianPath, children] : [record.obsidianPath];
|
|
18717
|
+
}
|
|
18718
|
+
/** 옮기지 못할 까닭 — 없으면 null. */
|
|
18719
|
+
async blocked(move) {
|
|
18720
|
+
if (within(move.to, move.from)) return "\uC81C \uC548\uC73C\uB85C \uC62E\uAE38 \uC218 \uC5C6\uB2E4";
|
|
18721
|
+
if (!await this.vaultFs.exists(move.from)) return "\uBCFC\uD2B8\uC5D0 \uB178\uD2B8\uAC00 \uC5C6\uB2E4";
|
|
18722
|
+
if (await this.vaultFs.exists(move.to) || this.stateDb.getAll().some((r) => within(r.obsidianPath, move.to))) {
|
|
18723
|
+
return `\uC62E\uAE38 \uC790\uB9AC(${move.to})\uC5D0 \uAC19\uC740 \uC774\uB984\uC774 \uC788\uB2E4`;
|
|
18724
|
+
}
|
|
18725
|
+
const isFolder = !isMarkdownPath(move.from);
|
|
18726
|
+
if (this.vaultFs.excludesPath?.(move.to, isFolder ? "folder" : "file")) {
|
|
18727
|
+
return `\uC62E\uAE38 \uC790\uB9AC(${move.to})\uAC00 \uB3D9\uAE30\uD654\uC5D0\uC11C \uBE60\uC9C4\uB2E4(paths \xB7 .im-nobsidian-ignore)`;
|
|
18728
|
+
}
|
|
18729
|
+
if (isFolder && this.holdsDatabaseOrConnection(move.from)) {
|
|
18730
|
+
return "\uD3F4\uB354 \uC548\uC5D0 DB \uB098 \uB2E4\uB978 \uC5F0\uACB0\uC774 \uC788\uB2E4 \u2014 DB \uBCF4\uAE30(.base)\uAC00 \uD3F4\uB354 \uACBD\uB85C\uB97C \uD488\uB294\uB2E4. Obsidian \uC5D0\uC11C \uC62E\uAE30\uBA74 \uB2E4\uC74C push \uAC00 Notion \uC744 \uB9DE\uCD98\uB2E4";
|
|
18731
|
+
}
|
|
18732
|
+
return null;
|
|
18733
|
+
}
|
|
18734
|
+
holdsDatabaseOrConnection(folder) {
|
|
18735
|
+
const databases = parseDiscoveredDbs(this.stateDb.getMeta(DISCOVERED_DBS_META_KEY));
|
|
18736
|
+
if (databases.some((db) => within(db.localFolder.replace(/\/+$/, ""), folder))) return true;
|
|
18737
|
+
const baseFiles = this.stateDb.getMeta(DB_BASE_FILES_META_KEY);
|
|
18738
|
+
if (baseFiles) {
|
|
18739
|
+
try {
|
|
18740
|
+
const entries = Object.values(
|
|
18741
|
+
JSON.parse(baseFiles)
|
|
18742
|
+
);
|
|
18743
|
+
if (entries.some((entry) => entry.basePath && within(entry.basePath, folder))) return true;
|
|
18744
|
+
} catch {
|
|
18745
|
+
}
|
|
18746
|
+
}
|
|
18747
|
+
if (this.stateDb.getAll().some((r) => r.fileType === "db-row" && within(r.obsidianPath, folder))) {
|
|
18748
|
+
return true;
|
|
18749
|
+
}
|
|
18750
|
+
return this.vaultFs.holdsOtherConnection?.(folder) ?? false;
|
|
18751
|
+
}
|
|
18752
|
+
/** 옮긴 것 아래의 레코드 · 위키링크 · 보존 마커 · 첨부 기록을 새 경로로 옮겨 적는다. */
|
|
18753
|
+
rewritePaths(move) {
|
|
18754
|
+
for (const record of this.stateDb.getAll()) {
|
|
18755
|
+
if (!within(record.obsidianPath, move.from)) continue;
|
|
18756
|
+
const from = record.obsidianPath;
|
|
18757
|
+
const to = move.to + from.slice(move.from.length);
|
|
18758
|
+
this.stateDb.updatePath(record.id, to);
|
|
18759
|
+
const entry = record.notionPageId ? this.stateDb.resolvePageId(record.notionPageId) : null;
|
|
18760
|
+
if (entry && entry.obsidianPath === from) {
|
|
18761
|
+
this.stateDb.deleteWikilink(from);
|
|
18762
|
+
this.stateDb.upsertWikilink({
|
|
18763
|
+
obsidianPath: to,
|
|
18764
|
+
notionPageId: entry.notionPageId,
|
|
18765
|
+
title: wikilinkTitleFromPath(to),
|
|
18766
|
+
aliases: entry.aliases
|
|
18767
|
+
});
|
|
18768
|
+
}
|
|
18769
|
+
const markers = this.stateDb.getPreserveMarkers(from);
|
|
18770
|
+
if (markers.length > 0) {
|
|
18771
|
+
this.stateDb.storePreserveMarkers(to, markers);
|
|
18772
|
+
this.stateDb.storePreserveMarkers(from, []);
|
|
18773
|
+
}
|
|
18774
|
+
if (record.notionPageId) {
|
|
18775
|
+
for (const file of this.stateDb.getFilesByPageId(record.notionPageId)) {
|
|
18776
|
+
if (!within(file.localPath, move.from)) continue;
|
|
18777
|
+
this.stateDb.registerFile({
|
|
18778
|
+
localPath: move.to + file.localPath.slice(move.from.length),
|
|
18779
|
+
notionPageId: file.notionPageId,
|
|
18780
|
+
fileUploadId: file.fileUploadId,
|
|
18781
|
+
fileType: file.fileType,
|
|
18782
|
+
fileHash: file.fileHash,
|
|
18783
|
+
fileSize: file.fileSize
|
|
18784
|
+
});
|
|
18785
|
+
this.stateDb.deleteFileRegistry(file.localPath);
|
|
18786
|
+
}
|
|
18787
|
+
}
|
|
18788
|
+
}
|
|
18789
|
+
}
|
|
18790
|
+
};
|
|
18791
|
+
function within(path, unit) {
|
|
18792
|
+
return path === unit || path.startsWith(`${unit}/`);
|
|
18793
|
+
}
|
|
18794
|
+
function baseName2(path) {
|
|
18795
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
18796
|
+
}
|
|
18797
|
+
function joinPath(folder, name) {
|
|
18798
|
+
return folder === "" ? name : `${folder}/${name}`;
|
|
18799
|
+
}
|
|
18800
|
+
|
|
18499
18801
|
// src/sync/change-inspector.ts
|
|
18500
18802
|
var ChangeInspector = class {
|
|
18501
18803
|
constructor(stateDb, vaultFs, notionClient, planner, puller) {
|
|
@@ -21569,17 +21871,7 @@ var PagePuller = class {
|
|
|
21569
21871
|
const parentId = await extractParentId(this.notionClient, page);
|
|
21570
21872
|
if (!parentId || notionIdsEqual(parentId, this.config.notion.rootPageId)) return "";
|
|
21571
21873
|
const parentRecord = this.stateDb.getByNotionId(parentId);
|
|
21572
|
-
if (parentRecord)
|
|
21573
|
-
if (isFolderRecord(parentRecord) && !this.placement.folderLookup().databaseAt(parentRecord.obsidianPath)) {
|
|
21574
|
-
return parentRecord.obsidianPath;
|
|
21575
|
-
}
|
|
21576
|
-
if (parentRecord.fileType === "folder-note" || parentRecord.fileType === "folder-only") {
|
|
21577
|
-
const pathParts = parentRecord.obsidianPath.split("/");
|
|
21578
|
-
pathParts.pop();
|
|
21579
|
-
return pathParts.join("/");
|
|
21580
|
-
}
|
|
21581
|
-
return stripMarkdownExtension(parentRecord.obsidianPath);
|
|
21582
|
-
}
|
|
21874
|
+
if (parentRecord) return childFolderOf(parentRecord, this.placement.folderLookup());
|
|
21583
21875
|
try {
|
|
21584
21876
|
const parentPage = await this.notionClient.getPage(parentId);
|
|
21585
21877
|
const parentTitle = this.notionClient.extractTitle(parentPage);
|
|
@@ -21957,6 +22249,9 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
21957
22249
|
// 감지가 빠뜨린 자식을 {@link takeUndiscoveredChildren} 가 둘의 차로 고른다.
|
|
21958
22250
|
_bodyChildren = /* @__PURE__ */ new Map();
|
|
21959
22251
|
_queued = /* @__PURE__ */ new Set();
|
|
22252
|
+
// Notion 에서 부모가 바뀐 추적 페이지(정규화 id → 새 부모 페이지 id) — pull 이 볼트에서도 옮긴다(ADR-039).
|
|
22253
|
+
// 옮겨도 수정 시각이 바뀌지 않아(실측) 변경으로는 보이지 않는다. 감지마다 다시 채운다.
|
|
22254
|
+
_moved = /* @__PURE__ */ new Map();
|
|
21960
22255
|
/** 이번 감지에서 자식 페이지를 가진 것으로 본 페이지인가 — 폴더노트 판정의 1차 근거. */
|
|
21961
22256
|
hasChildPages(pageId) {
|
|
21962
22257
|
return this._childParentIds.has(normalizeNotionId(pageId));
|
|
@@ -22018,6 +22313,16 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22018
22313
|
}
|
|
22019
22314
|
return changes;
|
|
22020
22315
|
}
|
|
22316
|
+
/** 이번 감지가 본, Notion 에서 옮긴 추적 페이지 — 페이지 id → 새 부모 페이지 id. */
|
|
22317
|
+
get movedPages() {
|
|
22318
|
+
return this._moved;
|
|
22319
|
+
}
|
|
22320
|
+
/** 받은 부모가 상태 DB 의 것과 다르면 옮긴 페이지로 적는다. */
|
|
22321
|
+
noteParent(pageId, parentId) {
|
|
22322
|
+
const record = this.stateDb.getByNotionId(pageId);
|
|
22323
|
+
if (record && movedInNotion(record, parentId))
|
|
22324
|
+
this._moved.set(normalizeNotionId(pageId), parentId);
|
|
22325
|
+
}
|
|
22021
22326
|
/** 새 감지를 시작한다 — 지난 실행의 본문 자식과 받기로 한 페이지를 비우고 이번 변경을 적는다. */
|
|
22022
22327
|
beginDetection(changes) {
|
|
22023
22328
|
this._bodyChildren.clear();
|
|
@@ -22064,6 +22369,7 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22064
22369
|
const changes = [];
|
|
22065
22370
|
this._childParentIds.clear();
|
|
22066
22371
|
this._pages.clear();
|
|
22372
|
+
this._moved.clear();
|
|
22067
22373
|
const lastPull = this.stateDb.getMeta(LAST_PULL_META_KEY);
|
|
22068
22374
|
const syncedRecords = this.stateDb.getAll();
|
|
22069
22375
|
const trackedPageIds = new Set(
|
|
@@ -22121,7 +22427,10 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22121
22427
|
for (const page of underRoot) {
|
|
22122
22428
|
throwIfAborted(signal);
|
|
22123
22429
|
const parentId = await extractParentId(this.notionClient, page);
|
|
22124
|
-
if (parentId)
|
|
22430
|
+
if (parentId) {
|
|
22431
|
+
this._childParentIds.add(normalizeNotionId(parentId));
|
|
22432
|
+
this.noteParent(page.id, parentId);
|
|
22433
|
+
}
|
|
22125
22434
|
}
|
|
22126
22435
|
for (const page of underRoot) {
|
|
22127
22436
|
titles.set(normalizeNotionId(page.id), this.notionClient.extractTitle(page));
|
|
@@ -22206,6 +22515,7 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22206
22515
|
const databaseIds = /* @__PURE__ */ new Set();
|
|
22207
22516
|
this._childParentIds.clear();
|
|
22208
22517
|
this._pages.clear();
|
|
22518
|
+
this._moved.clear();
|
|
22209
22519
|
const searchSince = incrementalSearchSince(since);
|
|
22210
22520
|
const recentPages = await this.notionClient.searchRecentPages(searchSince, options.signal);
|
|
22211
22521
|
const untracked = [];
|
|
@@ -22228,6 +22538,7 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22228
22538
|
}
|
|
22229
22539
|
continue;
|
|
22230
22540
|
}
|
|
22541
|
+
if (page.parent.type === "page_id") this.noteParent(page.id, page.parent.page_id);
|
|
22231
22542
|
const modified = this.observation.modification(record, page);
|
|
22232
22543
|
const rowDatabase = page.parentDatabaseId ?? (record.fileType === "db-row" ? record.notionParentId : null);
|
|
22233
22544
|
if (modified && rowDatabase) databaseIds.add(compactNotionId(rowDatabase));
|
|
@@ -22348,7 +22659,7 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22348
22659
|
// src/sync/rerender-targets.ts
|
|
22349
22660
|
function rerenderTargets(stateDb, scope) {
|
|
22350
22661
|
return stateDb.getAll().filter(
|
|
22351
|
-
(record) => isRerenderable(record) && record.notionPageId !== null && !scope.queued.has(record.notionPageId) && !scope.adopting.has(record.id) && !(scope.localChangedPaths?.has(record.obsidianPath) ?? false) && inAnyPathScope(record.obsidianPath, scope.paths)
|
|
22662
|
+
(record) => isRerenderable(record) && record.notionPageId !== null && !scope.queued.has(record.notionPageId) && !scope.adopting.has(record.id) && !(scope.localChangedPaths?.has(record.obsidianPath) ?? false) && inAnyPathScope(record.obsidianPath, scope.paths) && !(scope.excludes?.(record.obsidianPath) ?? false)
|
|
22352
22663
|
).map((record) => ({
|
|
22353
22664
|
pageId: record.notionPageId,
|
|
22354
22665
|
type: "modified",
|
|
@@ -22376,7 +22687,8 @@ async function detectMissingLocalFiles(config, stateDb, vaultFs, paths) {
|
|
|
22376
22687
|
// 존재 확인을 넣어 이 제외가 비로소 근거를 갖는다(tests/sync/db-row-restore-deleted).
|
|
22377
22688
|
// push 가 만든 폴더 페이지(폴더 레코드)도 실체가 폴더다 — 안의 노트를 되살리면 폴더도
|
|
22378
22689
|
// 생긴다. 예전에는 폴더 경로에 확장자 없는 파일을 썼다(S-17).
|
|
22379
|
-
(r.fileType === "file" || r.fileType === "folder-note") && !isFolderRecord(r) && r.notionPageId !== null && inAnyPathScope(r.obsidianPath, paths)
|
|
22690
|
+
(r.fileType === "file" || r.fileType === "folder-note") && !isFolderRecord(r) && r.notionPageId !== null && inAnyPathScope(r.obsidianPath, paths) && // 설정이 뺀 노트는 목록에 없을 뿐 지운 것이 아니다 — 되살리지 않는다(이슈 #8).
|
|
22691
|
+
!(vaultFs.excludesPath?.(r.obsidianPath) ?? false)
|
|
22380
22692
|
)
|
|
22381
22693
|
);
|
|
22382
22694
|
if (scoped.length === 0) return [];
|
|
@@ -22705,7 +23017,10 @@ var SyncOrchestrator = class {
|
|
|
22705
23017
|
this.observation = new RunObservation(stateDb, notionClient);
|
|
22706
23018
|
this.recovery = new InterruptedSyncRecovery(stateDb, notionClient);
|
|
22707
23019
|
this.detector = new RemoteDetector(config, stateDb, notionClient, this.observation);
|
|
22708
|
-
const changeDetector = new ChangeDetector(
|
|
23020
|
+
const changeDetector = new ChangeDetector(
|
|
23021
|
+
stateDb,
|
|
23022
|
+
(path) => vaultFs.excludesPath?.(path) ?? false
|
|
23023
|
+
);
|
|
22709
23024
|
const pipeline = createDefaultPipeline({
|
|
22710
23025
|
wikilinkResolver: (text) => stateDb.resolveWikilink(text)
|
|
22711
23026
|
});
|
|
@@ -22793,6 +23108,12 @@ var SyncOrchestrator = class {
|
|
|
22793
23108
|
);
|
|
22794
23109
|
this.placement.useDatabaseCreator(this.databaseCreator);
|
|
22795
23110
|
this.planner = new LocalPlanner(config, stateDb, vaultFs, changeDetector, this.placement);
|
|
23111
|
+
this.remoteMoves = new RemoteMoveFollower(
|
|
23112
|
+
config,
|
|
23113
|
+
stateDb,
|
|
23114
|
+
vaultFs,
|
|
23115
|
+
() => this.placement.folderLookup()
|
|
23116
|
+
);
|
|
22796
23117
|
this.puller = new PagePuller(
|
|
22797
23118
|
config,
|
|
22798
23119
|
stateDb,
|
|
@@ -22860,6 +23181,7 @@ var SyncOrchestrator = class {
|
|
|
22860
23181
|
databaseCreator;
|
|
22861
23182
|
// 이번 실행의 로컬 변경 — 옮긴 노트 · 폴더를 짝짓고 상태 DB 에 옮겨 적는다(S-11).
|
|
22862
23183
|
planner;
|
|
23184
|
+
remoteMoves;
|
|
22863
23185
|
// 원격에서 바뀐 것 — 전체 대조 · 증분 감지(ADR-027).
|
|
22864
23186
|
detector;
|
|
22865
23187
|
// 지난번에 본 뒤로 원격이 바뀌었나(N-05) — 덮어쓰기 · 지우기 전에, 변경을 보이기 전에 본다.
|
|
@@ -23204,6 +23526,17 @@ var SyncOrchestrator = class {
|
|
|
23204
23526
|
async pull(options) {
|
|
23205
23527
|
return this.gate.run("pull", () => this.executePull(options));
|
|
23206
23528
|
}
|
|
23529
|
+
/** 감지가 본, Notion 에서 옮긴 페이지를 볼트에서 옮긴다 — 경로를 좁힌 pull 은 그 범위의 것만. */
|
|
23530
|
+
async followRemoteMoves(options) {
|
|
23531
|
+
const moved = new Map(
|
|
23532
|
+
[...this.detector.movedPages].filter(([pageId]) => {
|
|
23533
|
+
const record = this.stateDb.getByNotionId(pageId);
|
|
23534
|
+
return !!record && (!options?.paths || inAnyPathScope(record.obsidianPath, options.paths));
|
|
23535
|
+
})
|
|
23536
|
+
);
|
|
23537
|
+
if (moved.size === 0) return { moved: [], skipped: [] };
|
|
23538
|
+
return await this.remoteMoves.follow(moved, { dryRun: options?.dryRun });
|
|
23539
|
+
}
|
|
23207
23540
|
async executePull(options) {
|
|
23208
23541
|
const startTime = Date.now();
|
|
23209
23542
|
this.assertConnectionScope();
|
|
@@ -23263,6 +23596,7 @@ var SyncOrchestrator = class {
|
|
|
23263
23596
|
}
|
|
23264
23597
|
const remoteChanges = detection.changes;
|
|
23265
23598
|
detected = remoteChanges;
|
|
23599
|
+
const remoteMoves = await this.followRemoteMoves(options);
|
|
23266
23600
|
const pendingDatabases = parsePendingDatabases(
|
|
23267
23601
|
this.stateDb.getMeta(PENDING_DATABASES_META_KEY)
|
|
23268
23602
|
);
|
|
@@ -23312,13 +23646,17 @@ var SyncOrchestrator = class {
|
|
|
23312
23646
|
this.config.sync.deleteSync
|
|
23313
23647
|
),
|
|
23314
23648
|
skippedDatabases: ledger.skipped
|
|
23315
|
-
}
|
|
23649
|
+
},
|
|
23650
|
+
moved: remoteMoves.moved,
|
|
23651
|
+
moveSkipped: remoteMoves.skipped
|
|
23316
23652
|
};
|
|
23317
23653
|
};
|
|
23318
|
-
const
|
|
23654
|
+
const excludes = (path) => this.vaultFs.excludesPath?.(path) ?? false;
|
|
23655
|
+
const filtered = remoteChanges.filter((c) => {
|
|
23319
23656
|
const record = this.stateDb.getByNotionId(c.pageId);
|
|
23320
|
-
|
|
23321
|
-
|
|
23657
|
+
if (record && excludes(record.obsidianPath)) return false;
|
|
23658
|
+
return !options?.paths || !!record && inAnyPathScope(record.obsidianPath, options.paths);
|
|
23659
|
+
});
|
|
23322
23660
|
const queuedIds = new Set(filtered.map((c) => c.pageId));
|
|
23323
23661
|
const missingLocal = localPlan ? await detectMissingLocalFiles(this.config, this.stateDb, this.vaultFs, options?.paths) : [];
|
|
23324
23662
|
const restoreChanges = missingLocal.filter(
|
|
@@ -23336,11 +23674,12 @@ var SyncOrchestrator = class {
|
|
|
23336
23674
|
localChangedPaths: localPlan ? new Set(localPlan.scan.changes.map((change) => change.path)) : null,
|
|
23337
23675
|
queued: /* @__PURE__ */ new Set([...queuedIds, ...restoreIds]),
|
|
23338
23676
|
adopting,
|
|
23339
|
-
paths: options.paths
|
|
23677
|
+
paths: options.paths,
|
|
23678
|
+
excludes
|
|
23340
23679
|
}) : [];
|
|
23341
23680
|
const workItems = [...filtered, ...restoreChanges, ...rerenderChanges];
|
|
23342
23681
|
if (options?.dryRun) {
|
|
23343
|
-
|
|
23682
|
+
const plan = await this.pullPlanner.planPull(
|
|
23344
23683
|
filtered,
|
|
23345
23684
|
restoreChanges,
|
|
23346
23685
|
restoreIds,
|
|
@@ -23350,6 +23689,7 @@ var SyncOrchestrator = class {
|
|
|
23350
23689
|
scan,
|
|
23351
23690
|
ledger
|
|
23352
23691
|
);
|
|
23692
|
+
return { ...plan, moved: remoteMoves.moved, moveSkipped: remoteMoves.skipped };
|
|
23353
23693
|
}
|
|
23354
23694
|
let pullTotal = workItems.length;
|
|
23355
23695
|
let pullCompleted = 0;
|
|
@@ -23766,17 +24106,104 @@ function pushOperationOf(change) {
|
|
|
23766
24106
|
return "update";
|
|
23767
24107
|
}
|
|
23768
24108
|
}
|
|
24109
|
+
|
|
24110
|
+
// src/utils/path-filter.ts
|
|
24111
|
+
var PathFilter = class {
|
|
24112
|
+
include;
|
|
24113
|
+
exclude;
|
|
24114
|
+
constructor(include, exclude) {
|
|
24115
|
+
this.include = compileAll(include);
|
|
24116
|
+
this.exclude = compileAll(exclude);
|
|
24117
|
+
}
|
|
24118
|
+
/** 동기화하지 않는 노트 · 첨부 — 포함 패턴 어느 것에도 맞지 않거나 제외 패턴에 맞는다. */
|
|
24119
|
+
excludesFile(path) {
|
|
24120
|
+
if (this.include.length > 0 && !this.include.some((p) => matchesWithin(path, p))) return true;
|
|
24121
|
+
return this.excludesFolder(path);
|
|
24122
|
+
}
|
|
24123
|
+
/**
|
|
24124
|
+
* 걷지 않을 폴더 — 제외 패턴만 본다. 포함 패턴은 안의 파일이 정한다: `00 Me/**\/*.md` 는 폴더
|
|
24125
|
+
* `00 Me` 에 맞지 않지만 그 안의 노트에 맞는다.
|
|
24126
|
+
*/
|
|
24127
|
+
excludesFolder(path) {
|
|
24128
|
+
return this.exclude.some((p) => matchesWithin(path, p));
|
|
24129
|
+
}
|
|
24130
|
+
};
|
|
24131
|
+
function toVaultPath(relativePath, separator) {
|
|
24132
|
+
return separator === "/" ? relativePath : relativePath.split(separator).join("/");
|
|
24133
|
+
}
|
|
24134
|
+
function compileAll(patterns) {
|
|
24135
|
+
return patterns.flatMap((raw) => {
|
|
24136
|
+
const pattern = normalizePattern(raw);
|
|
24137
|
+
return pattern === null ? [] : [compile(pattern)];
|
|
24138
|
+
});
|
|
24139
|
+
}
|
|
24140
|
+
function normalizePattern(raw) {
|
|
24141
|
+
const pattern = raw.trim().replace(/\\/g, "/").replace(/^(?:\.\/|\/)+/, "").replace(/\/+$/, "");
|
|
24142
|
+
return pattern === "" ? null : pattern;
|
|
24143
|
+
}
|
|
24144
|
+
function compile(pattern) {
|
|
24145
|
+
if (pattern === "**" || pattern === "**/*") return { kind: "all" };
|
|
24146
|
+
if (!/[*?]/.test(pattern)) return { kind: "prefix", prefix: pattern };
|
|
24147
|
+
if (pattern.endsWith("/**") && !/[*?]/.test(pattern.slice(0, -3))) {
|
|
24148
|
+
return { kind: "prefix", prefix: pattern.slice(0, -3) };
|
|
24149
|
+
}
|
|
24150
|
+
const re = globToRegExp(pattern);
|
|
24151
|
+
return pattern.includes("/") ? { kind: "anchored", re } : { kind: "segment", re };
|
|
24152
|
+
}
|
|
24153
|
+
function globToRegExp(pattern) {
|
|
24154
|
+
let source = "";
|
|
24155
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
24156
|
+
const char = pattern[i];
|
|
24157
|
+
if (char === "*" && pattern[i + 1] === "*") {
|
|
24158
|
+
const segmentStart = i === 0 || pattern[i - 1] === "/";
|
|
24159
|
+
if (segmentStart && pattern[i + 2] === "/") {
|
|
24160
|
+
source += "(?:.*/)?";
|
|
24161
|
+
i += 2;
|
|
24162
|
+
} else {
|
|
24163
|
+
source += ".*";
|
|
24164
|
+
i += 1;
|
|
24165
|
+
}
|
|
24166
|
+
} else if (char === "*") {
|
|
24167
|
+
source += "[^/]*";
|
|
24168
|
+
} else if (char === "?") {
|
|
24169
|
+
source += "[^/]";
|
|
24170
|
+
} else {
|
|
24171
|
+
source += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
24172
|
+
}
|
|
24173
|
+
}
|
|
24174
|
+
return new RegExp(`^${source}$`);
|
|
24175
|
+
}
|
|
24176
|
+
function matchesWithin(path, pattern) {
|
|
24177
|
+
switch (pattern.kind) {
|
|
24178
|
+
case "all":
|
|
24179
|
+
return true;
|
|
24180
|
+
case "prefix":
|
|
24181
|
+
return path === pattern.prefix || path.startsWith(`${pattern.prefix}/`);
|
|
24182
|
+
case "segment":
|
|
24183
|
+
return path.split("/").some((segment) => pattern.re.test(segment));
|
|
24184
|
+
case "anchored": {
|
|
24185
|
+
const segments = path.split("/");
|
|
24186
|
+
for (let depth2 = 1; depth2 <= segments.length; depth2++) {
|
|
24187
|
+
if (pattern.re.test(segments.slice(0, depth2).join("/"))) return true;
|
|
24188
|
+
}
|
|
24189
|
+
return false;
|
|
24190
|
+
}
|
|
24191
|
+
}
|
|
24192
|
+
}
|
|
24193
|
+
|
|
24194
|
+
// src/sync/node-vault-fs.ts
|
|
23769
24195
|
var NodeVaultFS = class {
|
|
23770
24196
|
constructor(rootPath, pathConfig) {
|
|
23771
24197
|
this.rootPath = rootPath;
|
|
23772
|
-
this.includePatterns = pathConfig?.include ?? [];
|
|
23773
24198
|
const configExclude = pathConfig?.exclude ?? [];
|
|
23774
24199
|
const ignorePatterns = this.loadImNobsidianIgnore();
|
|
23775
|
-
this.
|
|
24200
|
+
this.filter = new PathFilter(pathConfig?.include ?? [], [...configExclude, ...ignorePatterns]);
|
|
23776
24201
|
}
|
|
23777
24202
|
rootPath;
|
|
23778
|
-
|
|
23779
|
-
|
|
24203
|
+
filter;
|
|
24204
|
+
excludesPath(path, kind = "file") {
|
|
24205
|
+
return kind === "folder" ? this.filter.excludesFolder(path) : this.filter.excludesFile(path);
|
|
24206
|
+
}
|
|
23780
24207
|
loadImNobsidianIgnore() {
|
|
23781
24208
|
try {
|
|
23782
24209
|
const ignorePath = join(this.rootPath, IGNORE_FILE);
|
|
@@ -23850,38 +24277,20 @@ var NodeVaultFS = class {
|
|
|
23850
24277
|
await this.walkDirNonMd(this.rootPath, files);
|
|
23851
24278
|
return files;
|
|
23852
24279
|
}
|
|
23853
|
-
|
|
23854
|
-
|
|
23855
|
-
|
|
23856
|
-
const dir = pattern.slice(0, -3);
|
|
23857
|
-
return relativePath.startsWith(dir + "/") || relativePath === dir;
|
|
23858
|
-
} else if (pattern.startsWith("*.")) {
|
|
23859
|
-
const ext = pattern.slice(1);
|
|
23860
|
-
return relativePath.endsWith(ext);
|
|
23861
|
-
} else if (pattern.includes("*")) {
|
|
23862
|
-
const regexStr = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
23863
|
-
return new RegExp("^" + regexStr + "$").test(relativePath);
|
|
23864
|
-
} else {
|
|
23865
|
-
return relativePath === pattern || relativePath.startsWith(pattern + "/");
|
|
23866
|
-
}
|
|
23867
|
-
}
|
|
23868
|
-
isExcluded(relativePath) {
|
|
23869
|
-
if (this.includePatterns.length > 0) {
|
|
23870
|
-
const included = this.includePatterns.some((p) => this.matchesPattern(relativePath, p));
|
|
23871
|
-
if (!included) return true;
|
|
23872
|
-
}
|
|
23873
|
-
return this.excludePatterns.some((p) => this.matchesPattern(relativePath, p));
|
|
24280
|
+
/** 볼트 경로 — Windows 의 `relative()` 는 `\` 로 잇는다. 상태 DB · 패턴 · Notion 경로는 모두 `/` 다. */
|
|
24281
|
+
vaultPath(fullPath) {
|
|
24282
|
+
return toVaultPath(relative(this.rootPath, fullPath), sep);
|
|
23874
24283
|
}
|
|
23875
24284
|
async walkDirNonMd(dir, result) {
|
|
23876
24285
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
23877
24286
|
for (const entry of entries) {
|
|
23878
24287
|
const fullPath = join(dir, entry.name);
|
|
23879
24288
|
if (entry.name.startsWith(".")) continue;
|
|
23880
|
-
const relativePath =
|
|
23881
|
-
if (this.isExcluded(relativePath)) continue;
|
|
24289
|
+
const relativePath = this.vaultPath(fullPath);
|
|
23882
24290
|
if (entry.isDirectory()) {
|
|
24291
|
+
if (this.filter.excludesFolder(relativePath)) continue;
|
|
23883
24292
|
await this.walkDirNonMd(fullPath, result);
|
|
23884
|
-
} else if (entry.isFile() && !isMarkdownPath(entry.name)) {
|
|
24293
|
+
} else if (entry.isFile() && !isMarkdownPath(entry.name) && !this.filter.excludesFile(relativePath)) {
|
|
23885
24294
|
const fileStat = await stat(fullPath);
|
|
23886
24295
|
result.push({
|
|
23887
24296
|
path: relativePath,
|
|
@@ -23896,11 +24305,11 @@ var NodeVaultFS = class {
|
|
|
23896
24305
|
for (const entry of entries) {
|
|
23897
24306
|
const fullPath = join(dir, entry.name);
|
|
23898
24307
|
if (entry.name.startsWith(".")) continue;
|
|
23899
|
-
const relativePath =
|
|
23900
|
-
if (this.isExcluded(relativePath)) continue;
|
|
24308
|
+
const relativePath = this.vaultPath(fullPath);
|
|
23901
24309
|
if (entry.isDirectory()) {
|
|
24310
|
+
if (this.filter.excludesFolder(relativePath)) continue;
|
|
23902
24311
|
await this.walkDirStats(fullPath, result);
|
|
23903
|
-
} else if (entry.isFile() && isMarkdownPath(entry.name)) {
|
|
24312
|
+
} else if (entry.isFile() && isMarkdownPath(entry.name) && !this.filter.excludesFile(relativePath)) {
|
|
23904
24313
|
const fileStat = await stat(fullPath);
|
|
23905
24314
|
result.push({
|
|
23906
24315
|
path: relativePath,
|
|
@@ -23915,11 +24324,11 @@ var NodeVaultFS = class {
|
|
|
23915
24324
|
for (const entry of entries) {
|
|
23916
24325
|
const fullPath = join(dir, entry.name);
|
|
23917
24326
|
if (entry.name.startsWith(".")) continue;
|
|
23918
|
-
const relativePath =
|
|
23919
|
-
if (this.isExcluded(relativePath)) continue;
|
|
24327
|
+
const relativePath = this.vaultPath(fullPath);
|
|
23920
24328
|
if (entry.isDirectory()) {
|
|
24329
|
+
if (this.filter.excludesFolder(relativePath)) continue;
|
|
23921
24330
|
await this.walkDir(fullPath, result);
|
|
23922
|
-
} else if (entry.isFile() && isMarkdownPath(entry.name)) {
|
|
24331
|
+
} else if (entry.isFile() && isMarkdownPath(entry.name) && !this.filter.excludesFile(relativePath)) {
|
|
23923
24332
|
const content = await readFile(fullPath, "utf-8");
|
|
23924
24333
|
const fileStat = await stat(fullPath);
|
|
23925
24334
|
result.push({
|
|
@@ -24934,9 +25343,9 @@ function readColumnLayout(lines) {
|
|
|
24934
25343
|
close(i);
|
|
24935
25344
|
return;
|
|
24936
25345
|
}
|
|
24937
|
-
const
|
|
24938
|
-
if (
|
|
24939
|
-
const ratio =
|
|
25346
|
+
const sep3 = SEP_RE.exec(line);
|
|
25347
|
+
if (sep3) {
|
|
25348
|
+
const ratio = sep3[1] === void 0 ? void 0 : Number(sep3[1]);
|
|
24940
25349
|
open.columns.push({ line: i, ...Number.isFinite(ratio) ? { ratio } : {} });
|
|
24941
25350
|
}
|
|
24942
25351
|
});
|
|
@@ -24951,6 +25360,6 @@ function formatRatio(ratio) {
|
|
|
24951
25360
|
return String(Math.round(ratio * 100) / 100);
|
|
24952
25361
|
}
|
|
24953
25362
|
|
|
24954
|
-
export { BaseFileGenerator, BlankLineCollapser, CONFIG_FILE, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConnectionBoundaryError, ConnectionScopeError, ConversionPipeline, DB_VIEWS_FILE, DB_VIEWS_PATH, DEFAULT_CONFIG, DEFAULT_TITLE_PROPERTY, DEFAULT_WATCH_DEBOUNCE_MS, DIFF_SIGN, DatabaseSyncer, EmbedResolver, EntryEditor, FileHandler, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, GITIGNORE_ENTRY, IGNORE_FILE, INTERNAL_DIR, INTERNAL_DIR_GLOB, ImageHandler, InlineAnnotationPreserver, InlineDBParser, LocalImageRestorer, MARKER_BRAND, MathNormalizer, MentionToWikilink, NOTION_COLOR_NAMES, NOTION_TOKEN_PREFIX, NOTION_TO_BASES_VIEW, NodeVaultFS, NotionBlockBuilder, NotionClient, OBSIDIAN_CONFIG_DIR, PendingWalError, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, RENDER_LINE_RULES, SAVED_STATE_TABLES_QUERY, SEPARATOR_BODY, STATE_DB_FILE, STATE_DB_MIGRATIONS, STATE_DB_PATH, SavedStateDbError, ScopedVaultFS, SidecarGenerator, StateDB, StateDbLockedError, StateDbUnavailableError, StateLock, SyncBusyError, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, ViewDataProvider, WIKILINK_LOOKUPS, WatchSyncService, WikilinkResolver, applicableChoices, assertNoPendingWal, basesViewMappingOf, basesViewTypeOf, choiceForStrategy, classifyBodyFidelity, columnLabel, compactNotionId, computeBufferHash, computeHash, configDirFrom, connectionFolderOf, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, findConnectionFolders, formatUnifiedDiff, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, hasColumnLayout, hasDesignMarker, inAnyPathScope, isInternalPath, isMarkdownPath, isRemoteDeletion, lineDiff, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, normalizeVaultPath, noteNameFromPath, notionIdFromInput, notionIdsEqual, operationLabel, parseDesignColor, pendingWalBytes, readColumnLayout, readDesignLine, resolvePropertyName, sanitizeFileName, setLogger, sortEntries, stateDbLockPath, stateDbWalPath, stripMarkdownExtension, summarizeFidelity, systemTimeZone, threeWayMerge, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness, wikilinkLookupsIn };
|
|
25363
|
+
export { BaseFileGenerator, BlankLineCollapser, CONFIG_FILE, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, ConfigManager, ConfigSchema, ConflictResolver, ConnectionBoundaryError, ConnectionScopeError, ConversionPipeline, DB_VIEWS_FILE, DB_VIEWS_PATH, DEFAULT_CONFIG, DEFAULT_TITLE_PROPERTY, DEFAULT_WATCH_DEBOUNCE_MS, DIFF_SIGN, DatabaseSyncer, EmbedResolver, EntryEditor, FileHandler, FileWatcher, FrontmatterExtractor, FrontmatterGenerator, GITIGNORE_ENTRY, IGNORE_FILE, INTERNAL_DIR, INTERNAL_DIR_GLOB, ImageHandler, InlineAnnotationPreserver, InlineDBParser, LocalImageRestorer, MARKER_BRAND, MathNormalizer, MentionToWikilink, NOTION_COLOR_NAMES, NOTION_TOKEN_PREFIX, NOTION_TO_BASES_VIEW, NodeVaultFS, NotionBlockBuilder, NotionClient, OBSIDIAN_CONFIG_DIR, PathFilter, PendingWalError, PreserveMarkerCollector, PreserveMarkerInjector, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, RENDER_LINE_RULES, SAVED_STATE_TABLES_QUERY, SEPARATOR_BODY, STATE_DB_FILE, STATE_DB_MIGRATIONS, STATE_DB_PATH, SavedStateDbError, ScopedVaultFS, SidecarGenerator, StateDB, StateDbLockedError, StateDbUnavailableError, StateLock, SyncBusyError, SyncOrchestrator, TreeMapper, UnsupportedBlockStripper, ViewDataProvider, WIKILINK_LOOKUPS, WatchSyncService, WikilinkResolver, applicableChoices, assertNoPendingWal, basesViewMappingOf, basesViewTypeOf, choiceForStrategy, classifyBodyFidelity, columnLabel, compactNotionId, computeBufferHash, computeHash, configDirFrom, connectionFolderOf, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, findConnectionFolders, formatUnifiedDiff, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, hasColumnLayout, hasDesignMarker, inAnyPathScope, isInternalPath, isMarkdownPath, isRemoteDeletion, lineDiff, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, normalizeVaultPath, noteNameFromPath, notionIdFromInput, notionIdsEqual, operationLabel, parseDesignColor, pendingWalBytes, readColumnLayout, readDesignLine, resolvePropertyName, sanitizeFileName, setLogger, sortEntries, stateDbLockPath, stateDbWalPath, stripMarkdownExtension, summarizeFidelity, systemTimeZone, threeWayMerge, toVaultPath, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness, wikilinkLookupsIn };
|
|
24955
25364
|
//# sourceMappingURL=index.js.map
|
|
24956
25365
|
//# sourceMappingURL=index.js.map
|