@wonseok-han/confluence-sync 2.0.1 → 2.0.3
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/conversion/convert.js +1 -1
- package/dist/documents/anchors.js +1 -1
- package/dist/documents/catalog.js +29 -12
- package/dist/documents/obsidian.js +1 -1
- package/dist/site/assets/app.js +4 -4
- package/dist/site/assets/workspace.worker.js +128 -128
- package/dist/site/index.html +2 -2
- package/dist/sync/diff-links.js +37 -0
- package/dist/sync/diff.js +6 -2
- package/dist/sync/local-state.js +38 -2
- package/dist/sync/prepare.js +23 -27
- package/dist/sync/pull.js +3 -3
- package/dist/sync/push.js +2 -2
- package/dist/sync/reconcile.js +2 -2
- package/dist/sync/render.js +4 -4
- package/dist/sync/staging.js +4 -5
- package/dist/web/assets/app.js +4 -4
- package/dist/web/assets/workspace.worker.js +128 -128
- package/docs/user/CLI.md +2 -0
- package/docs/user/WEB.md +2 -0
- package/package.json +1 -1
|
@@ -167,7 +167,7 @@ export async function runConvert(argv) {
|
|
|
167
167
|
}
|
|
168
168
|
const allMd = collectMarkdown(baseDir); // 링크 해석용(전체)
|
|
169
169
|
const mdRels = allMd.map((f) => posix(relative(baseDir, f)));
|
|
170
|
-
const vault = buildVault(mdRels, collectAssets(baseDir).map((f) => posix(relative(baseDir, f))));
|
|
170
|
+
const vault = buildVault(mdRels, collectAssets(baseDir).map((f) => posix(relative(baseDir, f))), baseDir);
|
|
171
171
|
// obsidian 방향의 이름 중복 검사도 트리 전체 기준이어야 한다
|
|
172
172
|
const nameCount = new Map();
|
|
173
173
|
for (const f of allMd) {
|
|
@@ -17,7 +17,7 @@ const FENCE = /^\s*(?:```|~~~)/;
|
|
|
17
17
|
* 대상이 **비어 있을 수 있다** — `[[#헤딩]]` 은 "이 문서의 이 섹션"이라는 Obsidian 표기다.
|
|
18
18
|
* 표 안에서는 별칭 앞 파이프가 `\|` 로 이스케이프되므로 그 백슬래시도 함께 받아 준다.
|
|
19
19
|
*/
|
|
20
|
-
export const WIKILINK_RE = /(!?)\[\[([
|
|
20
|
+
export const WIKILINK_RE = /(!?)\[\[((?:(?!\[\[|\]\])[^|#\n])*)(#(?:(?!\[\[|\]\])[^|\n])+)?(?:\\?\|([^[\]]*))?\]\]/g;
|
|
21
21
|
/**
|
|
22
22
|
* WIKILINK_RE 의 캡처 그룹을 해석한다. 대상도 섹션도 없으면(`[[]]`) 링크가 아니므로 null.
|
|
23
23
|
* 표의 `\|` 때문에 앞 그룹 끝에 딸려온 백슬래시는 여기서 떼어낸다.
|
|
@@ -55,12 +55,12 @@ export function readDoc(baseDir, rel) {
|
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
/** base 상대 경로 → 제목·앵커 인덱스(링크와 섹션 링크를 해석하는 데 둘 다 필요하다) */
|
|
58
|
-
export function buildDocIndex(rels, baseDir) {
|
|
58
|
+
export function buildDocIndex(rels, baseDir, sourceDocs) {
|
|
59
59
|
const titles = {};
|
|
60
60
|
const anchors = {};
|
|
61
61
|
const docs = {};
|
|
62
62
|
for (const rel of rels) {
|
|
63
|
-
const doc = readDoc(baseDir, rel);
|
|
63
|
+
const doc = sourceDocs?.[rel] ?? readDoc(baseDir, rel);
|
|
64
64
|
titles[rel] = doc.title;
|
|
65
65
|
anchors[rel] = doc.anchors;
|
|
66
66
|
docs[rel] = doc;
|
|
@@ -98,7 +98,7 @@ export function resolveAnchorTargets(docs, vault, localTarget) {
|
|
|
98
98
|
let slug;
|
|
99
99
|
if (la.wiki) {
|
|
100
100
|
// dest 가 비면 `[[#헤딩]]` — 같은 문서의 섹션이다
|
|
101
|
-
target = la.dest === '' ? rel : (vault
|
|
101
|
+
target = la.dest === '' ? rel : vaultPath(vault, la.dest, false, rel);
|
|
102
102
|
slug = wikilinkAnchorToSlug(la.fragment);
|
|
103
103
|
}
|
|
104
104
|
else {
|
|
@@ -107,8 +107,10 @@ export function resolveAnchorTargets(docs, vault, localTarget) {
|
|
|
107
107
|
target = null;
|
|
108
108
|
slug = decodeAnchor(la.fragment).toLowerCase();
|
|
109
109
|
}
|
|
110
|
+
// Reference lookup may be ambiguous even when the vault resolved a nearby note.
|
|
111
|
+
// Keep that local target so preprocessing other notes cannot change anchor hashes.
|
|
110
112
|
if (localTarget && la.dest)
|
|
111
|
-
target = localTarget(rel, la.dest, la.wiki);
|
|
113
|
+
target = localTarget(rel, la.dest, la.wiki) ?? target;
|
|
112
114
|
if (!target || !slug)
|
|
113
115
|
continue;
|
|
114
116
|
// 실제로 존재하는 헤딩만 — 없는 앵커는 markdown.ts 가 경고로 잡는다
|
|
@@ -124,30 +126,45 @@ export function resolveAnchorTargets(docs, vault, localTarget) {
|
|
|
124
126
|
* Obsidian 은 `[[파일명]]`(짧은 이름)과 `[[폴더/파일명]]`(경로) 을 모두 허용하므로 둘 다 등록한다.
|
|
125
127
|
* 짧은 이름이 겹치면 경로가 사전순으로 앞서는 쪽이 이긴다(Obsidian 의 "가장 가까운 것" 규칙과는 다르지만 결정적이다).
|
|
126
128
|
*/
|
|
127
|
-
export function buildVault(mdRels, assetRels) {
|
|
129
|
+
export function buildVault(mdRels, assetRels, base) {
|
|
128
130
|
const index = (rels, stripExt) => {
|
|
129
131
|
const idx = {};
|
|
130
132
|
for (const rel of [...rels].sort()) {
|
|
131
133
|
const withoutExt = stripExt ? rel.replace(/\.md$/i, '') : rel;
|
|
132
134
|
for (const key of [withoutExt, basename(withoutExt)]) {
|
|
133
|
-
const k = key.toLowerCase();
|
|
135
|
+
const k = key.normalize('NFC').toLowerCase();
|
|
134
136
|
if (!(k in idx))
|
|
135
137
|
idx[k] = rel; // 먼저 등록된(사전순 앞선) 것이 이긴다
|
|
136
138
|
}
|
|
137
139
|
}
|
|
138
140
|
return idx;
|
|
139
141
|
};
|
|
140
|
-
return { notes: index(mdRels, true), assets: index(assetRels, false) };
|
|
142
|
+
return { notes: index(mdRels, true), assets: index(assetRels, false), rootName: base ? basename(base).normalize('NFC').toLowerCase() : undefined };
|
|
143
|
+
}
|
|
144
|
+
/** Resolve a vault-root path without reading outside the selected workspace. */
|
|
145
|
+
export function vaultPath(vault, target, embed = false, from = '') {
|
|
146
|
+
let key = target.normalize('NFC').toLowerCase();
|
|
147
|
+
if (key.startsWith('/') || key.includes('\\') || key.split('/').some(part => part === '..' || part === '.'))
|
|
148
|
+
return null;
|
|
149
|
+
const noteKey = key.replace(/\.md$/i, '');
|
|
150
|
+
const dir = dirname(from).normalize('NFC').toLowerCase();
|
|
151
|
+
const nearby = !key.includes('/') && dir !== '.' ? vault.notes[dir + '/' + noteKey] : undefined;
|
|
152
|
+
const direct = embed ? (vault.assets[key] ?? vault.notes[noteKey]) : (nearby ?? vault.notes[noteKey] ?? vault.assets[key]);
|
|
153
|
+
if (direct)
|
|
154
|
+
return direct;
|
|
155
|
+
if (!vault.rootName)
|
|
156
|
+
return null;
|
|
157
|
+
const parts = key.split('/'), at = parts.lastIndexOf(vault.rootName);
|
|
158
|
+
if (at < 0)
|
|
159
|
+
return null;
|
|
160
|
+
key = parts.slice(at + 1).join('/');
|
|
161
|
+
return embed ? (vault.assets[key] ?? vault.notes[key.replace(/\.md$/i, '')] ?? null) : (vault.notes[key.replace(/\.md$/i, '')] ?? vault.assets[key] ?? null);
|
|
141
162
|
}
|
|
142
163
|
/** 파일(rel) 기준으로 wikilink 대상을 상대경로로 풀어주는 resolver. */
|
|
143
164
|
export function vaultResolver(rel, vault) {
|
|
144
165
|
const dir = dirname(rel) === '.' ? '' : dirname(rel);
|
|
145
166
|
return (target, embed) => {
|
|
146
|
-
const
|
|
147
|
-
// 임베드(![[...]])는 대개 이미지, 링크([[...]])는 대개 노트 → 각자 먼저 찾아본다
|
|
148
|
-
const hit = embed
|
|
149
|
-
? (vault.assets[target.toLowerCase()] ?? vault.notes[key])
|
|
150
|
-
: (vault.notes[key] ?? vault.assets[target.toLowerCase()]);
|
|
167
|
+
const hit = vaultPath(vault, target, embed, rel);
|
|
151
168
|
if (!hit)
|
|
152
169
|
return null;
|
|
153
170
|
const r = relative(dir || '.', hit).split('\\').join('/');
|
|
@@ -46,7 +46,7 @@ const markdownDisplayText = (text) => text.replace(/[\\_]/g, '\\$&');
|
|
|
46
46
|
* 코드블록(``` / ~~~)과 인라인 코드(`...`) 안은 그대로 둔다 — 링크처럼 생긴 예제 코드를
|
|
47
47
|
* 망가뜨리지 않기 위해서다.
|
|
48
48
|
*/
|
|
49
|
-
function mapProse(md, fn) {
|
|
49
|
+
export function mapProse(md, fn) {
|
|
50
50
|
let inFence = false;
|
|
51
51
|
return md
|
|
52
52
|
.split('\n')
|