@im-nobsidian/core 0.11.0 → 0.12.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 +105 -10
- package/dist/index.js +783 -169
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { LogLevel, Client } from '@notionhq/client';
|
|
3
|
-
import { readFile, mkdir, writeFile, unlink, rename, stat, readdir } from 'fs/promises';
|
|
3
|
+
import { readFile, mkdir, writeFile, unlink, rmdir, rename, stat, readdir } from 'fs/promises';
|
|
4
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';
|
|
@@ -1476,6 +1476,13 @@ function normalizeAnnotations(annotations) {
|
|
|
1476
1476
|
};
|
|
1477
1477
|
}
|
|
1478
1478
|
var DEFAULT_STYLE = JSON.stringify(normalizeAnnotations(void 0));
|
|
1479
|
+
function titleRunsOf(properties) {
|
|
1480
|
+
for (const prop of Object.values(properties)) {
|
|
1481
|
+
const value = prop;
|
|
1482
|
+
if (value?.type === "title") return Array.isArray(value.title) ? value.title : null;
|
|
1483
|
+
}
|
|
1484
|
+
return null;
|
|
1485
|
+
}
|
|
1479
1486
|
function stringifyFrontmatter(content, data) {
|
|
1480
1487
|
const astral = maskAstralCharacters(data);
|
|
1481
1488
|
const out = matter.stringify(
|
|
@@ -1660,6 +1667,7 @@ function notionFileIdOf(url) {
|
|
|
1660
1667
|
}
|
|
1661
1668
|
function fileUrlOfBlock(block) {
|
|
1662
1669
|
const media = block.type ? block[block.type] : void 0;
|
|
1670
|
+
if (block.type === "embed") return media?.url ?? null;
|
|
1663
1671
|
return media?.file?.url ?? media?.external?.url ?? null;
|
|
1664
1672
|
}
|
|
1665
1673
|
|
|
@@ -1879,6 +1887,13 @@ var PropertyMapper = class _PropertyMapper {
|
|
|
1879
1887
|
relationKeys(keys) {
|
|
1880
1888
|
return keys.filter((key) => this.schema.get(key)?.type === "relation");
|
|
1881
1889
|
}
|
|
1890
|
+
/** 이 속성들 가운데 Notion 이 값을 정하는 것(수식 · 롤업 · ID · 생성일 …) — 볼트에서 고쳐도 보낼 수 없다. */
|
|
1891
|
+
readOnlyKeys(keys) {
|
|
1892
|
+
return keys.filter((key) => {
|
|
1893
|
+
const type = this.schema.get(key)?.type;
|
|
1894
|
+
return type !== void 0 && READ_ONLY_TYPES.has(type);
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1882
1897
|
/**
|
|
1883
1898
|
* 값을 보낼 수 있는 속성만, pull 이 적는 모양으로 — 원격 값과 로컬 값을 견줄 때 쓴다.
|
|
1884
1899
|
*
|
|
@@ -2307,13 +2322,6 @@ function dateText(value, zone) {
|
|
|
2307
2322
|
if (!DATE_REGEX.test(text)) return null;
|
|
2308
2323
|
return isWallTime(text) ? toZonedIso(text, zone) : text;
|
|
2309
2324
|
}
|
|
2310
|
-
function titleRunsOf(remote) {
|
|
2311
|
-
for (const prop of Object.values(remote)) {
|
|
2312
|
-
const value = prop;
|
|
2313
|
-
if (value?.type === "title") return Array.isArray(value.title) ? value.title : null;
|
|
2314
|
-
}
|
|
2315
|
-
return null;
|
|
2316
|
-
}
|
|
2317
2325
|
function richTextRunsOf(remote, key) {
|
|
2318
2326
|
const value = remote?.[key];
|
|
2319
2327
|
return value?.type === "rich_text" && Array.isArray(value.rich_text) ? value.rich_text : null;
|
|
@@ -2762,8 +2770,7 @@ function isNotionAccessDenied(error) {
|
|
|
2762
2770
|
return e.code === "restricted_resource" || e.status === 403;
|
|
2763
2771
|
}
|
|
2764
2772
|
function isTrashedOrArchived(page) {
|
|
2765
|
-
|
|
2766
|
-
return inTrash || page.archived === true;
|
|
2773
|
+
return page.in_trash === true || page.archived === true;
|
|
2767
2774
|
}
|
|
2768
2775
|
function isNotionBadRequest(error) {
|
|
2769
2776
|
return notionStatusOf(error) === 400;
|
|
@@ -2796,6 +2803,7 @@ var DiscoveryTooLargeError = class extends Error {
|
|
|
2796
2803
|
partial;
|
|
2797
2804
|
};
|
|
2798
2805
|
var NOT_IDEMPOTENT = { idempotent: false };
|
|
2806
|
+
var CREATES_BLOCKS = { idempotent: false, createsBlocks: true };
|
|
2799
2807
|
function parentDatabaseIdOf(page) {
|
|
2800
2808
|
const parent = page.parent;
|
|
2801
2809
|
if (parent?.type !== "data_source_id" && parent?.type !== "database_id") return null;
|
|
@@ -2841,8 +2849,8 @@ var NotionClient = class _NotionClient {
|
|
|
2841
2849
|
// 전역 쿨다운 밖에서, 로그 없이 흐른다.
|
|
2842
2850
|
retry: false,
|
|
2843
2851
|
// SDK 5.27+ 는 브라우저 환경(Obsidian 렌더러)에서 토큰을 받으면 던진다 — 남이 여는 페이지에 토큰이 실리는
|
|
2844
|
-
// 것을 막는 검사다. 플러그인의 토큰은 사용자 기기의 볼트 설정에 있다.
|
|
2845
|
-
|
|
2852
|
+
// 것을 막는 검사다. 플러그인의 토큰은 사용자 기기의 볼트 설정에 있다.
|
|
2853
|
+
dangerouslyAllowBrowser: true,
|
|
2846
2854
|
...options.fetch ? { fetch: options.fetch } : {}
|
|
2847
2855
|
},
|
|
2848
2856
|
options.notifications ?? DEFAULT_CONFIG.sync.notifications
|
|
@@ -2953,7 +2961,7 @@ var NotionClient = class _NotionClient {
|
|
|
2953
2961
|
properties,
|
|
2954
2962
|
children: params.children
|
|
2955
2963
|
}),
|
|
2956
|
-
|
|
2964
|
+
CREATES_BLOCKS
|
|
2957
2965
|
);
|
|
2958
2966
|
}
|
|
2959
2967
|
/**
|
|
@@ -2978,7 +2986,7 @@ var NotionClient = class _NotionClient {
|
|
|
2978
2986
|
}
|
|
2979
2987
|
})
|
|
2980
2988
|
),
|
|
2981
|
-
|
|
2989
|
+
CREATES_BLOCKS
|
|
2982
2990
|
);
|
|
2983
2991
|
const dataSourceId = db.data_sources?.[0]?.id ?? await this.primaryDataSourceId(db.id);
|
|
2984
2992
|
if (!dataSourceId) throw new Error(`\uB9CC\uB4E0 DB(${db.id})\uC758 data source \uB97C \uC54C \uC218 \uC5C6\uC74C`);
|
|
@@ -3023,7 +3031,7 @@ var NotionClient = class _NotionClient {
|
|
|
3023
3031
|
properties,
|
|
3024
3032
|
markdown: params.markdown
|
|
3025
3033
|
}),
|
|
3026
|
-
|
|
3034
|
+
CREATES_BLOCKS
|
|
3027
3035
|
);
|
|
3028
3036
|
}
|
|
3029
3037
|
/**
|
|
@@ -3438,7 +3446,7 @@ var NotionClient = class _NotionClient {
|
|
|
3438
3446
|
});
|
|
3439
3447
|
}
|
|
3440
3448
|
async archivePage(pageId) {
|
|
3441
|
-
await this.withRateLimit(() => this.client.pages.update({ page_id: pageId,
|
|
3449
|
+
await this.withRateLimit(() => this.client.pages.update({ page_id: pageId, in_trash: true }));
|
|
3442
3450
|
}
|
|
3443
3451
|
/**
|
|
3444
3452
|
* 파일 블록의 지금 받을 수 있는 주소. 블록이 없거나 볼 수 없으면 null 이다.
|
|
@@ -3582,7 +3590,7 @@ var NotionClient = class _NotionClient {
|
|
|
3582
3590
|
children: batch,
|
|
3583
3591
|
...after ? { position: { type: "after_block", after_block: { id: after } } } : {}
|
|
3584
3592
|
}),
|
|
3585
|
-
|
|
3593
|
+
CREATES_BLOCKS
|
|
3586
3594
|
);
|
|
3587
3595
|
const blocks = response.results.slice(0, batch.length);
|
|
3588
3596
|
created.push(...blocks);
|
|
@@ -4086,6 +4094,12 @@ var NotionClient = class _NotionClient {
|
|
|
4086
4094
|
* 슬롯을 붙잡아 남을 막는 대신, 다 같이 보는 이 게이트로 쉬게 한다.
|
|
4087
4095
|
*/
|
|
4088
4096
|
cooldownUntil = 0;
|
|
4097
|
+
/**
|
|
4098
|
+
* Free 워크스페이스 블록 한도에 막힌 거절과 그 시각. 막힌 동안 블록을 만드는 요청은 보내지 않고 이 거절을 그대로
|
|
4099
|
+
* 돌려준다 — 플랜을 올리기 전에는 풀리지 않으므로 노트마다 같은 거절을 받으러 가지 않는다. 그 사이 플랜을
|
|
4100
|
+
* 올렸을 수 있어 {@link BLOCK_LIMIT_RECHECK_MS} 가 지나면 다시 보내 본다.
|
|
4101
|
+
*/
|
|
4102
|
+
blockLimit = null;
|
|
4089
4103
|
/**
|
|
4090
4104
|
* 요청 1건을 rate limit 슬롯 안에서 실행하고, 재시도 가능한 실패는 백오프 후 다시 시도한다.
|
|
4091
4105
|
*
|
|
@@ -4106,6 +4120,10 @@ var NotionClient = class _NotionClient {
|
|
|
4106
4120
|
*/
|
|
4107
4121
|
async withRateLimit(fn, options) {
|
|
4108
4122
|
const idempotent = options?.idempotent ?? true;
|
|
4123
|
+
if (options?.createsBlocks && this.blockLimit) {
|
|
4124
|
+
if (Date.now() - this.blockLimit.at < BLOCK_LIMIT_RECHECK_MS) throw this.blockLimit.error;
|
|
4125
|
+
this.blockLimit = null;
|
|
4126
|
+
}
|
|
4109
4127
|
this.requestsSent++;
|
|
4110
4128
|
let backoffMs = 0;
|
|
4111
4129
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -4115,6 +4133,12 @@ var NotionClient = class _NotionClient {
|
|
|
4115
4133
|
this.lastRequestTime = Date.now();
|
|
4116
4134
|
return await fn();
|
|
4117
4135
|
} catch (error) {
|
|
4136
|
+
if (isWorkspaceBlockLimit(error)) {
|
|
4137
|
+
const limited = new NotionBlockLimitError(error);
|
|
4138
|
+
this.blockLimit = { error: limited, at: Date.now() };
|
|
4139
|
+
getLogger().warn(`[Im-Nobsidian] ${limited.message}`);
|
|
4140
|
+
throw limited;
|
|
4141
|
+
}
|
|
4118
4142
|
if (!isRetryable(error, idempotent) || attempt >= this.maxRetries) throw error;
|
|
4119
4143
|
const retryAfterMs = extractRetryAfter(error);
|
|
4120
4144
|
backoffMs = retryAfterMs !== null ? retryAfterMs * (1 + Math.random() * 0.25) : this.retryBaseDelayMs * Math.pow(this.retryBackoffFactor, attempt) * (0.5 + Math.random() * 0.5);
|
|
@@ -4179,10 +4203,31 @@ function isAmbiguousFailure(error) {
|
|
|
4179
4203
|
return status === 500 || status === 502 || status === 503 || status === 504;
|
|
4180
4204
|
}
|
|
4181
4205
|
function isRequestBlocked(error) {
|
|
4182
|
-
|
|
4206
|
+
return rateLimitReasonOf(error) === "public_api_request_blocked";
|
|
4207
|
+
}
|
|
4208
|
+
function rateLimitReasonOf(error) {
|
|
4209
|
+
if (notionStatusOf(error) !== 429) return null;
|
|
4183
4210
|
const data = error.additional_data;
|
|
4184
|
-
return data?.rate_limit_reason === "
|
|
4211
|
+
return typeof data?.rate_limit_reason === "string" ? data.rate_limit_reason : null;
|
|
4185
4212
|
}
|
|
4213
|
+
function isWorkspaceBlockLimit(error) {
|
|
4214
|
+
if (notionStatusOf(error) !== 403) return false;
|
|
4215
|
+
const data = error.additional_data;
|
|
4216
|
+
return data?.block_limit !== void 0 && data.block_limit !== null;
|
|
4217
|
+
}
|
|
4218
|
+
var BLOCK_LIMIT_RECHECK_MS = 5 * 6e4;
|
|
4219
|
+
var NotionBlockLimitError = class extends Error {
|
|
4220
|
+
status = 403;
|
|
4221
|
+
code = "restricted_resource";
|
|
4222
|
+
constructor(cause) {
|
|
4223
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
4224
|
+
super(
|
|
4225
|
+
`Notion \uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4\uAC00 \uBB34\uB8CC \uBE14\uB85D \uD55C\uB3C4\uB97C \uB2E4 \uC368\uC11C \uC0C8 \uB0B4\uC6A9\uC744 \uB9CC\uB4E4 \uC218 \uC5C6\uC74C \u2014 Notion \uC5D0\uC11C \uD50C\uB79C\uC744 \uC62C\uB9AC\uBA74 \uD480\uB9B0\uB2E4 (Notion: ${reason})`,
|
|
4226
|
+
{ cause }
|
|
4227
|
+
);
|
|
4228
|
+
this.name = "NotionBlockLimitError";
|
|
4229
|
+
}
|
|
4230
|
+
};
|
|
4186
4231
|
function isRetryable(error, idempotent) {
|
|
4187
4232
|
if (isRequestBlocked(error)) return false;
|
|
4188
4233
|
return isExplicitRejection(error) || idempotent && isAmbiguousFailure(error);
|
|
@@ -4193,10 +4238,13 @@ function describeRetryCause(error) {
|
|
|
4193
4238
|
const parts = [];
|
|
4194
4239
|
if (typeof e.status === "number") parts.push(`status ${e.status}`);
|
|
4195
4240
|
if (typeof e.code === "string") parts.push(e.code);
|
|
4241
|
+
const reason = rateLimitReasonOf(error);
|
|
4242
|
+
if (reason) parts.push(reason);
|
|
4196
4243
|
if (typeof e.message === "string" && e.message) parts.push(e.message.slice(0, 120));
|
|
4197
4244
|
return parts.length > 0 ? parts.join(" \xB7 ") : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958";
|
|
4198
4245
|
}
|
|
4199
4246
|
var MAX_RETRY_AFTER_MS = 6e4;
|
|
4247
|
+
var MAX_SHARED_RETRY_AFTER_MS = 5 * 6e4;
|
|
4200
4248
|
function readHeader(headers, name) {
|
|
4201
4249
|
if (typeof headers !== "object" || headers === null) return null;
|
|
4202
4250
|
const get = headers.get;
|
|
@@ -4223,7 +4271,8 @@ function extractRetryAfter(error) {
|
|
|
4223
4271
|
ms = Number.isNaN(at) ? Number.NaN : at - Date.now();
|
|
4224
4272
|
}
|
|
4225
4273
|
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
4226
|
-
|
|
4274
|
+
const cap = rateLimitReasonOf(error) === "public_api_space_request_rate_limit" ? MAX_SHARED_RETRY_AFTER_MS : MAX_RETRY_AFTER_MS;
|
|
4275
|
+
return Math.min(ms, cap);
|
|
4227
4276
|
}
|
|
4228
4277
|
function sleep(ms) {
|
|
4229
4278
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
@@ -5061,6 +5110,7 @@ function deferredSyncedRefMarker(index) {
|
|
|
5061
5110
|
}
|
|
5062
5111
|
var TOGGLE_START = compactMarker("toggle:start");
|
|
5063
5112
|
var TOGGLE_END = compactMarker("toggle:end");
|
|
5113
|
+
var LARGE_FILE_MARKER = compactMarker("large-file");
|
|
5064
5114
|
var TOGGLE_HEADING_START = compactMarker("toggle-heading");
|
|
5065
5115
|
var TOGGLE_HEADING_END = compactMarker("toggle-heading:end");
|
|
5066
5116
|
function calloutIndentMarker(depth2) {
|
|
@@ -5143,7 +5193,6 @@ var MARKER_TOKEN_RE = new RegExp(
|
|
|
5143
5193
|
|
|
5144
5194
|
// src/converter/pipeline.ts
|
|
5145
5195
|
var INLINE_DB_PROBE = new RegExp(`%% ${MARKER_BRAND_RE}:inline-db`);
|
|
5146
|
-
var TOGGLE_HEADING_PROBE = new RegExp(`%% ${MARKER_BRAND_RE}:toggle-heading`);
|
|
5147
5196
|
var ConversionPipeline = class {
|
|
5148
5197
|
preProcessors = [];
|
|
5149
5198
|
postProcessors = [];
|
|
@@ -5155,11 +5204,14 @@ var ConversionPipeline = class {
|
|
|
5155
5204
|
this.postProcessors.push(processor);
|
|
5156
5205
|
this.postProcessors.sort((a, b) => a.order - b.order);
|
|
5157
5206
|
}
|
|
5207
|
+
/**
|
|
5208
|
+
* 토글 헤딩은 Markdown API 경로가 변환한다(`toggle-heading.ts`) — 블록 경로로 보내면 콜아웃이 옛 이모지 꼴로
|
|
5209
|
+
* 바뀐다(`CalloutTransformer`). 예전에 여기 있던 토글 헤딩 탐지는 띄어 쓴 마커를 찾아 한 번도 맞지 않았다.
|
|
5210
|
+
*/
|
|
5158
5211
|
selectPath(content) {
|
|
5159
5212
|
const hasInlineDb = INLINE_DB_PROBE.test(content);
|
|
5160
5213
|
const hasColumnLayout2 = />\s*\[!col\]/.test(content);
|
|
5161
|
-
|
|
5162
|
-
if (hasInlineDb || hasColumnLayout2 || hasToggleHeading) {
|
|
5214
|
+
if (hasInlineDb || hasColumnLayout2) {
|
|
5163
5215
|
return "block-api";
|
|
5164
5216
|
}
|
|
5165
5217
|
return "markdown-api";
|
|
@@ -5788,6 +5840,27 @@ function obsidianMathCloseAt(line, open) {
|
|
|
5788
5840
|
return close;
|
|
5789
5841
|
}
|
|
5790
5842
|
|
|
5843
|
+
// src/converter/media-labels.ts
|
|
5844
|
+
var MEDIA_LABELS = {
|
|
5845
|
+
audio: "\u{1F50A}",
|
|
5846
|
+
video: "\u{1F3AC}",
|
|
5847
|
+
pdf: "\u{1F4C4}",
|
|
5848
|
+
file: "\u{1F4CE}",
|
|
5849
|
+
html: "\u{1F310}"
|
|
5850
|
+
};
|
|
5851
|
+
var LEGACY_MEDIA_ICONS = ["\u{1F39E}"];
|
|
5852
|
+
var MEDIA_PLACEHOLDER_CAPTIONS = new Set(Object.keys(MEDIA_LABELS));
|
|
5853
|
+
function mediaLabel(kind, caption) {
|
|
5854
|
+
return `${MEDIA_LABELS[kind]} ${caption || kind}`;
|
|
5855
|
+
}
|
|
5856
|
+
function mediaLinkRe(kind) {
|
|
5857
|
+
return new RegExp(`\\[${MEDIA_LABELS[kind]}\\s*([^\\]]*)\\]\\(([^)]+)\\)`, "g");
|
|
5858
|
+
}
|
|
5859
|
+
var MEDIA_LABEL_PREFIX_RE = new RegExp(
|
|
5860
|
+
`^(?:${[...Object.values(MEDIA_LABELS), ...LEGACY_MEDIA_ICONS].join("|")})\\s*`,
|
|
5861
|
+
"u"
|
|
5862
|
+
);
|
|
5863
|
+
|
|
5791
5864
|
// src/converter/container-head.ts
|
|
5792
5865
|
var CALLOUT_HEAD_LINE_RE = /^([ \t]{0,3})> \[!(\w+)\]([-+])?[ \t]*(.*)$/;
|
|
5793
5866
|
var TOGGLE_HEAD_SOURCE = String.raw`^([ \t]{0,3})> \[!toggle\]-`;
|
|
@@ -5862,7 +5935,7 @@ var EmbedResolver = class {
|
|
|
5862
5935
|
(match, alt, url, offset) => {
|
|
5863
5936
|
if (code.some(([start, end]) => offset >= start && offset < end)) return match;
|
|
5864
5937
|
if (isVideoUrl(url)) {
|
|
5865
|
-
return `[
|
|
5938
|
+
return `[${mediaLabel("video", alt)}](${url})`;
|
|
5866
5939
|
}
|
|
5867
5940
|
if (isExternalUrl(url)) {
|
|
5868
5941
|
images.push({ url, isExternal: true });
|
|
@@ -7426,6 +7499,112 @@ var BlankLineCollapser = class {
|
|
|
7426
7499
|
}
|
|
7427
7500
|
};
|
|
7428
7501
|
|
|
7502
|
+
// src/converter/heading-markers.ts
|
|
7503
|
+
var MARKER = `%%${MARKER_BRAND_RE}:(?:block-color:[a-z_]+|toggle-heading)%%`;
|
|
7504
|
+
var MARKERS = `(?:${MARKER}[ \\t]*)+`;
|
|
7505
|
+
var HEADING_WITH_MARKERS_RE = new RegExp(`^([\\t ]*)(#{1,6}[ \\t].*?)[ \\t]+(${MARKERS})$`, "gm");
|
|
7506
|
+
var MARKER_LINE_AFTER_HEADING_RE = new RegExp(
|
|
7507
|
+
`^([\\t ]*#{1,6}[ \\t].*?)[ \\t]*\\n[\\t ]*(${MARKERS})$`,
|
|
7508
|
+
"gm"
|
|
7509
|
+
);
|
|
7510
|
+
var HEADING_LINE_RE = /^[\t ]*#{1,6}[ \t]/;
|
|
7511
|
+
var MARKER_LINE_RE = new RegExp(`^[\\t ]*${MARKERS}$`);
|
|
7512
|
+
function placeHeadingMarkersBelow(content) {
|
|
7513
|
+
return mapOutsideCodeFences(
|
|
7514
|
+
content,
|
|
7515
|
+
(segment) => segment.replace(
|
|
7516
|
+
HEADING_WITH_MARKERS_RE,
|
|
7517
|
+
(_m, indent, heading, markers) => `${indent}${heading}
|
|
7518
|
+
${indent}${markers.trim()}`
|
|
7519
|
+
)
|
|
7520
|
+
);
|
|
7521
|
+
}
|
|
7522
|
+
function liftHeadingMarkers(content) {
|
|
7523
|
+
return mapOutsideCodeFences(
|
|
7524
|
+
content,
|
|
7525
|
+
(segment) => segment.replace(
|
|
7526
|
+
MARKER_LINE_AFTER_HEADING_RE,
|
|
7527
|
+
(_m, heading, markers) => `${heading} ${markers.trim()}`
|
|
7528
|
+
)
|
|
7529
|
+
);
|
|
7530
|
+
}
|
|
7531
|
+
function isHeadingLine(line) {
|
|
7532
|
+
return HEADING_LINE_RE.test(line);
|
|
7533
|
+
}
|
|
7534
|
+
function isHeadingMarkerLine(line) {
|
|
7535
|
+
return MARKER_LINE_RE.test(line);
|
|
7536
|
+
}
|
|
7537
|
+
function sectionLinesWithHeadingMarkers(all, start, end) {
|
|
7538
|
+
const lines = all.slice(start, end + 1);
|
|
7539
|
+
const last = lines.length - 1;
|
|
7540
|
+
const next = all[end + 1];
|
|
7541
|
+
if (last >= 0 && next !== void 0 && isHeadingLine(lines[last]) && isHeadingMarkerLine(next)) {
|
|
7542
|
+
lines[last] = `${lines[last]} ${next.trim()}`;
|
|
7543
|
+
}
|
|
7544
|
+
return lines;
|
|
7545
|
+
}
|
|
7546
|
+
|
|
7547
|
+
// src/converter/pre-processors/heading-marker-lifter.ts
|
|
7548
|
+
var HeadingMarkerLifter = class {
|
|
7549
|
+
name = "HeadingMarkerLifter";
|
|
7550
|
+
order = 0;
|
|
7551
|
+
process(input) {
|
|
7552
|
+
if (input.context.direction !== "push") {
|
|
7553
|
+
return { content: input.content, metadata: input.metadata };
|
|
7554
|
+
}
|
|
7555
|
+
return { content: liftHeadingMarkers(input.content), metadata: input.metadata };
|
|
7556
|
+
}
|
|
7557
|
+
};
|
|
7558
|
+
|
|
7559
|
+
// src/converter/degrade-link.ts
|
|
7560
|
+
function degradeLink(label, url, marker) {
|
|
7561
|
+
return `[${label}](${url.replace(/\(/g, "%28").replace(/\)/g, "%29")})${marker}`;
|
|
7562
|
+
}
|
|
7563
|
+
var DEGRADE_LINK_SOURCE = "(?:\\[(?:\\\\.|[^\\\\\\]])*\\]\\([^)]*\\))?";
|
|
7564
|
+
function embedVaultLink(src, caption = "") {
|
|
7565
|
+
const label = caption.replace(/<[^>]*>/g, "").replace(/[[\]\\]/g, "").trim();
|
|
7566
|
+
return degradeLink(label === "" ? "\u{1F517} Embed" : `\u{1F517} ${label}`, src, embedMarker(src, caption));
|
|
7567
|
+
}
|
|
7568
|
+
|
|
7569
|
+
// src/converter/large-file-link.ts
|
|
7570
|
+
var LARGE_FILE_LINK_RE = new RegExp(` ?${DEGRADE_LINK_SOURCE}${LARGE_FILE_MARKER}`, "g");
|
|
7571
|
+
function notionFileLocation(pageId, blockId) {
|
|
7572
|
+
const page = `https://www.notion.so/${compactNotionId(pageId)}`;
|
|
7573
|
+
return blockId ? `${page}#${compactNotionId(blockId)}` : page;
|
|
7574
|
+
}
|
|
7575
|
+
function fileSizeLabel(bytes) {
|
|
7576
|
+
const mb = bytes / 1024 / 1024;
|
|
7577
|
+
return mb >= 1 ? `${Math.round(mb)}MB` : `${Math.max(1, Math.round(bytes / 1024))}KB`;
|
|
7578
|
+
}
|
|
7579
|
+
function largeFileLink(location, bytes) {
|
|
7580
|
+
return ` ${degradeLink(`\u2197 Notion \uC5D0\uC11C \uC5F4\uAE30 \xB7 ${fileSizeLabel(bytes)}`, location, LARGE_FILE_MARKER)}`;
|
|
7581
|
+
}
|
|
7582
|
+
function withLargeFileLink(content, fileLink, link) {
|
|
7583
|
+
const at = content.indexOf(fileLink);
|
|
7584
|
+
if (at < 0) return content;
|
|
7585
|
+
const end = at + fileLink.length;
|
|
7586
|
+
const sticky = new RegExp(LARGE_FILE_LINK_RE.source, "y");
|
|
7587
|
+
sticky.lastIndex = end;
|
|
7588
|
+
const existing = sticky.exec(content);
|
|
7589
|
+
const rest = existing ? end + existing[0].length : end;
|
|
7590
|
+
return `${content.slice(0, end)}${link}${content.slice(rest)}`;
|
|
7591
|
+
}
|
|
7592
|
+
function stripLargeFileLinks(content) {
|
|
7593
|
+
return content.includes(LARGE_FILE_MARKER) ? content.replace(LARGE_FILE_LINK_RE, "") : content;
|
|
7594
|
+
}
|
|
7595
|
+
|
|
7596
|
+
// src/converter/pre-processors/large-file-link-stripper.ts
|
|
7597
|
+
var LargeFileLinkStripper = class {
|
|
7598
|
+
name = "LargeFileLinkStripper";
|
|
7599
|
+
order = 0;
|
|
7600
|
+
process(input) {
|
|
7601
|
+
if (input.context.direction !== "push") {
|
|
7602
|
+
return { content: input.content, metadata: input.metadata };
|
|
7603
|
+
}
|
|
7604
|
+
return { content: stripLargeFileLinks(input.content), metadata: input.metadata };
|
|
7605
|
+
}
|
|
7606
|
+
};
|
|
7607
|
+
|
|
7429
7608
|
// src/utils/wikilink-title.ts
|
|
7430
7609
|
function wikilinkTitleFromPath(obsidianPath) {
|
|
7431
7610
|
return noteNameFromPath(obsidianPath);
|
|
@@ -7436,7 +7615,7 @@ function formatWikilink(target, label) {
|
|
|
7436
7615
|
|
|
7437
7616
|
// src/converter/link-restore.ts
|
|
7438
7617
|
var NOTION_LINK_REGEX = new RegExp(
|
|
7439
|
-
`\\[${MARKER_LABEL_CAPTURE}\\]\\((?:https:\\/\\/(?:www\\.)?notion\\.so\\/|notion:\\/\\/)([a-f0-9-]+)\\)(?!%%${MARKER_BRAND_RE}:(?:unknown|embed)
|
|
7618
|
+
`\\[${MARKER_LABEL_CAPTURE}\\]\\((?:https:\\/\\/(?:www\\.)?notion\\.so\\/|notion:\\/\\/)([a-f0-9-]+)\\)(?!%%${MARKER_BRAND_RE}:(?:unknown|embed):|${LARGE_FILE_MARKER})`,
|
|
7440
7619
|
"g"
|
|
7441
7620
|
);
|
|
7442
7621
|
var WIKILINK_PRESERVE_REGEX = new RegExp(
|
|
@@ -7701,7 +7880,7 @@ var PreserveMarkerInjector = class {
|
|
|
7701
7880
|
const rendered = this.renderMarker(m);
|
|
7702
7881
|
if (rendered === null) return false;
|
|
7703
7882
|
if (content.includes(rendered)) return false;
|
|
7704
|
-
return !this.
|
|
7883
|
+
return !this.restoredElsewhere(m);
|
|
7705
7884
|
});
|
|
7706
7885
|
if (missing.length === 0) {
|
|
7707
7886
|
return { content, metadata: input.metadata };
|
|
@@ -7736,15 +7915,11 @@ var PreserveMarkerInjector = class {
|
|
|
7736
7915
|
const body = entries.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
|
|
7737
7916
|
return spacedMarker(`${marker.type}:${body}`);
|
|
7738
7917
|
}
|
|
7739
|
-
/**
|
|
7740
|
-
|
|
7741
|
-
const params = marker.params ?? {};
|
|
7918
|
+
/** 다른 복원기가 맡는 마커 — 본문에 다시 끼우면 중복 · 오염이다. */
|
|
7919
|
+
restoredElsewhere(marker) {
|
|
7742
7920
|
switch (marker.type) {
|
|
7743
|
-
case "wikilink":
|
|
7744
|
-
|
|
7745
|
-
if (!text) return false;
|
|
7746
|
-
return content.includes(`[[${text}]]`) || content.includes(`[[${text}|`);
|
|
7747
|
-
}
|
|
7921
|
+
case "wikilink":
|
|
7922
|
+
return true;
|
|
7748
7923
|
case "local-image":
|
|
7749
7924
|
case "local-file":
|
|
7750
7925
|
return true;
|
|
@@ -8604,7 +8779,10 @@ var CommentRestorer = class {
|
|
|
8604
8779
|
if (input.context.direction !== "pull" || local === void 0) {
|
|
8605
8780
|
return { content: input.content, metadata: input.metadata };
|
|
8606
8781
|
}
|
|
8607
|
-
return {
|
|
8782
|
+
return {
|
|
8783
|
+
content: restoreComments(input.content, liftHeadingMarkers(local)),
|
|
8784
|
+
metadata: input.metadata
|
|
8785
|
+
};
|
|
8608
8786
|
}
|
|
8609
8787
|
};
|
|
8610
8788
|
|
|
@@ -8953,9 +9131,10 @@ function pieces(local, pulled) {
|
|
|
8953
9131
|
let j = 0;
|
|
8954
9132
|
const zip = (to) => {
|
|
8955
9133
|
const theirs = pulled.slice(j, to);
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
9134
|
+
const paired = Math.min(pending.length, theirs.length);
|
|
9135
|
+
for (let k = 0; k < paired; k++) out.push([[pending[k]], [theirs[k]]]);
|
|
9136
|
+
if (pending.length > paired) out.push([pending.slice(paired), []]);
|
|
9137
|
+
for (let k = paired; k < theirs.length; k++) out.push([[], [theirs[k]]]);
|
|
8959
9138
|
pending = [];
|
|
8960
9139
|
j = to;
|
|
8961
9140
|
};
|
|
@@ -9172,6 +9351,18 @@ function readable(note) {
|
|
|
9172
9351
|
}
|
|
9173
9352
|
}
|
|
9174
9353
|
|
|
9354
|
+
// src/converter/post-processors/heading-marker-placer.ts
|
|
9355
|
+
var HeadingMarkerPlacer = class {
|
|
9356
|
+
name = "HeadingMarkerPlacer";
|
|
9357
|
+
order = 140;
|
|
9358
|
+
process(input) {
|
|
9359
|
+
if (input.context.direction !== "pull") {
|
|
9360
|
+
return { content: input.content, metadata: input.metadata };
|
|
9361
|
+
}
|
|
9362
|
+
return { content: placeHeadingMarkersBelow(input.content), metadata: input.metadata };
|
|
9363
|
+
}
|
|
9364
|
+
};
|
|
9365
|
+
|
|
9175
9366
|
// src/converter/literal-escapes.ts
|
|
9176
9367
|
var ASCII_PUNCT_RE = /[!-/:-@[-`{-~]/;
|
|
9177
9368
|
var MATH_FENCE_LINE_RE = /^(?:[ \t]*>)*[ \t]*\$\$[ \t]*$/;
|
|
@@ -9752,6 +9943,8 @@ function findBodyEnd(lines, from, level) {
|
|
|
9752
9943
|
// src/utils/sanitize.ts
|
|
9753
9944
|
var INVALID_CHARS = /[<>:"/\\|?*\x00-\x1f]/g;
|
|
9754
9945
|
var WIKILINK_UNSAFE_CHARS = /[[\]]/g;
|
|
9946
|
+
var LINK_SUBPATH_CHARS = /[#^]/g;
|
|
9947
|
+
var TRAILING_DOTS_AND_SPACES = /[.\s]+$/;
|
|
9755
9948
|
var TRAILING_DOTS = /\.+$/;
|
|
9756
9949
|
var RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
9757
9950
|
"CON",
|
|
@@ -9777,15 +9970,38 @@ var RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
|
9777
9970
|
"LPT8",
|
|
9778
9971
|
"LPT9"
|
|
9779
9972
|
]);
|
|
9780
|
-
|
|
9781
|
-
|
|
9782
|
-
|
|
9783
|
-
|
|
9784
|
-
|
|
9785
|
-
|
|
9786
|
-
|
|
9973
|
+
var FILE_NAME_MAX_BYTES = 200;
|
|
9974
|
+
var FILE_SYSTEM_NAME_MAX_BYTES = 255;
|
|
9975
|
+
var utf8 = new TextEncoder();
|
|
9976
|
+
function utf8Bytes(text) {
|
|
9977
|
+
return utf8.encode(text).length;
|
|
9978
|
+
}
|
|
9979
|
+
function truncateBytes(name, maxBytes) {
|
|
9980
|
+
if (utf8.encode(name).length <= maxBytes) return name;
|
|
9981
|
+
let out = "";
|
|
9982
|
+
let bytes = 0;
|
|
9983
|
+
for (const { segment } of new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(
|
|
9984
|
+
name
|
|
9985
|
+
)) {
|
|
9986
|
+
const size = utf8.encode(segment).length;
|
|
9987
|
+
if (bytes + size > maxBytes) break;
|
|
9988
|
+
out += segment;
|
|
9989
|
+
bytes += size;
|
|
9787
9990
|
}
|
|
9788
|
-
return
|
|
9991
|
+
return out;
|
|
9992
|
+
}
|
|
9993
|
+
function guarded(name) {
|
|
9994
|
+
const safe = RESERVED_NAMES.has(name.toUpperCase()) ? `_${name}` : name;
|
|
9995
|
+
return safe.length === 0 ? "_untitled" : safe;
|
|
9996
|
+
}
|
|
9997
|
+
function sanitizeFileName(name) {
|
|
9998
|
+
const cleaned = name.replace(INVALID_CHARS, "_").replace(WIKILINK_UNSAFE_CHARS, "_").replace(LINK_SUBPATH_CHARS, "_").trim();
|
|
9999
|
+
return guarded(truncateBytes(cleaned, FILE_NAME_MAX_BYTES).replace(TRAILING_DOTS_AND_SPACES, ""));
|
|
10000
|
+
}
|
|
10001
|
+
function legacySanitizeFileName(name) {
|
|
10002
|
+
return guarded(
|
|
10003
|
+
name.replace(INVALID_CHARS, "_").replace(WIKILINK_UNSAFE_CHARS, "_").replace(TRAILING_DOTS, "").trim().slice(0, 200)
|
|
10004
|
+
);
|
|
9789
10005
|
}
|
|
9790
10006
|
|
|
9791
10007
|
// src/converter/nfm-attrs.ts
|
|
@@ -10289,16 +10505,6 @@ function breaksToEmptyBlocks(content) {
|
|
|
10289
10505
|
});
|
|
10290
10506
|
}
|
|
10291
10507
|
|
|
10292
|
-
// src/converter/degrade-link.ts
|
|
10293
|
-
function degradeLink(label, url, marker) {
|
|
10294
|
-
return `[${label}](${url.replace(/\(/g, "%28").replace(/\)/g, "%29")})${marker}`;
|
|
10295
|
-
}
|
|
10296
|
-
var DEGRADE_LINK_SOURCE = "(?:\\[(?:\\\\.|[^\\\\\\]])*\\]\\([^)]*\\))?";
|
|
10297
|
-
function embedVaultLink(src, caption = "") {
|
|
10298
|
-
const label = caption.replace(/<[^>]*>/g, "").replace(/[[\]\\]/g, "").trim();
|
|
10299
|
-
return degradeLink(label === "" ? "\u{1F517} Embed" : `\u{1F517} ${label}`, src, embedMarker(src, caption));
|
|
10300
|
-
}
|
|
10301
|
-
|
|
10302
10508
|
// src/converter/placeholder-indent.ts
|
|
10303
10509
|
var PLACEHOLDER_LINE_RE = new RegExp(`^([ \\t]*)${MEDIA_PLACEHOLDER_HEAD}`);
|
|
10304
10510
|
var LEADING_SPACE_RE3 = /^[ \t]*/;
|
|
@@ -11240,6 +11446,7 @@ var NOTION_AUDIO_RE = /<audio src="([^"]*)">([\s\S]*?)<\/audio>/g;
|
|
|
11240
11446
|
var NOTION_VIDEO_RE = /<video src="([^"]*)">([\s\S]*?)<\/video>/g;
|
|
11241
11447
|
var NOTION_PDF_RE = /<pdf src="([^"]*)">([\s\S]*?)<\/pdf>/g;
|
|
11242
11448
|
var NOTION_FILE_RE = /<file src="([^"]*)">([\s\S]*?)<\/file>/g;
|
|
11449
|
+
var NOTION_HOSTED_EMBED_RE = /<embed\s+src="([^"]*)"[^>]*>([^\n]*?)\s*<\/embed>/g;
|
|
11243
11450
|
var NOTION_TAB_RE = /<tab title="([^"]*)">([\s\S]*?)<\/tab>/g;
|
|
11244
11451
|
var NOTION_LABELED_PAGE_LINK_RE = new RegExp(
|
|
11245
11452
|
`\\[${MARKER_LABEL_CAPTURE}\\]\\(https?:\\/\\/(?:[a-z]+\\.)?notion\\.(?:so|com)\\/(?:p\\/)?([a-f0-9]{32}|[a-f0-9-]{36})\\)`,
|
|
@@ -11294,7 +11501,7 @@ function unescapeBrackets(content) {
|
|
|
11294
11501
|
);
|
|
11295
11502
|
}
|
|
11296
11503
|
function obsidianToNotionEnhanced(obsidian) {
|
|
11297
|
-
let result = obsidian;
|
|
11504
|
+
let result = liftHeadingMarkers(stripLargeFileLinks(obsidian));
|
|
11298
11505
|
result = quotedCodeToChildren(result);
|
|
11299
11506
|
result = breaksToEmptyBlocks(result);
|
|
11300
11507
|
result = pipeTablesToNfmTables(result);
|
|
@@ -11556,20 +11763,20 @@ ${indented}
|
|
|
11556
11763
|
function convertMediaTags(content) {
|
|
11557
11764
|
let result = content;
|
|
11558
11765
|
result = result.replace(NOTION_AUDIO_RE, (_match, src, caption) => {
|
|
11559
|
-
|
|
11560
|
-
return cap ? `[\u{1F50A} ${cap}](${src})` : `[\u{1F50A} audio](${src})`;
|
|
11766
|
+
return `[${mediaLabel("audio", caption.trim())}](${src})`;
|
|
11561
11767
|
});
|
|
11562
11768
|
result = result.replace(NOTION_VIDEO_RE, (_match, src, caption) => {
|
|
11563
|
-
|
|
11564
|
-
return cap ? `[\u{1F3AC} ${cap}](${src})` : `[\u{1F3AC} video](${src})`;
|
|
11769
|
+
return `[${mediaLabel("video", caption.trim())}](${src})`;
|
|
11565
11770
|
});
|
|
11566
11771
|
result = result.replace(NOTION_PDF_RE, (_match, src, caption) => {
|
|
11567
|
-
|
|
11568
|
-
return cap ? `[\u{1F4C4} ${cap}](${src})` : `[\u{1F4C4} pdf](${src})`;
|
|
11772
|
+
return `[${mediaLabel("pdf", caption.trim())}](${src})`;
|
|
11569
11773
|
});
|
|
11570
11774
|
result = result.replace(NOTION_FILE_RE, (_match, src, caption) => {
|
|
11571
|
-
|
|
11572
|
-
|
|
11775
|
+
return `[${mediaLabel("file", caption.trim())}](${src})`;
|
|
11776
|
+
});
|
|
11777
|
+
result = result.replace(NOTION_HOSTED_EMBED_RE, (match, src, caption) => {
|
|
11778
|
+
if (!isNotionHostedFileUrl(src)) return match;
|
|
11779
|
+
return `[${mediaLabel("html", caption.trim())}](${src})`;
|
|
11573
11780
|
});
|
|
11574
11781
|
return result;
|
|
11575
11782
|
}
|
|
@@ -11806,10 +12013,11 @@ $$`;
|
|
|
11806
12013
|
function unescapeNotionChars(content) {
|
|
11807
12014
|
return content.replace(/\\~/g, "~").replace(/\\\^/g, "^");
|
|
11808
12015
|
}
|
|
11809
|
-
var OBSIDIAN_MEDIA_AUDIO_RE =
|
|
11810
|
-
var OBSIDIAN_MEDIA_VIDEO_RE =
|
|
11811
|
-
var OBSIDIAN_MEDIA_PDF_RE =
|
|
11812
|
-
var OBSIDIAN_MEDIA_FILE_RE =
|
|
12016
|
+
var OBSIDIAN_MEDIA_AUDIO_RE = mediaLinkRe("audio");
|
|
12017
|
+
var OBSIDIAN_MEDIA_VIDEO_RE = mediaLinkRe("video");
|
|
12018
|
+
var OBSIDIAN_MEDIA_PDF_RE = mediaLinkRe("pdf");
|
|
12019
|
+
var OBSIDIAN_MEDIA_FILE_RE = mediaLinkRe("file");
|
|
12020
|
+
var OBSIDIAN_MEDIA_HTML_RE = mediaLinkRe("html");
|
|
11813
12021
|
function restoreMediaTags(content) {
|
|
11814
12022
|
let result = content;
|
|
11815
12023
|
const restore = (tag) => (_match, caption, src) => {
|
|
@@ -11820,6 +12028,10 @@ function restoreMediaTags(content) {
|
|
|
11820
12028
|
result = result.replace(OBSIDIAN_MEDIA_VIDEO_RE, restore("video"));
|
|
11821
12029
|
result = result.replace(OBSIDIAN_MEDIA_PDF_RE, restore("pdf"));
|
|
11822
12030
|
result = result.replace(OBSIDIAN_MEDIA_FILE_RE, restore("file"));
|
|
12031
|
+
result = result.replace(OBSIDIAN_MEDIA_HTML_RE, (_match, caption, src) => {
|
|
12032
|
+
const cap = caption.trim() === "html" ? "" : caption;
|
|
12033
|
+
return `<embed src="${src}">${cap}</embed>`;
|
|
12034
|
+
});
|
|
11823
12035
|
return result;
|
|
11824
12036
|
}
|
|
11825
12037
|
var OBSIDIAN_TAB_RE = /^> \[!tab\]\s*(.+)((?:\n> .*)*)/gm;
|
|
@@ -12090,6 +12302,8 @@ function sortKeys(value) {
|
|
|
12090
12302
|
// src/converter/pipeline-factory.ts
|
|
12091
12303
|
function createDefaultPipeline(options) {
|
|
12092
12304
|
const pipeline = new ConversionPipeline();
|
|
12305
|
+
pipeline.registerPreProcessor(new HeadingMarkerLifter());
|
|
12306
|
+
pipeline.registerPreProcessor(new LargeFileLinkStripper());
|
|
12093
12307
|
pipeline.registerPreProcessor(new InlineAnnotationPreserver());
|
|
12094
12308
|
pipeline.registerPreProcessor(new UnsupportedBlockStripper());
|
|
12095
12309
|
pipeline.registerPreProcessor(new CommentStripper());
|
|
@@ -12120,6 +12334,7 @@ function createDefaultPipeline(options) {
|
|
|
12120
12334
|
pipeline.registerPostProcessor(
|
|
12121
12335
|
new LocalFormRestorer((note, context, children) => sentForm(pipeline, note, context, children))
|
|
12122
12336
|
);
|
|
12337
|
+
pipeline.registerPostProcessor(new HeadingMarkerPlacer());
|
|
12123
12338
|
return pipeline;
|
|
12124
12339
|
}
|
|
12125
12340
|
|
|
@@ -12227,6 +12442,11 @@ function isNotionUploadable(path) {
|
|
|
12227
12442
|
const ext = /\.[^.]+$/.exec(name)?.[0].toLowerCase();
|
|
12228
12443
|
return ext !== void 0 && NOTION_UPLOADABLE_EXTENSIONS.has(ext);
|
|
12229
12444
|
}
|
|
12445
|
+
var HTML_BLOCK_EXTENSIONS = /* @__PURE__ */ new Set([".html", ".htm"]);
|
|
12446
|
+
function isHtmlBlockFile(path) {
|
|
12447
|
+
const ext = /\.[^./]+$/.exec(path)?.[0].toLowerCase();
|
|
12448
|
+
return ext !== void 0 && HTML_BLOCK_EXTENSIONS.has(ext);
|
|
12449
|
+
}
|
|
12230
12450
|
function getBlockType(filename) {
|
|
12231
12451
|
const ext = filename.match(/\.[^.]+$/)?.[0]?.toLowerCase();
|
|
12232
12452
|
if (!ext) return "file";
|
|
@@ -12354,10 +12574,15 @@ function toWikilinkAlias(label) {
|
|
|
12354
12574
|
return label.replace(/!?\[([^\]]*)\]\(([^)]*)\)/g, "$1").replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, (_m, t, d) => d ?? t).replace(/[\r\n]+/g, " ").replace(/\\([!-/:-@[-`{-~])/g, "$1").replace(/\]\]/g, ")").replace(/\|/g, "-").trim();
|
|
12355
12575
|
}
|
|
12356
12576
|
var INTERNAL_FILE_URL_RE = /^file:\/\/%7B.*%7D%7D$/;
|
|
12357
|
-
var
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12577
|
+
var MEDIA_TAG_NAMES = "file|video|audio|pdf|embed";
|
|
12578
|
+
function mediaTagRe(src) {
|
|
12579
|
+
return new RegExp(
|
|
12580
|
+
`<(?:${MEDIA_TAG_NAMES})\\s+src="(${src})">([\\s\\S]*?)<\\/(?:${MEDIA_TAG_NAMES})>`,
|
|
12581
|
+
"g"
|
|
12582
|
+
);
|
|
12583
|
+
}
|
|
12584
|
+
var MEDIA_TAG_RE = mediaTagRe('[^"]+');
|
|
12585
|
+
var MEDIA_TAG_NAME_RE = new RegExp(`^<(${MEDIA_TAG_NAMES})\\s`);
|
|
12361
12586
|
var PLACEHOLDER_IMAGE_STEMS = /* @__PURE__ */ new Set(["image", "untitled"]);
|
|
12362
12587
|
var EMBED_SIZE_RE = /^\d+(?:x\d+)?$/;
|
|
12363
12588
|
function splitEmbedTarget(target) {
|
|
@@ -12381,8 +12606,8 @@ function embedTargetFromCaption(target, caption, uploadedName) {
|
|
|
12381
12606
|
return description ? sized(`${path}|${alias}`) : `${path}|${alias}`;
|
|
12382
12607
|
}
|
|
12383
12608
|
function captionOfLabel(label) {
|
|
12384
|
-
const caption = label.replace(
|
|
12385
|
-
return
|
|
12609
|
+
const caption = label.replace(MEDIA_LABEL_PREFIX_RE, "").replace(/\\(.)/g, "$1").trim();
|
|
12610
|
+
return MEDIA_LABEL_PREFIX_RE.test(label) && MEDIA_PLACEHOLDER_CAPTIONS.has(caption) ? "" : caption;
|
|
12386
12611
|
}
|
|
12387
12612
|
function fileNameOf2(caption, url) {
|
|
12388
12613
|
return savedNameOf(caption, uploadedFileNameFromUrl(url));
|
|
@@ -12409,7 +12634,7 @@ function isNotionMediaUrl(url) {
|
|
|
12409
12634
|
function notionMediaSpans(markdown) {
|
|
12410
12635
|
const spans = [];
|
|
12411
12636
|
for (const link of scanMarkdownLinks(markdown)) {
|
|
12412
|
-
if (!link.isEmbed && !
|
|
12637
|
+
if (!link.isEmbed && !MEDIA_LABEL_PREFIX_RE.test(link.label)) continue;
|
|
12413
12638
|
if (isNotionMediaUrl(link.url)) spans.push(link);
|
|
12414
12639
|
}
|
|
12415
12640
|
for (const tag of markdown.matchAll(MEDIA_TAG_RE)) {
|
|
@@ -12481,6 +12706,7 @@ function mediaBlock2(kind, fileUploadId, vaultTarget, target) {
|
|
|
12481
12706
|
const description = captionOfEmbed(target);
|
|
12482
12707
|
const caption = description ? [{ type: "text", text: { content: description } }] : [];
|
|
12483
12708
|
const file = { type: "file_upload", file_upload: { id: fileUploadId }, caption };
|
|
12709
|
+
if (kind !== "image" && isHtmlBlockFile(vaultTarget)) return { type: "embed", embed: file };
|
|
12484
12710
|
const type = kind === "image" ? "image" : getBlockType(baseName(vaultTarget));
|
|
12485
12711
|
return type === "file" ? { type, file: { ...file, name: baseName(vaultTarget) } } : { type, [type]: file };
|
|
12486
12712
|
}
|
|
@@ -13125,17 +13351,15 @@ var ImageHandler = class {
|
|
|
13125
13351
|
}
|
|
13126
13352
|
async downloadAllFiles(markdown, pageTitle, pageId) {
|
|
13127
13353
|
const fileLinks = scanMarkdownLinks(markdown).filter(
|
|
13128
|
-
(s) => !s.isEmbed &&
|
|
13354
|
+
(s) => !s.isEmbed && MEDIA_LABEL_PREFIX_RE.test(s.label)
|
|
13129
13355
|
);
|
|
13130
13356
|
const notionHttpMatches = fileLinks.filter(
|
|
13131
13357
|
(s) => /^https?:\/\//.test(s.url) && this.isNotionFileUrl(s.url)
|
|
13132
13358
|
);
|
|
13133
13359
|
const mdInternalMatches = fileLinks.filter((s) => INTERNAL_FILE_URL_RE.test(s.url));
|
|
13134
|
-
const
|
|
13135
|
-
const xmlHttpMatches = [...markdown.matchAll(xmlHttpRegex)];
|
|
13360
|
+
const xmlHttpMatches = [...markdown.matchAll(mediaTagRe('https?:\\/\\/[^"]+'))];
|
|
13136
13361
|
const notionXmlHttpMatches = xmlHttpMatches.filter((m) => m[1] && this.isNotionFileUrl(m[1]));
|
|
13137
|
-
const
|
|
13138
|
-
const xmlInternalMatches = [...markdown.matchAll(xmlInternalRegex)];
|
|
13362
|
+
const xmlInternalMatches = [...markdown.matchAll(mediaTagRe("file:\\/\\/%7B.*?%7D%7D"))];
|
|
13139
13363
|
const totalMatches = notionHttpMatches.length + mdInternalMatches.length + notionXmlHttpMatches.length + xmlInternalMatches.length;
|
|
13140
13364
|
if (totalMatches === 0) {
|
|
13141
13365
|
return { content: markdown, downloads: [] };
|
|
@@ -13147,9 +13371,12 @@ var ImageHandler = class {
|
|
|
13147
13371
|
for (const span of notionHttpMatches) {
|
|
13148
13372
|
await sema.acquire();
|
|
13149
13373
|
try {
|
|
13150
|
-
const caption = fileNameOf2(span.label.replace(
|
|
13374
|
+
const caption = fileNameOf2(span.label.replace(MEDIA_LABEL_PREFIX_RE, ""), span.url);
|
|
13151
13375
|
const download = await this.downloadFile(span.url, pageTitle, caption);
|
|
13152
|
-
if (!download.localPath)
|
|
13376
|
+
if (!download.localPath) {
|
|
13377
|
+
result = keepLargeFile(result, span.full, download, pageId);
|
|
13378
|
+
continue;
|
|
13379
|
+
}
|
|
13153
13380
|
downloads.push(download);
|
|
13154
13381
|
const obsidianLink = fileEmbedOf(download.localPath, download.name ?? caption);
|
|
13155
13382
|
result = result.replace(span.full, () => obsidianLink);
|
|
@@ -13178,9 +13405,12 @@ var ImageHandler = class {
|
|
|
13178
13405
|
getLogger().warn(`[Im-Nobsidian] \uD30C\uC77C Block URL \uC870\uD68C \uC2E4\uD328: ${fileName2}`);
|
|
13179
13406
|
continue;
|
|
13180
13407
|
}
|
|
13181
|
-
const caption = savedNameOf(span.label.replace(
|
|
13408
|
+
const caption = savedNameOf(span.label.replace(MEDIA_LABEL_PREFIX_RE, ""), fileName2);
|
|
13182
13409
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
13183
|
-
if (!download.localPath)
|
|
13410
|
+
if (!download.localPath) {
|
|
13411
|
+
result = keepLargeFile(result, span.full, download, pageId, blockId);
|
|
13412
|
+
continue;
|
|
13413
|
+
}
|
|
13184
13414
|
downloads.push(download);
|
|
13185
13415
|
const obsidianLink = fileEmbedOf(download.localPath, download.name ?? caption);
|
|
13186
13416
|
result = result.replace(span.full, () => obsidianLink);
|
|
@@ -13203,7 +13433,10 @@ var ImageHandler = class {
|
|
|
13203
13433
|
const url = match[1];
|
|
13204
13434
|
const caption = fileNameOf2(match[2] ?? "", url);
|
|
13205
13435
|
const download = await this.downloadFile(url, pageTitle, caption);
|
|
13206
|
-
if (!download.localPath)
|
|
13436
|
+
if (!download.localPath) {
|
|
13437
|
+
result = keepLargeFile(result, match[0], download, pageId);
|
|
13438
|
+
continue;
|
|
13439
|
+
}
|
|
13207
13440
|
downloads.push(download);
|
|
13208
13441
|
const obsidianLink = fileEmbedOf(download.localPath, download.name ?? caption);
|
|
13209
13442
|
result = result.replace(match[0], () => obsidianLink);
|
|
@@ -13235,7 +13468,10 @@ var ImageHandler = class {
|
|
|
13235
13468
|
}
|
|
13236
13469
|
const caption = savedNameOf(match[2] ?? "", fileName2);
|
|
13237
13470
|
const download = await this.downloadFile(realUrl, pageTitle, caption);
|
|
13238
|
-
if (!download.localPath)
|
|
13471
|
+
if (!download.localPath) {
|
|
13472
|
+
result = keepLargeFile(result, match[0], download, pageId, blockId);
|
|
13473
|
+
continue;
|
|
13474
|
+
}
|
|
13239
13475
|
downloads.push(download);
|
|
13240
13476
|
const obsidianLink = fileEmbedOf(download.localPath, download.name ?? caption);
|
|
13241
13477
|
result = result.replace(match[0], () => obsidianLink);
|
|
@@ -13286,18 +13522,21 @@ var ImageHandler = class {
|
|
|
13286
13522
|
const contentLength = response.headers.get("content-length");
|
|
13287
13523
|
const MAX_FILE_SIZE = this.maxFileSizeBytes;
|
|
13288
13524
|
if (contentLength && parseInt(contentLength, 10) > MAX_FILE_SIZE) {
|
|
13289
|
-
const
|
|
13525
|
+
const bytes = parseInt(contentLength, 10);
|
|
13290
13526
|
getLogger().info(
|
|
13291
|
-
`[Im-Nobsidian] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uAC74\uB108\uB700 \u2014 \uD06C\uAE30 \uC0C1\uD55C \uCD08\uACFC (${
|
|
13527
|
+
`[Im-Nobsidian] \uD30C\uC77C \uB2E4\uC6B4\uB85C\uB4DC \uAC74\uB108\uB700 \u2014 \uD06C\uAE30 \uC0C1\uD55C \uCD08\uACFC (${fileSizeLabel(bytes)} > ${fileSizeLabel(MAX_FILE_SIZE)}), Notion \uC5D0\uC11C \uC5F4\uAE30 \uB9C1\uD06C\uB97C \uB460: ${caption}`
|
|
13292
13528
|
);
|
|
13293
|
-
return { originalUrl: url, localPath: "", hash: "", size: 0 };
|
|
13529
|
+
return { originalUrl: url, localPath: "", hash: "", size: 0, skippedBytes: bytes };
|
|
13294
13530
|
}
|
|
13295
13531
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
13296
13532
|
const fileHash = createHash("sha256").update(buffer).digest("hex");
|
|
13297
13533
|
const hash = nameHashOf(fileHash);
|
|
13298
13534
|
const extension = this.guessFileExtension(url, response.headers.get("content-type"));
|
|
13299
13535
|
const name = withoutOwnHash(caption, hash);
|
|
13300
|
-
const safeName =
|
|
13536
|
+
const safeName = truncateBytes(
|
|
13537
|
+
name.replace(/[^a-zA-Z0-9가-힣\s._-]/g, "").replace(/\s+/g, "-").slice(0, 80),
|
|
13538
|
+
FILE_SYSTEM_NAME_MAX_BYTES - utf8Bytes(`-${hash}${extension}`)
|
|
13539
|
+
);
|
|
13301
13540
|
const fileName2 = `${safeName}-${hash}${extension}`;
|
|
13302
13541
|
const localPath = `${this.attachmentFolder}/${fileName2}`;
|
|
13303
13542
|
await this.vaultFs.ensureFolder(this.attachmentFolder);
|
|
@@ -13359,6 +13598,11 @@ var ImageHandler = class {
|
|
|
13359
13598
|
return "";
|
|
13360
13599
|
}
|
|
13361
13600
|
};
|
|
13601
|
+
function keepLargeFile(content, fileLink, download, pageId, blockId) {
|
|
13602
|
+
if (download.skippedBytes === void 0 || !pageId) return content;
|
|
13603
|
+
const link = largeFileLink(notionFileLocation(pageId, blockId), download.skippedBytes);
|
|
13604
|
+
return withLargeFileLink(content, fileLink, link);
|
|
13605
|
+
}
|
|
13362
13606
|
|
|
13363
13607
|
// src/view/notion-filter-translator.ts
|
|
13364
13608
|
function translateNotionFilter(filter, schema, resolveName) {
|
|
@@ -13688,6 +13932,10 @@ function filterGroup(node) {
|
|
|
13688
13932
|
return ["not", node.not];
|
|
13689
13933
|
}
|
|
13690
13934
|
var BASE_FILE_HEADER = "# Auto-generated by Im-Nobsidian";
|
|
13935
|
+
function inFolderFilter(folderPath) {
|
|
13936
|
+
return `file.inFolder("${folderPath}")`;
|
|
13937
|
+
}
|
|
13938
|
+
var IN_FOLDER_FILTER_RE = /file\.inFolder\("([^"\n]*)"\)/g;
|
|
13691
13939
|
var BaseFileGenerator = class {
|
|
13692
13940
|
generate(options) {
|
|
13693
13941
|
const lines = [];
|
|
@@ -13703,7 +13951,7 @@ var BaseFileGenerator = class {
|
|
|
13703
13951
|
writeFilters(lines, folderPath) {
|
|
13704
13952
|
lines.push("filters:");
|
|
13705
13953
|
lines.push(" and:");
|
|
13706
|
-
lines.push(` -
|
|
13954
|
+
lines.push(` - ${inFolderFilter(folderPath)}`);
|
|
13707
13955
|
lines.push(` - file.ext == "md"`);
|
|
13708
13956
|
lines.push("");
|
|
13709
13957
|
}
|
|
@@ -14541,9 +14789,9 @@ function noteTitle(frontmatter, path) {
|
|
|
14541
14789
|
return explicitTitle(frontmatter) ?? wikilinkTitleFromPath(path);
|
|
14542
14790
|
}
|
|
14543
14791
|
function isFileNameOf(title, fileName2) {
|
|
14544
|
-
|
|
14545
|
-
|
|
14546
|
-
|
|
14792
|
+
return [sanitizeFileName(title), legacySanitizeFileName(title)].some(
|
|
14793
|
+
(safe) => fileName2 === safe || fileName2.startsWith(safe) && /^ \([0-9a-f]{8,32}\)$/i.test(fileName2.slice(safe.length))
|
|
14794
|
+
);
|
|
14547
14795
|
}
|
|
14548
14796
|
function followsFileName(title, path) {
|
|
14549
14797
|
return isFileNameOf(title, wikilinkTitleFromPath(path));
|
|
@@ -14571,8 +14819,11 @@ function titleMayChange(base, current, from, to) {
|
|
|
14571
14819
|
function extractTitle(filePath) {
|
|
14572
14820
|
return noteNameFromPath(filePath);
|
|
14573
14821
|
}
|
|
14574
|
-
function titleProperty(title) {
|
|
14575
|
-
|
|
14822
|
+
function titleProperty(title, remote) {
|
|
14823
|
+
const runs = remote ? titleRunsOf(remote) : null;
|
|
14824
|
+
return {
|
|
14825
|
+
title: { title: (runs && patchRichText(runs, title)) ?? [{ text: { content: title } }] }
|
|
14826
|
+
};
|
|
14576
14827
|
}
|
|
14577
14828
|
function extractAliases(properties) {
|
|
14578
14829
|
const raw = properties.aliases ?? properties.alias;
|
|
@@ -14599,10 +14850,14 @@ function selectDbRowFiles(files, localFolder) {
|
|
|
14599
14850
|
return rel.length > 0 && !rel.includes("/");
|
|
14600
14851
|
});
|
|
14601
14852
|
}
|
|
14853
|
+
var PAGE_ID_SUFFIX_LENGTHS = [8, 16, 32];
|
|
14602
14854
|
function pagePathCandidates(dir, safeName, pageId) {
|
|
14603
14855
|
const base = dir ? `${dir}/${safeName}` : safeName;
|
|
14604
14856
|
const raw = pageId.replace(/-/g, "");
|
|
14605
|
-
return [
|
|
14857
|
+
return [
|
|
14858
|
+
`${base}.md`,
|
|
14859
|
+
...PAGE_ID_SUFFIX_LENGTHS.map((len) => `${base} (${raw.slice(0, len)}).md`)
|
|
14860
|
+
];
|
|
14606
14861
|
}
|
|
14607
14862
|
function resolveDbRowPath(localFolder, safeName, pageId, lookupByPath) {
|
|
14608
14863
|
const ownedBySelf = (candidate) => {
|
|
@@ -14613,9 +14868,9 @@ function resolveDbRowPath(localFolder, safeName, pageId, lookupByPath) {
|
|
|
14613
14868
|
const candidates = pagePathCandidates(localFolder, safeName, pageId);
|
|
14614
14869
|
return candidates.find(ownedBySelf) ?? candidates[candidates.length - 1];
|
|
14615
14870
|
}
|
|
14616
|
-
function inClaimOrder(items, chosen,
|
|
14871
|
+
function inClaimOrder(items, chosen, idOf2) {
|
|
14617
14872
|
const slots = items.flatMap((item, index) => chosen(item) ? [index] : []);
|
|
14618
|
-
const key = (item) => compactNotionId(
|
|
14873
|
+
const key = (item) => compactNotionId(idOf2(item));
|
|
14619
14874
|
const members = slots.map((index) => items[index]).sort((a, b) => key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
|
|
14620
14875
|
const ordered = [...items];
|
|
14621
14876
|
slots.forEach((slot, k) => ordered[slot] = members[k]);
|
|
@@ -14979,8 +15234,14 @@ var DbBaseFiles = class {
|
|
|
14979
15234
|
const entries = this.load();
|
|
14980
15235
|
const key = compactNotionId(databaseId);
|
|
14981
15236
|
const known = entries.get(key);
|
|
14982
|
-
if (known?.basePath === info.basePath && known.title === info.title)
|
|
14983
|
-
|
|
15237
|
+
if (known?.basePath === info.basePath && known.title === info.title && known.hash === info.hash) {
|
|
15238
|
+
return;
|
|
15239
|
+
}
|
|
15240
|
+
entries.set(key, {
|
|
15241
|
+
basePath: info.basePath,
|
|
15242
|
+
title: info.title,
|
|
15243
|
+
...info.hash ? { hash: info.hash } : {}
|
|
15244
|
+
});
|
|
14984
15245
|
this.loadedRaw = JSON.stringify(Object.fromEntries(entries));
|
|
14985
15246
|
this.stateDb.setMeta(DB_BASE_FILES_META_KEY, this.loadedRaw);
|
|
14986
15247
|
}
|
|
@@ -14997,7 +15258,11 @@ var DbBaseFiles = class {
|
|
|
14997
15258
|
for (const [key, value] of Object.entries(parsed)) {
|
|
14998
15259
|
const entry = value;
|
|
14999
15260
|
if (typeof entry?.basePath === "string" && typeof entry.title === "string") {
|
|
15000
|
-
this.entries.set(compactNotionId(key), {
|
|
15261
|
+
this.entries.set(compactNotionId(key), {
|
|
15262
|
+
basePath: entry.basePath,
|
|
15263
|
+
title: entry.title,
|
|
15264
|
+
...typeof entry.hash === "string" ? { hash: entry.hash } : {}
|
|
15265
|
+
});
|
|
15001
15266
|
}
|
|
15002
15267
|
}
|
|
15003
15268
|
} catch {
|
|
@@ -15082,9 +15347,9 @@ async function retargetBaseFiles(vaultFs, moves) {
|
|
|
15082
15347
|
if (!file.path.endsWith(BASE_SUFFIX)) continue;
|
|
15083
15348
|
const content = await vaultFs.readFile(file.path);
|
|
15084
15349
|
if (!isGeneratedDbArtifact(file.path, content)) continue;
|
|
15085
|
-
const next = content.replace(
|
|
15350
|
+
const next = content.replace(IN_FOLDER_FILTER_RE, (whole, folder) => {
|
|
15086
15351
|
const to = finalPath(folder, folders);
|
|
15087
|
-
return to === folder ? whole :
|
|
15352
|
+
return to === folder ? whole : inFolderFilter(to);
|
|
15088
15353
|
});
|
|
15089
15354
|
if (next !== content) await vaultFs.writeFile(file.path, next);
|
|
15090
15355
|
}
|
|
@@ -15094,7 +15359,6 @@ async function retargetBaseFiles(vaultFs, moves) {
|
|
|
15094
15359
|
);
|
|
15095
15360
|
}
|
|
15096
15361
|
}
|
|
15097
|
-
var IN_FOLDER = /file\.inFolder\("([^"\n]*)"\)/g;
|
|
15098
15362
|
function finalPath(path, moves) {
|
|
15099
15363
|
let current = path;
|
|
15100
15364
|
for (const move of moves) {
|
|
@@ -15660,7 +15924,7 @@ var DatabaseSyncer = class {
|
|
|
15660
15924
|
*/
|
|
15661
15925
|
appearances = /* @__PURE__ */ new WeakMap();
|
|
15662
15926
|
baseFileGenerator = new BaseFileGenerator();
|
|
15663
|
-
/** 사용자가
|
|
15927
|
+
/** 사용자가 만들거나 고친 `.base` 라 덮어쓰지 않았다고 이미 알린 경로 — 한 pull 이 같은 DB 를 여러 번 다시 쓴다. */
|
|
15664
15928
|
keptUserBases = /* @__PURE__ */ new Set();
|
|
15665
15929
|
sidecarGenerator = new SidecarGenerator();
|
|
15666
15930
|
/** Notion 에서 바꾼 제목으로 이름을 바꾼 행과 실제로 옮긴 차례 — pull 마감이 가져가 다른 노트의 링크를 고친다. */
|
|
@@ -15669,6 +15933,8 @@ var DatabaseSyncer = class {
|
|
|
15669
15933
|
renamed: [],
|
|
15670
15934
|
applied: []
|
|
15671
15935
|
};
|
|
15936
|
+
/** 볼트에서 사라져 되살린 행의 경로 — pull 마감이 가져가 화면에 알린다(D8). */
|
|
15937
|
+
restoredRows = [];
|
|
15672
15938
|
/**
|
|
15673
15939
|
* 실제로 기록한 .base 경로/DB 제목 — placeholder 임베드 재작성(F22)의 SSOT. 이번 pull 이 조회하지
|
|
15674
15940
|
* 않은 DB 의 것도 남아 있다({@link DbBaseFiles}).
|
|
@@ -15786,8 +16052,10 @@ var DatabaseSyncer = class {
|
|
|
15786
16052
|
created++;
|
|
15787
16053
|
if (outcome.action === "written") writtenPaths.push(outcome.path);
|
|
15788
16054
|
} else if (outcome.action === "written") {
|
|
15789
|
-
if (action === "restore")
|
|
15790
|
-
|
|
16055
|
+
if (action === "restore") {
|
|
16056
|
+
restored++;
|
|
16057
|
+
this.restoredRows.push(outcome.path);
|
|
16058
|
+
} else updated++;
|
|
15791
16059
|
writtenPaths.push(outcome.path);
|
|
15792
16060
|
} else if (outcome.action === "conflict") {
|
|
15793
16061
|
conflicts.push(outcome.conflict);
|
|
@@ -16136,17 +16404,23 @@ var DatabaseSyncer = class {
|
|
|
16136
16404
|
});
|
|
16137
16405
|
const safeName = sanitizeFileName(dbName);
|
|
16138
16406
|
const basePath = `${dbConfig.localFolder}/${safeName}${BASE_SUFFIX}`;
|
|
16139
|
-
|
|
16407
|
+
const previous = this.baseFileInfo.get(dbConfig.databaseId);
|
|
16408
|
+
const existing = await this.vaultFs.readFile(basePath).catch(() => null);
|
|
16409
|
+
let hash = computeHash(baseContent);
|
|
16410
|
+
if (existing !== null && !isGeneratedDbArtifact(basePath, existing)) {
|
|
16140
16411
|
if (!this.keptUserBases.has(basePath)) {
|
|
16141
16412
|
this.keptUserBases.add(basePath);
|
|
16142
16413
|
getLogger().info(
|
|
16143
16414
|
`[DB Sync] \uC9C1\uC811 \uB9CC\uB4E0 .base \uB77C Notion \uBCF4\uAE30\uB85C \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC74C: ${basePath} \u2014 Notion \uBCF4\uAE30\uB97C \uBC1B\uC73C\uB824\uBA74 \uC774 \uD30C\uC77C\uC758 \uC774\uB984\uC744 \uBC14\uAFB8\uC138\uC694`
|
|
16144
16415
|
);
|
|
16145
16416
|
}
|
|
16146
|
-
} else {
|
|
16417
|
+
} else if (existing !== null && existing !== baseContent && editedSince(previous, basePath, existing)) {
|
|
16418
|
+
hash = previous.hash;
|
|
16419
|
+
this.warnEditedBase(basePath);
|
|
16420
|
+
} else if (existing !== baseContent) {
|
|
16147
16421
|
await this.vaultFs.writeFile(basePath, baseContent);
|
|
16148
16422
|
}
|
|
16149
|
-
this.baseFileInfo.set(dbConfig.databaseId, { basePath, title: dbName });
|
|
16423
|
+
this.baseFileInfo.set(dbConfig.databaseId, { basePath, title: dbName, hash });
|
|
16150
16424
|
getLogger().debug(`[DB Sync] .base \uD30C\uC77C \uC0DD\uC131: ${basePath}`);
|
|
16151
16425
|
await this.generateSidecar(
|
|
16152
16426
|
dbConfig,
|
|
@@ -16157,7 +16431,7 @@ var DatabaseSyncer = class {
|
|
|
16157
16431
|
meta.isInline,
|
|
16158
16432
|
await this.appearanceOf(meta, safeName)
|
|
16159
16433
|
);
|
|
16160
|
-
await this.cleanupStaleDbArtifacts(dbConfig.localFolder, safeName);
|
|
16434
|
+
await this.cleanupStaleDbArtifacts(dbConfig.localFolder, safeName, previous);
|
|
16161
16435
|
} catch (error) {
|
|
16162
16436
|
getLogger().warn(`[DB Sync] .base \uD30C\uC77C \uC0DD\uC131 \uC2E4\uD328 (\uACC4\uC18D \uC9C4\uD589):`, error);
|
|
16163
16437
|
}
|
|
@@ -16189,17 +16463,20 @@ var DatabaseSyncer = class {
|
|
|
16189
16463
|
...a.showTitle ? {} : { showTitle: false }
|
|
16190
16464
|
};
|
|
16191
16465
|
}
|
|
16192
|
-
/**
|
|
16193
|
-
|
|
16194
|
-
|
|
16195
|
-
|
|
16466
|
+
/** 볼트에서 고친 `.base` 를 덮어쓰거나 지우지 않았다고 한 번 알린다. */
|
|
16467
|
+
warnEditedBase(path) {
|
|
16468
|
+
if (this.keptUserBases.has(path)) return;
|
|
16469
|
+
this.keptUserBases.add(path);
|
|
16470
|
+
getLogger().warn(
|
|
16471
|
+
`[DB Sync] \uBCFC\uD2B8\uC5D0\uC11C \uACE0\uCE5C .base \uB77C Notion \uBCF4\uAE30\uB85C \uB36E\uC5B4\uC4F0\uC9C0 \uC54A\uC74C: ${path} \u2014 Notion \uC758 \uBCF4\uAE30\uB97C \uB2E4\uC2DC \uBC1B\uC73C\uB824\uBA74 \uC774 \uD30C\uC77C\uC744 \uC9C0\uC6B0\uC138\uC694`
|
|
16472
|
+
);
|
|
16196
16473
|
}
|
|
16197
16474
|
/**
|
|
16198
16475
|
* DB 제목이 바뀌면 `.base`/`.notion.json` 파일명(safeName)이 바뀌어 옛 이름의 산출물이
|
|
16199
16476
|
* 같은 폴더에 고아로 잔존한다(drift·부활 원인). DB 폴더 **직속**의 산출물 중 현재 이름이
|
|
16200
16477
|
* 아닌 것을 삭제해 멱등을 보장한다. 하위 폴더(중첩 DB)의 산출물은 보호한다.
|
|
16201
16478
|
*/
|
|
16202
|
-
async cleanupStaleDbArtifacts(localFolder, safeName) {
|
|
16479
|
+
async cleanupStaleDbArtifacts(localFolder, safeName, previous) {
|
|
16203
16480
|
try {
|
|
16204
16481
|
const keep = /* @__PURE__ */ new Set([
|
|
16205
16482
|
`${localFolder}/${safeName}${BASE_SUFFIX}`,
|
|
@@ -16208,7 +16485,12 @@ var DatabaseSyncer = class {
|
|
|
16208
16485
|
const files = await this.vaultFs.listNonMarkdownFiles();
|
|
16209
16486
|
const stale = selectStaleDbArtifacts(files, localFolder, keep);
|
|
16210
16487
|
for (const path of stale) {
|
|
16211
|
-
|
|
16488
|
+
const content = await this.vaultFs.readFile(path).catch(() => null);
|
|
16489
|
+
if (content === null || !isGeneratedDbArtifact(path, content)) continue;
|
|
16490
|
+
if (editedSince(previous, path, content)) {
|
|
16491
|
+
this.warnEditedBase(path);
|
|
16492
|
+
continue;
|
|
16493
|
+
}
|
|
16212
16494
|
await this.vaultFs.deleteFile(path);
|
|
16213
16495
|
getLogger().info(`[DB Sync] \uACE0\uC544 DB \uC0B0\uCD9C\uBB3C \uC0AD\uC81C(rename): ${path}`);
|
|
16214
16496
|
}
|
|
@@ -16386,6 +16668,12 @@ var DatabaseSyncer = class {
|
|
|
16386
16668
|
}
|
|
16387
16669
|
return (path) => this.stateDb.getByPath(path) ?? (untracked.has(path) ? { notionPageId: null } : null);
|
|
16388
16670
|
}
|
|
16671
|
+
/** 되살린 행의 경로를 가져가고 비운다. */
|
|
16672
|
+
takeRestoredRows() {
|
|
16673
|
+
const rows = this.restoredRows;
|
|
16674
|
+
this.restoredRows = [];
|
|
16675
|
+
return rows;
|
|
16676
|
+
}
|
|
16389
16677
|
/** 이름을 바꾼 행을 가져가고 비운다. */
|
|
16390
16678
|
takeRenamedRows() {
|
|
16391
16679
|
const rows = this.renamedRows;
|
|
@@ -16558,6 +16846,9 @@ var DatabaseSyncer = class {
|
|
|
16558
16846
|
function rowProperties(page) {
|
|
16559
16847
|
return page.properties ?? {};
|
|
16560
16848
|
}
|
|
16849
|
+
function editedSince(previous, path, content) {
|
|
16850
|
+
return previous?.basePath === path && previous.hash !== void 0 && computeHash(content) !== previous.hash;
|
|
16851
|
+
}
|
|
16561
16852
|
|
|
16562
16853
|
// src/audit/completeness.ts
|
|
16563
16854
|
function groupLocalRowsByDatabase(local) {
|
|
@@ -16654,6 +16945,40 @@ async function verifyPageCompleteness(remote, local, rootPageId) {
|
|
|
16654
16945
|
};
|
|
16655
16946
|
}
|
|
16656
16947
|
|
|
16948
|
+
// src/audit/nested-folder-pages.ts
|
|
16949
|
+
var ID_SUFFIX = new RegExp(
|
|
16950
|
+
` \\((?:${PAGE_ID_SUFFIX_LENGTHS.map((len) => `[0-9a-f]{${len}}`).join("|")})\\)$`
|
|
16951
|
+
);
|
|
16952
|
+
function folderOf3(record) {
|
|
16953
|
+
if (record.fileType === "db-row") return null;
|
|
16954
|
+
if (isFolderRecord(record)) return record.obsidianPath;
|
|
16955
|
+
const slash = record.obsidianPath.lastIndexOf("/");
|
|
16956
|
+
if (slash < 0) return null;
|
|
16957
|
+
const folder = record.obsidianPath.slice(0, slash);
|
|
16958
|
+
const stem = stripMarkdownExtension(record.obsidianPath.slice(slash + 1)).replace(ID_SUFFIX, "");
|
|
16959
|
+
return stem === folder.slice(folder.lastIndexOf("/") + 1) ? folder : null;
|
|
16960
|
+
}
|
|
16961
|
+
function findNestedFolderPages(records) {
|
|
16962
|
+
const byPage = /* @__PURE__ */ new Map();
|
|
16963
|
+
for (const record of records) {
|
|
16964
|
+
if (record.notionPageId !== null) byPage.set(compactNotionId(record.notionPageId), record);
|
|
16965
|
+
}
|
|
16966
|
+
const found2 = [];
|
|
16967
|
+
for (const inner of records) {
|
|
16968
|
+
if (inner.notionPageId === null || inner.notionParentId === null) continue;
|
|
16969
|
+
const folder = folderOf3(inner);
|
|
16970
|
+
if (folder === null) continue;
|
|
16971
|
+
const outer = byPage.get(compactNotionId(inner.notionParentId));
|
|
16972
|
+
if (!outer || outer === inner || folderOf3(outer) !== folder) continue;
|
|
16973
|
+
found2.push({
|
|
16974
|
+
folder,
|
|
16975
|
+
outer: { path: outer.obsidianPath, notionPageId: outer.notionPageId },
|
|
16976
|
+
inner: { path: inner.obsidianPath, notionPageId: inner.notionPageId }
|
|
16977
|
+
});
|
|
16978
|
+
}
|
|
16979
|
+
return found2.sort((a, b) => a.folder < b.folder ? -1 : a.folder > b.folder ? 1 : 0);
|
|
16980
|
+
}
|
|
16981
|
+
|
|
16657
16982
|
// src/conflict/merger.ts
|
|
16658
16983
|
function threeWayMerge(base, local, remote) {
|
|
16659
16984
|
const baseLines = base.split("\n");
|
|
@@ -17068,6 +17393,9 @@ var ScopedVaultFS = class {
|
|
|
17068
17393
|
async ensureFolder(path) {
|
|
17069
17394
|
return await this.base.ensureFolder(this.toBase(path, true));
|
|
17070
17395
|
}
|
|
17396
|
+
async removeEmptyFolder(path) {
|
|
17397
|
+
return await this.base.removeEmptyFolder?.(this.toBase(path, true)) ?? false;
|
|
17398
|
+
}
|
|
17071
17399
|
/** 맡은 노트만 읽는다 — 볼트 전체의 본문을 읽지 않는다. */
|
|
17072
17400
|
async listMarkdownFiles() {
|
|
17073
17401
|
const stats = await this.listMarkdownFileStats();
|
|
@@ -17145,7 +17473,7 @@ function nextFullReconcileAt(lastFullPullAt, intervalSec) {
|
|
|
17145
17473
|
const last = lastFullPullAt === null ? NaN : Date.parse(lastFullPullAt);
|
|
17146
17474
|
return Number.isFinite(last) ? new Date(last + intervalSec * 1e3).toISOString() : null;
|
|
17147
17475
|
}
|
|
17148
|
-
function remoteScanInfo(scan, lastFullPullAt, deleteSync) {
|
|
17476
|
+
function remoteScanInfo(scan, lastFullPullAt, deleteSync, deletionsChecked = false) {
|
|
17149
17477
|
return scan.kind === "full" ? {
|
|
17150
17478
|
kind: "full",
|
|
17151
17479
|
reason: scan.reason,
|
|
@@ -17156,7 +17484,7 @@ function remoteScanInfo(scan, lastFullPullAt, deleteSync) {
|
|
|
17156
17484
|
kind: "incremental",
|
|
17157
17485
|
lastFullAt: scan.lastFullAt,
|
|
17158
17486
|
nextFullAt: scan.nextFullAt,
|
|
17159
|
-
deletionsDeferred: deleteSync
|
|
17487
|
+
deletionsDeferred: deleteSync && !deletionsChecked
|
|
17160
17488
|
};
|
|
17161
17489
|
}
|
|
17162
17490
|
var ALL_DATABASES = { kind: "all" };
|
|
@@ -17178,6 +17506,14 @@ function parsePendingDatabases(raw) {
|
|
|
17178
17506
|
function serializePendingDatabases(ids) {
|
|
17179
17507
|
return JSON.stringify([...ids].sort());
|
|
17180
17508
|
}
|
|
17509
|
+
function addPendingDatabases(stateDb, databaseIds) {
|
|
17510
|
+
const pending = parsePendingDatabases(stateDb.getMeta(PENDING_DATABASES_META_KEY));
|
|
17511
|
+
const before = pending.size;
|
|
17512
|
+
for (const id of databaseIds) pending.add(compactNotionId(id));
|
|
17513
|
+
if (pending.size !== before) {
|
|
17514
|
+
stateDb.setMeta(PENDING_DATABASES_META_KEY, serializePendingDatabases(pending));
|
|
17515
|
+
}
|
|
17516
|
+
}
|
|
17181
17517
|
var DatabasePullLedger = class {
|
|
17182
17518
|
constructor(selection, pending) {
|
|
17183
17519
|
this.selection = selection;
|
|
@@ -17539,8 +17875,7 @@ var InterruptedSyncRecovery = class {
|
|
|
17539
17875
|
if (isNotionObjectNotFound(error)) continue;
|
|
17540
17876
|
throw error;
|
|
17541
17877
|
}
|
|
17542
|
-
|
|
17543
|
-
if (inTrash || page.archived) continue;
|
|
17878
|
+
if (isTrashedOrArchived(page)) continue;
|
|
17544
17879
|
return page;
|
|
17545
17880
|
}
|
|
17546
17881
|
return null;
|
|
@@ -17559,8 +17894,7 @@ var InterruptedSyncRecovery = class {
|
|
|
17559
17894
|
});
|
|
17560
17895
|
for (const row of rows) {
|
|
17561
17896
|
if (this.stateDb.getByNotionId(row.id)) continue;
|
|
17562
|
-
|
|
17563
|
-
if (inTrash || row.archived) continue;
|
|
17897
|
+
if (isTrashedOrArchived(row)) continue;
|
|
17564
17898
|
return row;
|
|
17565
17899
|
}
|
|
17566
17900
|
return null;
|
|
@@ -18059,7 +18393,7 @@ var DatabaseDiscovery = class {
|
|
|
18059
18393
|
}
|
|
18060
18394
|
let safeName;
|
|
18061
18395
|
if (dbTitle) {
|
|
18062
|
-
safeName = dbTitle
|
|
18396
|
+
safeName = databaseFolderName(dbTitle);
|
|
18063
18397
|
} else {
|
|
18064
18398
|
const parentName = parentFolder.split("/").pop() || "";
|
|
18065
18399
|
safeName = parentName ? `${parentName}-DB` : `db-${dbId.replace(/-/g, "").slice(0, 8)}`;
|
|
@@ -18152,6 +18486,12 @@ var DatabaseDiscovery = class {
|
|
|
18152
18486
|
return refs;
|
|
18153
18487
|
}
|
|
18154
18488
|
};
|
|
18489
|
+
function databaseFolderName(title) {
|
|
18490
|
+
return truncateBytes(
|
|
18491
|
+
title.replace(/[^a-zA-Z0-9가-힣\s_-]/g, "").replace(/\s+/g, "-"),
|
|
18492
|
+
FILE_SYSTEM_NAME_MAX_BYTES
|
|
18493
|
+
);
|
|
18494
|
+
}
|
|
18155
18495
|
|
|
18156
18496
|
// src/sync/folder-placement.ts
|
|
18157
18497
|
var FolderPlacement = class {
|
|
@@ -18975,7 +19315,7 @@ var RemoteRenameFollower = class {
|
|
|
18975
19315
|
/** 바꿀 것이 없으면 null. */
|
|
18976
19316
|
async followOne(record, dryRun) {
|
|
18977
19317
|
const page = await this.pageOf(record.notionPageId);
|
|
18978
|
-
if (page
|
|
19318
|
+
if (isTrashedOrArchived(page)) return null;
|
|
18979
19319
|
const title = this.notionClient.extractTitle(page);
|
|
18980
19320
|
if (!retitledInNotion(...await this.frontmatters(record), record.obsidianPath, title)) {
|
|
18981
19321
|
return null;
|
|
@@ -19116,7 +19456,7 @@ function rewriteTarget(target, source, context, kind) {
|
|
|
19116
19456
|
const shown = keepExtension ? destination : destination.slice(0, -".md".length);
|
|
19117
19457
|
let next;
|
|
19118
19458
|
if (resolved.relative) {
|
|
19119
|
-
next = relativePath(
|
|
19459
|
+
next = relativePath(folderOf4(source.to), shown);
|
|
19120
19460
|
if (!linkpath.startsWith("./") && next.startsWith("./")) next = next.slice(2);
|
|
19121
19461
|
} else if (style === "name") {
|
|
19122
19462
|
const name = baseName3(shown);
|
|
@@ -19138,14 +19478,14 @@ function resolveLinkpath(linkpath, sourcePath, files) {
|
|
|
19138
19478
|
const matches = byName(files, baseName3(candidate)).filter((file) => file.endsWith(suffix));
|
|
19139
19479
|
if (matches.length === 1) return found(matches[0], linkpath);
|
|
19140
19480
|
if (matches.length > 1) {
|
|
19141
|
-
const local = `${folderPrefix(
|
|
19481
|
+
const local = `${folderPrefix(folderOf4(sourcePath))}${candidate}`;
|
|
19142
19482
|
return matches.includes(local) ? found(local, linkpath) : null;
|
|
19143
19483
|
}
|
|
19144
19484
|
}
|
|
19145
19485
|
return null;
|
|
19146
19486
|
}
|
|
19147
19487
|
function resolveRelative(linkpath, sourcePath, files) {
|
|
19148
|
-
const joined = normalizeSegments(`${folderPrefix(
|
|
19488
|
+
const joined = normalizeSegments(`${folderPrefix(folderOf4(sourcePath))}${linkpath}`);
|
|
19149
19489
|
if (joined === null) return null;
|
|
19150
19490
|
for (const candidate of withExtension(joined)) {
|
|
19151
19491
|
if (files.has(candidate)) return { ...found(candidate, linkpath), relative: true };
|
|
@@ -19204,7 +19544,7 @@ function relativePath(from, to) {
|
|
|
19204
19544
|
function encodeMarkdownTarget(next, original) {
|
|
19205
19545
|
return original.includes("%") || next.includes(" ") ? next.replace(/ /g, "%20") : next;
|
|
19206
19546
|
}
|
|
19207
|
-
function
|
|
19547
|
+
function folderOf4(path) {
|
|
19208
19548
|
const slash = path.lastIndexOf("/");
|
|
19209
19549
|
return slash === -1 ? "" : path.slice(0, slash);
|
|
19210
19550
|
}
|
|
@@ -20473,9 +20813,7 @@ var DatabaseCreator = class {
|
|
|
20473
20813
|
discovered.push({ databaseId, localFolder: folder, titleProperty: titleProperty2 });
|
|
20474
20814
|
this.stateDb.setMeta(DISCOVERED_DBS_META_KEY, JSON.stringify(discovered));
|
|
20475
20815
|
}
|
|
20476
|
-
|
|
20477
|
-
pending.add(compactNotionId(databaseId));
|
|
20478
|
-
this.stateDb.setMeta(PENDING_DATABASES_META_KEY, serializePendingDatabases(pending));
|
|
20816
|
+
addPendingDatabases(this.stateDb, [databaseId]);
|
|
20479
20817
|
}
|
|
20480
20818
|
async readSidecar(path) {
|
|
20481
20819
|
try {
|
|
@@ -20780,6 +21118,7 @@ var PagePusher = class {
|
|
|
20780
21118
|
* (ADR-017). relation 을 보내면 가리킨 행의 반대쪽 relation 이, 행을 만들면 그 행의 수식 · ID 가 바뀐다.
|
|
20781
21119
|
*/
|
|
20782
21120
|
markRemoteUnverified(pageIds) {
|
|
21121
|
+
const databases = [];
|
|
20783
21122
|
for (const pageId of new Set(pageIds)) {
|
|
20784
21123
|
const target = this.stateDb.getByNotionId(pageId);
|
|
20785
21124
|
if (!target?.notionLastEdited) continue;
|
|
@@ -20788,6 +21127,76 @@ var PagePusher = class {
|
|
|
20788
21127
|
lastEditedBy: null,
|
|
20789
21128
|
seenAt: null
|
|
20790
21129
|
});
|
|
21130
|
+
if (target.fileType === "db-row" && target.notionParentId) {
|
|
21131
|
+
databases.push(target.notionParentId);
|
|
21132
|
+
}
|
|
21133
|
+
}
|
|
21134
|
+
addPendingDatabases(this.stateDb, databases);
|
|
21135
|
+
}
|
|
21136
|
+
/** 이번 실행에서 볼트의 값을 올리지 않은 읽기 전용 속성 — 노트 경로별. */
|
|
21137
|
+
readOnlySkips = /* @__PURE__ */ new Map();
|
|
21138
|
+
/** 올리지 않은 읽기 전용 속성을 꺼낸다(목록은 비운다). */
|
|
21139
|
+
takeReadOnlySkips() {
|
|
21140
|
+
const skips = [...this.readOnlySkips].sort(([a], [b]) => a.localeCompare(b)).map(([path, properties]) => ({ path, properties }));
|
|
21141
|
+
this.readOnlySkips.clear();
|
|
21142
|
+
return skips;
|
|
21143
|
+
}
|
|
21144
|
+
/**
|
|
21145
|
+
* dry-run 이 알릴 읽기 전용 속성 — 실제 push 처럼 볼트에서 바꾼 행 속성(새 행은 모든 속성) 가운데 Notion 이 값을
|
|
21146
|
+
* 정하는 것. 원격은 읽지 않고 지난 동기화 사본과 견준다 — 원격도 바뀌었으면 실제 push 와 다를 수 있다. 사본이
|
|
21147
|
+
* 없거나 frontmatter · 스키마를 읽지 못하면 그 노트는 건너뛴다(실제 push 가 이유와 함께 알린다).
|
|
21148
|
+
*/
|
|
21149
|
+
async previewReadOnlySkips(changes) {
|
|
21150
|
+
const skips = [];
|
|
21151
|
+
for (const change of changes) {
|
|
21152
|
+
const row = this.previewRow(change);
|
|
21153
|
+
if (!row) continue;
|
|
21154
|
+
try {
|
|
21155
|
+
const current = parseFrontmatter(await this.vaultFs.readFile(change.path));
|
|
21156
|
+
const { changed, cleared } = diffRowProperties(row.base, current.data);
|
|
21157
|
+
const mapper = await this.rowSchemas.mapperFor(row.databaseId);
|
|
21158
|
+
const readOnly = mapper.readOnlyKeys(
|
|
21159
|
+
withoutDecorations([...Object.keys(changed), ...cleared])
|
|
21160
|
+
);
|
|
21161
|
+
if (readOnly.length > 0) skips.push({ path: change.path, properties: readOnly });
|
|
21162
|
+
} catch {
|
|
21163
|
+
continue;
|
|
21164
|
+
}
|
|
21165
|
+
}
|
|
21166
|
+
return skips.sort((a, b) => a.path.localeCompare(b.path));
|
|
21167
|
+
}
|
|
21168
|
+
/** dry-run 이 견줄 행 — 새 행은 기준 없이, 고친 행은 지난 동기화 사본과. 행이 아니거나 사본이 없으면 null. */
|
|
21169
|
+
previewRow(change) {
|
|
21170
|
+
if (change.type === "created") {
|
|
21171
|
+
const databaseId2 = this.placement.newRowDatabaseOf(change.path);
|
|
21172
|
+
return databaseId2 ? { databaseId: databaseId2, base: null } : null;
|
|
21173
|
+
}
|
|
21174
|
+
if (change.type !== "modified") return null;
|
|
21175
|
+
const record = this.stateDb.getByPath(change.path);
|
|
21176
|
+
const databaseId = record ? rowDatabaseOf(this.config, record) : null;
|
|
21177
|
+
const base = record && databaseId ? this.baseRowState(record) : null;
|
|
21178
|
+
return databaseId && base ? { databaseId, base: base.properties } : null;
|
|
21179
|
+
}
|
|
21180
|
+
/**
|
|
21181
|
+
* 보내지 않은 속성을 알린다. 읽기 전용 속성은 경고로 따로 모은다 — 다음 pull 이 볼트의 값을 Notion 값으로 되돌리니
|
|
21182
|
+
* 고친 사람이 알아야 한다. 나머지(DB 에 없는 속성 · 변환 불가 …)는 예전처럼 정보 로그다.
|
|
21183
|
+
*/
|
|
21184
|
+
reportUnsent(path, mapper, skipped, what) {
|
|
21185
|
+
const unsent = withoutDecorations(skipped);
|
|
21186
|
+
const readOnly = mapper.readOnlyKeys(unsent);
|
|
21187
|
+
if (readOnly.length > 0) {
|
|
21188
|
+
this.readOnlySkips.set(path, readOnly);
|
|
21189
|
+
getLogger().warn(
|
|
21190
|
+
`[Im-Nobsidian] \uC77D\uAE30 \uC804\uC6A9 \uC18D\uC131 ${readOnly.length}\uAC1C\uB294 Notion \uC774 \uAC12\uC744 \uC815\uD574 \uC62C\uB9AC\uC9C0 \uC54A\uC74C \u2014 \uB2E4\uC74C pull \uC774 Notion \uAC12\uC73C\uB85C \uB418\uB3CC\uB9B0\uB2E4: ${path} \u2014 ${readOnly.join(", ")}`
|
|
21191
|
+
);
|
|
21192
|
+
} else {
|
|
21193
|
+
this.readOnlySkips.delete(path);
|
|
21194
|
+
}
|
|
21195
|
+
const others = unsent.filter((key) => !readOnly.includes(key));
|
|
21196
|
+
if (others.length > 0) {
|
|
21197
|
+
getLogger().info(
|
|
21198
|
+
`[Im-Nobsidian] ${what} ${others.length}\uAC1C\uB294 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C(DB \uC5D0 \uC5C6\uB294 \uC18D\uC131 \xB7 \uBE44\uC6B8 \uC218 \uC5C6\uB294 \uC18D\uC131 \xB7 \uBCC0\uD658 \uBD88\uAC00): ${path} \u2014 ${others.join(", ")}`
|
|
21199
|
+
);
|
|
20791
21200
|
}
|
|
20792
21201
|
}
|
|
20793
21202
|
/** relation 을 보내지 못한 행을 꺼낸다(목록은 비운다). */
|
|
@@ -20812,7 +21221,7 @@ var PagePusher = class {
|
|
|
20812
21221
|
const selectedPath = this.pipeline.selectPath(content);
|
|
20813
21222
|
if (selectedPath === "block-api") {
|
|
20814
21223
|
getLogger().warn(
|
|
20815
|
-
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column
|
|
21224
|
+
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column) \u2014 converted with reduced fidelity in v0.1.0`
|
|
20816
21225
|
);
|
|
20817
21226
|
}
|
|
20818
21227
|
const conversionResult = this.pipeline.convertToNotion(content, {
|
|
@@ -20956,7 +21365,7 @@ var PagePusher = class {
|
|
|
20956
21365
|
const updatePath = this.pipeline.selectPath(content);
|
|
20957
21366
|
if (updatePath === "block-api") {
|
|
20958
21367
|
getLogger().warn(
|
|
20959
|
-
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column
|
|
21368
|
+
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column) \u2014 converted with reduced fidelity in v0.1.0`
|
|
20960
21369
|
);
|
|
20961
21370
|
}
|
|
20962
21371
|
const conversionResult = this.pipeline.convertToNotion(content, {
|
|
@@ -20987,7 +21396,7 @@ var PagePusher = class {
|
|
|
20987
21396
|
bodyFingerprint = await this.drift.remoteBodyFingerprintOf(record.notionPageId);
|
|
20988
21397
|
}
|
|
20989
21398
|
const newTitle = this.changedPageTitle(record, content, path);
|
|
20990
|
-
const propsToUpdate = newTitle === null ? void 0 : titleProperty(newTitle);
|
|
21399
|
+
const propsToUpdate = newTitle === null ? void 0 : titleProperty(newTitle, pagePropertiesOf(remote));
|
|
20991
21400
|
const decorated = await this.decorations.push(
|
|
20992
21401
|
record.notionPageId,
|
|
20993
21402
|
path,
|
|
@@ -21170,16 +21579,11 @@ var PagePusher = class {
|
|
|
21170
21579
|
current: current.data,
|
|
21171
21580
|
uploads: await this.uploadsFor(mapper, changes.changed, path)
|
|
21172
21581
|
});
|
|
21173
|
-
|
|
21174
|
-
if (unsentProperties.length > 0) {
|
|
21175
|
-
getLogger().info(
|
|
21176
|
-
`[Im-Nobsidian] \uC0C8 \uD589 \uC18D\uC131 ${unsentProperties.length}\uAC1C\uB294 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C(DB \uC5D0 \uC5C6\uB294 \uC18D\uC131 \xB7 \uC77D\uAE30 \uC804\uC6A9 \xB7 \uBCC0\uD658 \uBD88\uAC00): ${path} \u2014 ${unsentProperties.join(", ")}`
|
|
21177
|
-
);
|
|
21178
|
-
}
|
|
21582
|
+
this.reportUnsent(path, mapper, skipped, "\uC0C8 \uD589 \uC18D\uC131");
|
|
21179
21583
|
const selectedPath = this.pipeline.selectPath(content);
|
|
21180
21584
|
if (selectedPath === "block-api") {
|
|
21181
21585
|
getLogger().warn(
|
|
21182
|
-
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column
|
|
21586
|
+
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column) \u2014 converted with reduced fidelity in v0.1.0`
|
|
21183
21587
|
);
|
|
21184
21588
|
}
|
|
21185
21589
|
const conversionResult = this.pipeline.convertToNotion(content, {
|
|
@@ -21378,12 +21782,7 @@ var PagePusher = class {
|
|
|
21378
21782
|
...against === null || options.overwriteRemote ? {} : { uploads: await this.uploadsFor(mapper, changes.changed, path) }
|
|
21379
21783
|
}
|
|
21380
21784
|
);
|
|
21381
|
-
|
|
21382
|
-
if (unsentProperties.length > 0) {
|
|
21383
|
-
getLogger().info(
|
|
21384
|
-
`[Im-Nobsidian] \uD589 \uC18D\uC131 ${unsentProperties.length}\uAC1C\uB294 \uBCF4\uB0B4\uC9C0 \uC54A\uC74C(DB \uC5D0 \uC5C6\uB294 \uC18D\uC131 \xB7 \uC77D\uAE30 \uC804\uC6A9 \xB7 \uBE44\uC6B8 \uC218 \uC5C6\uB294 \uC18D\uC131 \xB7 \uBCC0\uD658 \uBD88\uAC00): ${path} \u2014 ${unsentProperties.join(", ")}`
|
|
21385
|
-
);
|
|
21386
|
-
}
|
|
21785
|
+
this.reportUnsent(path, mapper, skipped, "\uD589 \uC18D\uC131");
|
|
21387
21786
|
const bodyChanged = against?.body == null || against.body !== current.body;
|
|
21388
21787
|
const settles = drift === "none";
|
|
21389
21788
|
if (bodyChanged && !this.drift.overwritesRemote(record, options.overwriteRemote)) {
|
|
@@ -21395,7 +21794,7 @@ var PagePusher = class {
|
|
|
21395
21794
|
const selectedPath = this.pipeline.selectPath(content);
|
|
21396
21795
|
if (selectedPath === "block-api") {
|
|
21397
21796
|
getLogger().warn(
|
|
21398
|
-
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column
|
|
21797
|
+
`[Im-Nobsidian] "${path}" contains block-api features (inline-db/column) \u2014 converted with reduced fidelity in v0.1.0`
|
|
21399
21798
|
);
|
|
21400
21799
|
}
|
|
21401
21800
|
conversionResult = this.pipeline.convertToNotion(content, {
|
|
@@ -21473,6 +21872,8 @@ var PagePusher = class {
|
|
|
21473
21872
|
aliases: extractAliases(current.data)
|
|
21474
21873
|
});
|
|
21475
21874
|
});
|
|
21875
|
+
if (mapper.readOnlyKeys(withoutDecorations(skipped)).length > 0)
|
|
21876
|
+
this.markRemoteUnverified([pageId]);
|
|
21476
21877
|
}
|
|
21477
21878
|
/**
|
|
21478
21879
|
* 지난 동기화 시점의 행. 사본이 없거나 읽지 못하면 null — 호출측은 비교할 기준이 없다고
|
|
@@ -21517,7 +21918,10 @@ var PagePusher = class {
|
|
|
21517
21918
|
const title = change.title ? change.title(this.notionClient.extractTitle(remote)) : null;
|
|
21518
21919
|
if (newParent !== null) await this.notionClient.movePage(pageId, newParent);
|
|
21519
21920
|
if (title !== null) {
|
|
21520
|
-
written = await this.notionClient.updatePageProperties(
|
|
21921
|
+
written = await this.notionClient.updatePageProperties(
|
|
21922
|
+
pageId,
|
|
21923
|
+
titleProperty(title, pagePropertiesOf(remote))
|
|
21924
|
+
);
|
|
21521
21925
|
} else if (newParent !== null) {
|
|
21522
21926
|
written = await this.notionClient.getPage(pageId);
|
|
21523
21927
|
}
|
|
@@ -21863,6 +22267,8 @@ var PagePuller = class {
|
|
|
21863
22267
|
// 제목이 같은 새 페이지끼리 경로를 고르는 차례({@link orderCreates}) — 페이지 → 앞 차례가 경로를 고르면
|
|
21864
22268
|
// 풀리는 약속과 내 차례를 끝내는 함수.
|
|
21865
22269
|
claimTurns = /* @__PURE__ */ new Map();
|
|
22270
|
+
// Notion 에서 사라져 볼트에서 지운 페이지의 하위 폴더 — pull 이 마칠 때 비었으면 지운다({@link removeVacatedFolders}).
|
|
22271
|
+
vacatedFolders = /* @__PURE__ */ new Set();
|
|
21866
22272
|
decorationsCache;
|
|
21867
22273
|
get decorations() {
|
|
21868
22274
|
return this.decorationsCache ??= new PageDecorations(this.notionClient, this.imageHandler);
|
|
@@ -21877,6 +22283,35 @@ var PagePuller = class {
|
|
|
21877
22283
|
this.claimTurns.clear();
|
|
21878
22284
|
this._pullImageCount = 0;
|
|
21879
22285
|
this._pullFileCount = 0;
|
|
22286
|
+
this.vacatedFolders.clear();
|
|
22287
|
+
}
|
|
22288
|
+
/**
|
|
22289
|
+
* 이번 pull 이 지운 페이지의 폴더가 비었으면 지운다 — 안쪽부터, 그래야 바깥이 빈다. 예전에는 노트만 지우고
|
|
22290
|
+
* 폴더를 남겨 볼트에 빈 폴더가 쌓였다. 사용자 파일 · 숨김 파일이 하나라도 남은 폴더는 둔다
|
|
22291
|
+
* ({@link VaultFS.removeEmptyFolder}).
|
|
22292
|
+
*/
|
|
22293
|
+
async removeVacatedFolders() {
|
|
22294
|
+
const folders = [...this.vacatedFolders].sort(
|
|
22295
|
+
(a, b) => b.split("/").length - a.split("/").length
|
|
22296
|
+
);
|
|
22297
|
+
this.vacatedFolders.clear();
|
|
22298
|
+
for (const folder of folders) {
|
|
22299
|
+
try {
|
|
22300
|
+
await this.vaultFs.removeEmptyFolder?.(folder);
|
|
22301
|
+
} catch (error) {
|
|
22302
|
+
getLogger().warn(`[Im-Nobsidian] \uBE48 \uD3F4\uB354 ${folder} \uB97C \uC9C0\uC6B0\uC9C0 \uBABB\uD568:`, error);
|
|
22303
|
+
}
|
|
22304
|
+
}
|
|
22305
|
+
}
|
|
22306
|
+
/**
|
|
22307
|
+
* 원격에서 사라진 페이지의 하위 폴더를 적어 둔다 — 그 아래 페이지도 함께 사라졌으면 pull 끝에 빈다. 볼트 맨 위와
|
|
22308
|
+
* DB 폴더는 적지 않는다 — DB 는 페이지가 아니다.
|
|
22309
|
+
*/
|
|
22310
|
+
noteVacatedFolder(record) {
|
|
22311
|
+
const lookup = this.placement.folderLookup();
|
|
22312
|
+
if (isFolderRecord(record) && lookup.databaseAt(record.obsidianPath)) return;
|
|
22313
|
+
const folder = childFolderOf(record, lookup);
|
|
22314
|
+
if (folder !== "" && !lookup.databaseAt(folder)) this.vacatedFolders.add(folder);
|
|
21880
22315
|
}
|
|
21881
22316
|
/** 이번 pull 이 내려받은 이미지 수. */
|
|
21882
22317
|
get imageCount() {
|
|
@@ -22056,7 +22491,7 @@ var PagePuller = class {
|
|
|
22056
22491
|
if (!folderNote) return { action: "unchanged", path: record.obsidianPath };
|
|
22057
22492
|
record = folderNote;
|
|
22058
22493
|
}
|
|
22059
|
-
const remoteGone = page
|
|
22494
|
+
const remoteGone = isTrashedOrArchived(page);
|
|
22060
22495
|
if (remoteGone && !await this.vaultFs.exists(record.obsidianPath)) {
|
|
22061
22496
|
return { action: "unchanged", path: record.obsidianPath };
|
|
22062
22497
|
}
|
|
@@ -22152,6 +22587,7 @@ var PagePuller = class {
|
|
|
22152
22587
|
this.stateDb.delete(record.id);
|
|
22153
22588
|
this.stateDb.deleteWikilink(record.obsidianPath);
|
|
22154
22589
|
});
|
|
22590
|
+
if (this.config.sync.deleteSync) this.noteVacatedFolder(record);
|
|
22155
22591
|
return { action: "untracked", path: record.obsidianPath };
|
|
22156
22592
|
}
|
|
22157
22593
|
const outcome = await applyRemoteDeletion(this.stateDb, this.vaultFs, record, {
|
|
@@ -22159,6 +22595,7 @@ var PagePuller = class {
|
|
|
22159
22595
|
strategy: this.config.sync.conflictStrategy,
|
|
22160
22596
|
remoteChange: change
|
|
22161
22597
|
});
|
|
22598
|
+
if (outcome.action === "deleted" && this.config.sync.deleteSync) this.noteVacatedFolder(record);
|
|
22162
22599
|
return { ...outcome, path: record.obsidianPath };
|
|
22163
22600
|
}
|
|
22164
22601
|
/**
|
|
@@ -22314,8 +22751,7 @@ var PagePuller = class {
|
|
|
22314
22751
|
*/
|
|
22315
22752
|
async pullFolderRecord(record, page) {
|
|
22316
22753
|
const folder = record.obsidianPath;
|
|
22317
|
-
|
|
22318
|
-
if (remoteGone) return null;
|
|
22754
|
+
if (isTrashedOrArchived(page)) return null;
|
|
22319
22755
|
const { markdown } = await this.notionClient.getPageMarkdown(page.id);
|
|
22320
22756
|
if (!hasBodyBesidesChildren(markdown)) {
|
|
22321
22757
|
this.observation.record(record.id, page, remoteBodyFingerprint(markdown));
|
|
@@ -22647,6 +23083,13 @@ var PullPlanner = class {
|
|
|
22647
23083
|
const restoredModified = planned.filter(
|
|
22648
23084
|
(c) => c.type === "modified" && restoreIds.has(c.pageId)
|
|
22649
23085
|
).length;
|
|
23086
|
+
const restoredPaths = [
|
|
23087
|
+
...[
|
|
23088
|
+
...restoreChanges,
|
|
23089
|
+
...planned.filter((c) => c.type === "modified" && restoreIds.has(c.pageId))
|
|
23090
|
+
].map((change) => this.stateDb.getByNotionId(change.pageId)?.obsidianPath).filter((path) => path !== void 0),
|
|
23091
|
+
...rows.filter((row) => row.operation === "update" && row.restore).map((row) => row.path)
|
|
23092
|
+
];
|
|
22650
23093
|
const rowCount = (operation, restore = false) => rows.filter((row) => row.operation === operation && (row.restore ?? false) === restore).length;
|
|
22651
23094
|
return {
|
|
22652
23095
|
created: count("created") + rowCount("create"),
|
|
@@ -22655,6 +23098,7 @@ var PullPlanner = class {
|
|
|
22655
23098
|
// 복원은 updated 에 섞지 않는다 — dry-run 이 "수정 N건" 이라고만 말하면
|
|
22656
23099
|
// 사용자가 사라진 파일이 되살아난다는 사실을 미리 알 수 없다.
|
|
22657
23100
|
restored: restoreChanges.length + restoredModified + rowCount("update", true),
|
|
23101
|
+
...restoredPaths.length > 0 ? { restoredPaths } : {},
|
|
22658
23102
|
conflicts: [],
|
|
22659
23103
|
writtenPaths: [],
|
|
22660
23104
|
failed,
|
|
@@ -22729,6 +23173,42 @@ var PullPlanner = class {
|
|
|
22729
23173
|
}
|
|
22730
23174
|
};
|
|
22731
23175
|
|
|
23176
|
+
// src/sync/deletion-sweep.ts
|
|
23177
|
+
var DELETION_SWEEP_CURSOR_META_KEY = "deletion_sweep_cursor";
|
|
23178
|
+
var DELETION_SWEEP_BUDGET = 15;
|
|
23179
|
+
function sweepable(records, skip) {
|
|
23180
|
+
return records.filter((r) => r.notionPageId !== null && !skip.has(idOf(r))).sort((a, b) => idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0);
|
|
23181
|
+
}
|
|
23182
|
+
function sweepBatch(records, cursor, skip, budget = DELETION_SWEEP_BUDGET) {
|
|
23183
|
+
const ordered = sweepable(records, skip);
|
|
23184
|
+
if (ordered.length === 0) return { batch: [], next: null, complete: true };
|
|
23185
|
+
if (ordered.length <= budget) {
|
|
23186
|
+
return { batch: ordered, next: idOf(ordered[ordered.length - 1]), complete: true };
|
|
23187
|
+
}
|
|
23188
|
+
const start = Math.max(0, cursor === null ? 0 : ordered.findIndex((r) => idOf(r) > cursor));
|
|
23189
|
+
const batch = Array.from({ length: budget }, (_, i) => ordered[(start + i) % ordered.length]);
|
|
23190
|
+
return { batch, next: idOf(batch[batch.length - 1]), complete: false };
|
|
23191
|
+
}
|
|
23192
|
+
function trackedDescendants(records, goneIds) {
|
|
23193
|
+
const gone = new Set(goneIds);
|
|
23194
|
+
const out = [];
|
|
23195
|
+
for (let grew = true; grew; ) {
|
|
23196
|
+
grew = false;
|
|
23197
|
+
for (const record of records) {
|
|
23198
|
+
if (record.notionPageId === null || record.notionParentId === null) continue;
|
|
23199
|
+
const id = idOf(record);
|
|
23200
|
+
if (gone.has(id) || !gone.has(compactNotionId(record.notionParentId))) continue;
|
|
23201
|
+
gone.add(id);
|
|
23202
|
+
out.push(record);
|
|
23203
|
+
grew = true;
|
|
23204
|
+
}
|
|
23205
|
+
}
|
|
23206
|
+
return out;
|
|
23207
|
+
}
|
|
23208
|
+
function idOf(record) {
|
|
23209
|
+
return compactNotionId(record.notionPageId);
|
|
23210
|
+
}
|
|
23211
|
+
|
|
22732
23212
|
// src/sync/remote-detector.ts
|
|
22733
23213
|
var RemoteDetector = class _RemoteDetector {
|
|
22734
23214
|
constructor(config, stateDb, notionClient, observation) {
|
|
@@ -22764,6 +23244,12 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22764
23244
|
// Notion 에서 부모가 바뀐 추적 페이지(정규화 id → 새 부모 페이지 id) — pull 이 볼트에서도 옮긴다(ADR-039).
|
|
22765
23245
|
// 옮겨도 수정 시각이 바뀌지 않아(실측) 변경으로는 보이지 않는다. 감지마다 다시 채운다.
|
|
22766
23246
|
_moved = /* @__PURE__ */ new Map();
|
|
23247
|
+
// 증분 감지가 search 로 살아 있다고 본 페이지(압축 id) — 삭제 확인이 다시 묻지 않는다.
|
|
23248
|
+
_seenAlive = /* @__PURE__ */ new Set();
|
|
23249
|
+
// 이번 감지가 삭제를 물은 뒤의 커서 — pull 이 마칠 때 적는다({@link saveDeletionSweep}). undefined 면 묻지 않았다.
|
|
23250
|
+
_sweepCursor;
|
|
23251
|
+
// 이번 감지가 추적 중인 것을 모두 물었나 — 그러면 증분이어도 미룬 삭제가 없다.
|
|
23252
|
+
_sweptAll = false;
|
|
22767
23253
|
/** 이번 감지에서 자식 페이지를 가진 것으로 본 페이지인가 — 폴더노트 판정의 1차 근거. */
|
|
22768
23254
|
hasChildPages(pageId) {
|
|
22769
23255
|
return this._childParentIds.has(normalizeNotionId(pageId));
|
|
@@ -22805,7 +23291,7 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22805
23291
|
throwIfAborted(signal);
|
|
22806
23292
|
try {
|
|
22807
23293
|
const page = await this.notionClient.getPage(childId);
|
|
22808
|
-
if (page
|
|
23294
|
+
if (isTrashedOrArchived(page)) continue;
|
|
22809
23295
|
const actualParent = await extractParentId(this.notionClient, page);
|
|
22810
23296
|
if (!actualParent || !notionIdsEqual(actualParent, parentId)) continue;
|
|
22811
23297
|
this._childParentIds.add(normalizeNotionId(parentId));
|
|
@@ -22868,10 +23354,83 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
22868
23354
|
* @param options.databases 바뀐 DB 도 찾는다(pull · dry-run). 상태 확인은 DB 를 조회하지 않아 찾지 않는다.
|
|
22869
23355
|
*/
|
|
22870
23356
|
async detectRemote(scan, options) {
|
|
23357
|
+
this._seenAlive.clear();
|
|
23358
|
+
this._sweepCursor = void 0;
|
|
23359
|
+
this._sweptAll = false;
|
|
22871
23360
|
const detection = scan.kind === "full" ? { changes: await this.detectRemoteChanges(options.signal), databaseIds: null } : await this.detectRemoteChangesIncremental(scan.since, options);
|
|
23361
|
+
if (options.databases) {
|
|
23362
|
+
detection.changes.push(
|
|
23363
|
+
...await this.sweepDeletions(scan, detection.changes, options.signal)
|
|
23364
|
+
);
|
|
23365
|
+
}
|
|
22872
23366
|
this.beginDetection(detection.changes);
|
|
22873
23367
|
return detection;
|
|
22874
23368
|
}
|
|
23369
|
+
/**
|
|
23370
|
+
* 이번 감지가 보지 못하는 삭제를 차례로 몇 개씩 원격에 묻는다({@link sweepBatch}). Notion 은 지운 것을
|
|
23371
|
+
* 목록으로 주지 않는다 — search · DB 조회 모두 휴지통을 돌려주지 않고 거르지도 못한다(ADR-044).
|
|
23372
|
+
*
|
|
23373
|
+
* - 증분 → 추적 중인 페이지 · 행 모두가 차례에 든다. search 가 살아 있다고 보인 것은 뺀다.
|
|
23374
|
+
* - 전체 대조 → 조회 필터를 단 DB 의 행만. 나머지는 순회 · DB 조회가 가른다. 필터를 단 DB 는 조회에 없는
|
|
23375
|
+
* 행이 필터 밖인지 지워진 것인지 몰라 가르지 않는다(DatabaseSyncer) — 여기서 하나씩 묻는다.
|
|
23376
|
+
*
|
|
23377
|
+
* 지워진 페이지 아래의 추적 페이지는 차례와 상관없이 함께 묻는다 — 부모를 휴지통에 넣으면 그 아래도 모두
|
|
23378
|
+
* 휴지통이다(실측 2026-10-28). 삭제는 deleteSync 일 때만, 첫 pull 이 아닐 때만 가른다 — 전체 대조와 같다.
|
|
23379
|
+
*/
|
|
23380
|
+
async sweepDeletions(scan, found2, signal) {
|
|
23381
|
+
if (!this.config.sync.deleteSync || !this.stateDb.getMeta(LAST_PULL_META_KEY)) return [];
|
|
23382
|
+
const records = this.stateDb.getAll();
|
|
23383
|
+
const candidates = scan.kind === "incremental" ? records : records.filter((r) => this.inFilteredDatabase(r));
|
|
23384
|
+
const skip = /* @__PURE__ */ new Set([...this._seenAlive, ...found2.map((c) => compactNotionId(c.pageId))]);
|
|
23385
|
+
const { batch, next, complete: complete2 } = sweepBatch(
|
|
23386
|
+
candidates,
|
|
23387
|
+
this.stateDb.getMeta(DELETION_SWEEP_CURSOR_META_KEY) || null,
|
|
23388
|
+
skip
|
|
23389
|
+
);
|
|
23390
|
+
this._sweepCursor = next;
|
|
23391
|
+
this._sweptAll = scan.kind === "incremental" && complete2;
|
|
23392
|
+
const deletions = [];
|
|
23393
|
+
const gone = /* @__PURE__ */ new Set();
|
|
23394
|
+
const ask = async (record) => {
|
|
23395
|
+
throwIfAborted(signal);
|
|
23396
|
+
const id = compactNotionId(record.notionPageId);
|
|
23397
|
+
try {
|
|
23398
|
+
if ((await remotePresence(this.notionClient, record.notionPageId)).kind !== "gone") return;
|
|
23399
|
+
} catch (error) {
|
|
23400
|
+
getLogger().warn(
|
|
23401
|
+
`[Im-Nobsidian] ${record.obsidianPath} \u2014 \uC6D0\uACA9\uC744 \uD655\uC778\uD558\uC9C0 \uBABB\uD574 \uC774\uBC88\uC5D0\uB294 \uC0AD\uC81C\uB97C \uAC00\uB974\uC9C0 \uC54A\uC74C:`,
|
|
23402
|
+
error
|
|
23403
|
+
);
|
|
23404
|
+
return;
|
|
23405
|
+
}
|
|
23406
|
+
gone.add(id);
|
|
23407
|
+
deletions.push(remoteDeletionChange(record));
|
|
23408
|
+
};
|
|
23409
|
+
for (const record of batch) await ask(record);
|
|
23410
|
+
const asked = new Set(batch.map((r) => compactNotionId(r.notionPageId)));
|
|
23411
|
+
for (const record of trackedDescendants(records, gone)) {
|
|
23412
|
+
const id = compactNotionId(record.notionPageId);
|
|
23413
|
+
if (!asked.has(id) && !skip.has(id)) await ask(record);
|
|
23414
|
+
}
|
|
23415
|
+
return deletions;
|
|
23416
|
+
}
|
|
23417
|
+
/** 조회 필터(`pullFilter`)를 단 설정 DB 의 행인가. */
|
|
23418
|
+
inFilteredDatabase(record) {
|
|
23419
|
+
if (record.fileType !== "db-row" || record.notionParentId === null) return false;
|
|
23420
|
+
return (this.config.notion.databases ?? []).some(
|
|
23421
|
+
(db) => db.pullFilter !== void 0 && notionIdsEqual(db.databaseId, record.notionParentId)
|
|
23422
|
+
);
|
|
23423
|
+
}
|
|
23424
|
+
/** 이번 감지가 추적 중인 페이지 · 행을 모두 원격에 물었나 — 결과의 `deletionsDeferred` 를 가른다. */
|
|
23425
|
+
deletionsChecked() {
|
|
23426
|
+
return this._sweptAll;
|
|
23427
|
+
}
|
|
23428
|
+
/** 삭제를 물은 자리를 적는다 — 다음 pull 이 이어서 묻는다. 경로를 좁힌 pull · dry-run 은 부르지 않는다. */
|
|
23429
|
+
saveDeletionSweep() {
|
|
23430
|
+
if (this._sweepCursor !== void 0) {
|
|
23431
|
+
this.stateDb.setMeta(DELETION_SWEEP_CURSOR_META_KEY, this._sweepCursor ?? "");
|
|
23432
|
+
}
|
|
23433
|
+
}
|
|
22875
23434
|
/**
|
|
22876
23435
|
* 전체 대조 — 루트 아래 페이지를 모두 훑고 추적 중인데 목록에 없는 페이지를 원격에 물어 삭제를
|
|
22877
23436
|
* 가른다. 취소하면 {@link OperationAbortedError} 를 던진다 — 다 훑지 못한 목록으로 삭제를 가르지
|
|
@@ -23012,9 +23571,8 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
23012
23571
|
}
|
|
23013
23572
|
/**
|
|
23014
23573
|
* 증분 원격 변경 감지 — `since` 이후 수정된 페이지만 search 로 받아 created/modified 만
|
|
23015
|
-
* 만든다.
|
|
23016
|
-
*
|
|
23017
|
-
* 전체 대조는 주기마다 돈다(ADR-027 — 예전에는 deleteSync 가 켜져 있으면 매번 돌았다. I10).
|
|
23574
|
+
* 만든다. search API 는 in_trash/archived 페이지를 반환하지 않아 삭제는 여기서 보이지 않는다 —
|
|
23575
|
+
* 차례로 묻는 삭제 확인({@link sweepDeletions})과 주기마다 도는 전체 대조(ADR-027)가 가른다.
|
|
23018
23576
|
*
|
|
23019
23577
|
* 바뀐 행 · 새 행 · 스키마를 고친 DB 도 모은다 — pull 은 그 DB 만 조회한다. 새 행은 페이지로 받지
|
|
23020
23578
|
* 않는다: DB 조회가 행 속성과 함께 받는다. 그래서 부모를 묻지 않는다(행마다 요청 1회 이상 아낀다).
|
|
@@ -23032,6 +23590,7 @@ var RemoteDetector = class _RemoteDetector {
|
|
|
23032
23590
|
const recentPages = await this.notionClient.searchRecentPages(searchSince, options.signal);
|
|
23033
23591
|
const untracked = [];
|
|
23034
23592
|
for (const page of recentPages) {
|
|
23593
|
+
this._seenAlive.add(compactNotionId(page.id));
|
|
23035
23594
|
const record = this.stateDb.getByNotionId(page.id);
|
|
23036
23595
|
if (!record) {
|
|
23037
23596
|
if (page.parentDatabaseId) {
|
|
@@ -23185,6 +23744,9 @@ function isRerenderable(record) {
|
|
|
23185
23744
|
}
|
|
23186
23745
|
|
|
23187
23746
|
// src/sync/missing-local-files.ts
|
|
23747
|
+
function restoredWhenMissing(record) {
|
|
23748
|
+
return record.notionPageId !== null && !isFolderRecord(record) && (record.fileType === "file" || record.fileType === "folder-note" || record.fileType === "db-row");
|
|
23749
|
+
}
|
|
23188
23750
|
async function detectMissingLocalFiles(config, stateDb, vaultFs, paths) {
|
|
23189
23751
|
if (config.sync.deleteSync) return [];
|
|
23190
23752
|
const records = stateDb.getAll();
|
|
@@ -23463,11 +24025,11 @@ ${text}
|
|
|
23463
24025
|
skipped.push({ what: "\uBCF4\uAE30", reason: ".base \uB97C \uC77D\uC9C0 \uBABB\uD588\uB2E4" });
|
|
23464
24026
|
return [];
|
|
23465
24027
|
}
|
|
23466
|
-
const
|
|
23467
|
-
for (const p of properties)
|
|
24028
|
+
const idOf2 = /* @__PURE__ */ new Map();
|
|
24029
|
+
for (const p of properties) idOf2.set(p.type === "title" ? "file.name" : p.name, p.id);
|
|
23468
24030
|
const columnId = (raw) => {
|
|
23469
24031
|
if (typeof raw !== "string") return void 0;
|
|
23470
|
-
return
|
|
24032
|
+
return idOf2.get(raw) ?? idOf2.get(raw.replace(/^note\./, ""));
|
|
23471
24033
|
};
|
|
23472
24034
|
const views = [];
|
|
23473
24035
|
const list = Array.isArray(parsed.views) ? parsed.views : [];
|
|
@@ -23762,6 +24324,7 @@ var SyncOrchestrator = class {
|
|
|
23762
24324
|
this.rowSchemas.clear();
|
|
23763
24325
|
this.placement.resetUnpreparedFolders();
|
|
23764
24326
|
this.databaseCreator.reset();
|
|
24327
|
+
this.pusher.takeReadOnlySkips();
|
|
23765
24328
|
if (!options?.dryRun) await this.recovery.recoverInterruptedPushOps();
|
|
23766
24329
|
const plan = await this.planner.planLocalChanges(await this.vaultFs.listMarkdownFileStats());
|
|
23767
24330
|
const view = this.planner.localView(plan);
|
|
@@ -23815,6 +24378,8 @@ var SyncOrchestrator = class {
|
|
|
23815
24378
|
view.lookup,
|
|
23816
24379
|
!options?.paths
|
|
23817
24380
|
);
|
|
24381
|
+
const unsentDeletions2 = applicable.filter((change) => this.unsentDeletion(change)).map((change) => change.path);
|
|
24382
|
+
const readOnlySkipped = await this.pusher.previewReadOnlySkips(applicable);
|
|
23818
24383
|
return {
|
|
23819
24384
|
createdDatabases: newDatabases.size,
|
|
23820
24385
|
created: count("create"),
|
|
@@ -23822,13 +24387,16 @@ var SyncOrchestrator = class {
|
|
|
23822
24387
|
deleted: count("delete"),
|
|
23823
24388
|
moved: count("move"),
|
|
23824
24389
|
failed: refusedFailures,
|
|
23825
|
-
duration: Date.now() - startTime
|
|
24390
|
+
duration: Date.now() - startTime,
|
|
24391
|
+
...unsentDeletions2.length > 0 ? { unsentDeletions: unsentDeletions2 } : {},
|
|
24392
|
+
...readOnlySkipped.length > 0 ? { readOnlySkipped } : {}
|
|
23826
24393
|
};
|
|
23827
24394
|
}
|
|
23828
24395
|
this.stateDb.setMeta(PUSH_IN_PROGRESS_META_KEY, "true");
|
|
23829
24396
|
await this.observation.resolveBotUserId();
|
|
23830
24397
|
const counts = { created: 0, updated: 0, deleted: 0, moved: 0 };
|
|
23831
24398
|
const failed = [...refusedFailures];
|
|
24399
|
+
const unsentDeletions = [];
|
|
23832
24400
|
let completed = 0;
|
|
23833
24401
|
const total = movableFolders.length + applicable.length;
|
|
23834
24402
|
counts.moved += await this.pusher.pushFolderMoves(
|
|
@@ -23856,6 +24424,7 @@ var SyncOrchestrator = class {
|
|
|
23856
24424
|
case "deleted": {
|
|
23857
24425
|
const propagated = await this.pusher.pushDelete(change.path);
|
|
23858
24426
|
if (propagated) counts.deleted++;
|
|
24427
|
+
else if (this.unsentDeletion(change)) unsentDeletions.push(change.path);
|
|
23859
24428
|
break;
|
|
23860
24429
|
}
|
|
23861
24430
|
}
|
|
@@ -24011,9 +24580,20 @@ var SyncOrchestrator = class {
|
|
|
24011
24580
|
deleted: counts.deleted,
|
|
24012
24581
|
moved: counts.moved,
|
|
24013
24582
|
failed,
|
|
24014
|
-
duration: Date.now() - startTime
|
|
24583
|
+
duration: Date.now() - startTime,
|
|
24584
|
+
readOnlySkipped: this.pusher.takeReadOnlySkips(),
|
|
24585
|
+
...unsentDeletions.length > 0 ? { unsentDeletions } : {}
|
|
24015
24586
|
};
|
|
24016
24587
|
}
|
|
24588
|
+
/**
|
|
24589
|
+
* 볼트에서 지운 이 노트를 Notion 에서 지우지 않아 다음 pull 이 되살리나 — deleteSync 가 꺼져 있고 pull 이
|
|
24590
|
+
* 되살리는 레코드일 때. 예전에는 대기로 적기만 하고 알리지 않아, 지운 파일이 말없이 돌아왔다(D8).
|
|
24591
|
+
*/
|
|
24592
|
+
unsentDeletion(change) {
|
|
24593
|
+
if (change.type !== "deleted" || this.config.sync.deleteSync) return false;
|
|
24594
|
+
const record = this.stateDb.getByPath(change.path);
|
|
24595
|
+
return !!record && restoredWhenMissing(record);
|
|
24596
|
+
}
|
|
24017
24597
|
/**
|
|
24018
24598
|
* 만든 DB 에 relation · rollup · formula 를 붙이고, 행 스키마를 다시 읽게 한 뒤 보기를 만든다. 실패는
|
|
24019
24599
|
* 다음 push 가 이어 붙인다 — 행은 그대로 올린다.
|
|
@@ -24086,6 +24666,7 @@ var SyncOrchestrator = class {
|
|
|
24086
24666
|
this.assertConnectionScope();
|
|
24087
24667
|
this.puller.beginPull();
|
|
24088
24668
|
this.rowSchemas.clear();
|
|
24669
|
+
this.databaseSyncer.takeRestoredRows();
|
|
24089
24670
|
const emptyResult = {
|
|
24090
24671
|
created: 0,
|
|
24091
24672
|
updated: 0,
|
|
@@ -24118,6 +24699,7 @@ var SyncOrchestrator = class {
|
|
|
24118
24699
|
this.discovery.clearInlineRefs();
|
|
24119
24700
|
const conflicts = [];
|
|
24120
24701
|
const writtenPaths = [];
|
|
24702
|
+
const restoredPaths = [];
|
|
24121
24703
|
const excluded = [];
|
|
24122
24704
|
const failed = [];
|
|
24123
24705
|
let detected = [];
|
|
@@ -24161,7 +24743,9 @@ var SyncOrchestrator = class {
|
|
|
24161
24743
|
pendingDatabases
|
|
24162
24744
|
);
|
|
24163
24745
|
const finalize = async (created, updated, deleted, restored = 0) => {
|
|
24746
|
+
await this.puller.removeVacatedFolders();
|
|
24164
24747
|
const renamedRows = this.databaseSyncer.takeRenamedRows();
|
|
24748
|
+
restoredPaths.push(...this.databaseSyncer.takeRestoredRows());
|
|
24165
24749
|
const relinked = await this.linkUpdater.apply([...relocated.applied, ...renamedRows.applied]);
|
|
24166
24750
|
const linkCount = writtenPaths.length > 0 ? await this.resolveNotionLinks(writtenPaths) : 0;
|
|
24167
24751
|
if (!options?.paths) {
|
|
@@ -24177,6 +24761,7 @@ var SyncOrchestrator = class {
|
|
|
24177
24761
|
this.stateDb.setMeta(LAST_FULL_PULL_META_KEY, startedAt);
|
|
24178
24762
|
}
|
|
24179
24763
|
this.detector.savePendingDatabases(ledger);
|
|
24764
|
+
this.detector.saveDeletionSweep();
|
|
24180
24765
|
}
|
|
24181
24766
|
this.stateDb.setMeta(LAST_SYNC_META_KEY, (/* @__PURE__ */ new Date()).toISOString());
|
|
24182
24767
|
this.stateDb.setMeta(PULL_IN_PROGRESS_META_KEY, "");
|
|
@@ -24185,6 +24770,7 @@ var SyncOrchestrator = class {
|
|
|
24185
24770
|
updated,
|
|
24186
24771
|
deleted,
|
|
24187
24772
|
restored,
|
|
24773
|
+
...restoredPaths.length > 0 ? { restoredPaths } : {},
|
|
24188
24774
|
conflicts,
|
|
24189
24775
|
writtenPaths,
|
|
24190
24776
|
failed,
|
|
@@ -24196,7 +24782,8 @@ var SyncOrchestrator = class {
|
|
|
24196
24782
|
...remoteScanInfo(
|
|
24197
24783
|
scan,
|
|
24198
24784
|
this.stateDb.getMeta(LAST_FULL_PULL_META_KEY),
|
|
24199
|
-
this.config.sync.deleteSync
|
|
24785
|
+
this.config.sync.deleteSync,
|
|
24786
|
+
this.detector.deletionsChecked()
|
|
24200
24787
|
),
|
|
24201
24788
|
skippedDatabases: ledger.skipped
|
|
24202
24789
|
},
|
|
@@ -24247,6 +24834,10 @@ var SyncOrchestrator = class {
|
|
|
24247
24834
|
);
|
|
24248
24835
|
return {
|
|
24249
24836
|
...plan,
|
|
24837
|
+
remoteScan: plan.remoteScan && {
|
|
24838
|
+
...plan.remoteScan,
|
|
24839
|
+
deletionsDeferred: plan.remoteScan.deletionsDeferred && !this.detector.deletionsChecked()
|
|
24840
|
+
},
|
|
24250
24841
|
moved: relocated.moved,
|
|
24251
24842
|
moveSkipped: relocated.skipped,
|
|
24252
24843
|
excluded: [...exclusions.values()]
|
|
@@ -24340,8 +24931,10 @@ var SyncOrchestrator = class {
|
|
|
24340
24931
|
conflicts.push(outcome.conflict);
|
|
24341
24932
|
} else if (outcome.action === "written") {
|
|
24342
24933
|
writtenPaths.push(outcome.path);
|
|
24343
|
-
if (restoreIds.has(change.pageId))
|
|
24344
|
-
|
|
24934
|
+
if (restoreIds.has(change.pageId)) {
|
|
24935
|
+
counts.restored++;
|
|
24936
|
+
restoredPaths.push(outcome.path);
|
|
24937
|
+
} else counts.updated++;
|
|
24345
24938
|
}
|
|
24346
24939
|
return {
|
|
24347
24940
|
path: outcome.path,
|
|
@@ -24569,7 +25162,12 @@ var SyncOrchestrator = class {
|
|
|
24569
25162
|
this.stateDb,
|
|
24570
25163
|
this.config.notion.rootPageId
|
|
24571
25164
|
);
|
|
24572
|
-
return {
|
|
25165
|
+
return {
|
|
25166
|
+
databases,
|
|
25167
|
+
pages,
|
|
25168
|
+
nestedFolderPages: findNestedFolderPages(this.stateDb.getAll()),
|
|
25169
|
+
complete: databases.complete && (pages?.complete ?? true)
|
|
25170
|
+
};
|
|
24573
25171
|
}
|
|
24574
25172
|
async fetch() {
|
|
24575
25173
|
return this.gate.run("fetch", () => this.executeFetch());
|
|
@@ -24814,6 +25412,19 @@ var NodeVaultFS = class {
|
|
|
24814
25412
|
const fullPath = join(this.rootPath, path);
|
|
24815
25413
|
await unlink(fullPath);
|
|
24816
25414
|
}
|
|
25415
|
+
/** `rmdir` 는 비지 않은 폴더를 지우지 않는다(ENOTEMPTY) — 숨김 파일도 그대로 둔다. */
|
|
25416
|
+
async removeEmptyFolder(path) {
|
|
25417
|
+
try {
|
|
25418
|
+
await rmdir(join(this.rootPath, path));
|
|
25419
|
+
return true;
|
|
25420
|
+
} catch (error) {
|
|
25421
|
+
const code = error.code;
|
|
25422
|
+
if (code === "ENOTEMPTY" || code === "EEXIST" || code === "ENOENT" || code === "ENOTDIR") {
|
|
25423
|
+
return false;
|
|
25424
|
+
}
|
|
25425
|
+
throw error;
|
|
25426
|
+
}
|
|
25427
|
+
}
|
|
24817
25428
|
async moveFile(from, to) {
|
|
24818
25429
|
const fromPath = join(this.rootPath, from);
|
|
24819
25430
|
const toPath = join(this.rootPath, to);
|
|
@@ -25772,6 +26383,9 @@ var FilteredVaultFS = class {
|
|
|
25772
26383
|
ensureFolder(path) {
|
|
25773
26384
|
return this.inner.ensureFolder(path);
|
|
25774
26385
|
}
|
|
26386
|
+
async removeEmptyFolder(path) {
|
|
26387
|
+
return await this.inner.removeEmptyFolder?.(path) ?? false;
|
|
26388
|
+
}
|
|
25775
26389
|
getFileStat(path) {
|
|
25776
26390
|
return this.inner.getFileStat(path);
|
|
25777
26391
|
}
|
|
@@ -25998,6 +26612,6 @@ function formatRatio(ratio) {
|
|
|
25998
26612
|
return String(Math.round(ratio * 100) / 100);
|
|
25999
26613
|
}
|
|
26000
26614
|
|
|
26001
|
-
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, FilteredVaultFS, 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, parseIgnorePatterns, pendingWalBytes, readColumnLayout, readDesignLine, resolvePropertyName, sanitizeFileName, setLogger, sortEntries, stateDbLockPath, stateDbWalPath, stripMarkdownExtension, summarizeFidelity, systemTimeZone, threeWayMerge, toVaultPath, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness, wikilinkLookupsIn };
|
|
26615
|
+
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, FilteredVaultFS, 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, findNestedFolderPages, formatUnifiedDiff, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, hasColumnLayout, hasDesignMarker, inAnyPathScope, isHeadingLine, isHeadingMarkerLine, isInternalPath, isMarkdownPath, isRemoteDeletion, lineDiff, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, normalizeVaultPath, noteNameFromPath, notionIdFromInput, notionIdsEqual, operationLabel, parseDesignColor, parseIgnorePatterns, pendingWalBytes, readColumnLayout, readDesignLine, resolvePropertyName, sanitizeFileName, sectionLinesWithHeadingMarkers, setLogger, sortEntries, stateDbLockPath, stateDbWalPath, stripMarkdownExtension, summarizeFidelity, systemTimeZone, threeWayMerge, toVaultPath, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness, wikilinkLookupsIn };
|
|
26002
26616
|
//# sourceMappingURL=index.js.map
|
|
26003
26617
|
//# sourceMappingURL=index.js.map
|