@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
package/dist/site/index.html
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="ko"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'sha256-gC1xP5ikAuS9OEjIyx77wCJoX4h4jhP+0cQnKC4FHD4='; connect-src 'self' http://127.0.0.1:* http://localhost:* https://*.blob.vercel-storage.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-src 'self'; worker-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'"><meta name="referrer" content="no-referrer"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="csync-token" content=""><meta name="csync-runtime" content="standalone"><meta name="csync-version" content="2.0.
|
|
3
|
-
<header class="app-header"><a class="brand" href="/" aria-label="confluence-sync 홈"><img class="brand-symbol" src="/assets/favicon.svg?v=paperlink" alt="" aria-hidden="true" width="30" height="30">confluence<span class="brand-light">sync</span></a><span class="app-version" aria-label="현재 버전">v2.0.
|
|
2
|
+
<html lang="ko"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'sha256-gC1xP5ikAuS9OEjIyx77wCJoX4h4jhP+0cQnKC4FHD4='; connect-src 'self' http://127.0.0.1:* http://localhost:* https://*.blob.vercel-storage.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; frame-src 'self'; worker-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'"><meta name="referrer" content="no-referrer"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="csync-token" content=""><meta name="csync-runtime" content="standalone"><meta name="csync-version" content="2.0.3"><title>문서 워크스페이스 · confluence-sync</title><link rel="icon" type="image/svg+xml" href="/assets/favicon.svg?v=paperlink"><link rel="stylesheet" href="/assets/app.css"></head><body>
|
|
3
|
+
<header class="app-header"><a class="brand" href="/" aria-label="confluence-sync 홈"><img class="brand-symbol" src="/assets/favicon.svg?v=paperlink" alt="" aria-hidden="true" width="30" height="30">confluence<span class="brand-light">sync</span></a><span class="app-version" aria-label="현재 버전">v2.0.3</span><span class="local-status"><i></i><span id="local-connection-status">브라우저 폴더 선택 필요</span></span><button id="local-disconnect" class="quiet" type="button" hidden>연결 해제</button></header>
|
|
4
4
|
<main id="workspace-interface" class="app-main">
|
|
5
5
|
<div class="page-heading"><div><div class="eyebrow">DOCUMENT WORKSPACE</div><h1>문서 워크스페이스<span class="title-dot">.</span></h1></div><div class="page-heading-actions"><p>문서를 변환하고, Confluence에서 가져오거나 업로드하세요.</p><button id="open-help" aria-haspopup="dialog" aria-controls="help-dialog">사용 도움말</button></div></div>
|
|
6
6
|
<div class="mode-tabs" role="tablist" aria-label="문서 작업"><button id="tab-convert" role="tab" aria-selected="true" aria-controls="convert-view">문서 변환</button><button id="tab-sync" role="tab" aria-selected="false" aria-controls="sync-view" tabindex="-1">Confluence</button></div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Reuse Pull link/anchor conversion in a disposable tree; user files stay read-only. */
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, resolve, dirname, relative } from 'node:path';
|
|
5
|
+
import { collectMarkdown, buildVault, vaultResolver } from '../documents/catalog.js';
|
|
6
|
+
import { buildFrontmatter, splitFrontmatter, resolveWikilinks } from '../documents/obsidian.js';
|
|
7
|
+
import { anchorPass, relinkPass, readDocInfo } from './pull.js';
|
|
8
|
+
import { normalizeWikiPaths } from './local-state.js';
|
|
9
|
+
import { buildIgnorer } from './ignore.js';
|
|
10
|
+
import { inNestedWorkspace } from './workspace.js';
|
|
11
|
+
export function diffLinks(base, path, title, before, after, wiki, mapping) {
|
|
12
|
+
const ignore = buildIgnorer(base, []), files = collectMarkdown(base).map(file => relative(base, file).split('\\').join('/')).filter(file => !ignore.ignores(file) && !inNestedWorkspace(base, file));
|
|
13
|
+
const stage = mkdtempSync(join(tmpdir(), 'csync-diff-'));
|
|
14
|
+
try {
|
|
15
|
+
const file = resolve(stage, path), paths = new Map(), refs = new Map();
|
|
16
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
17
|
+
writeFileSync(file, buildFrontmatter({ title }) + before);
|
|
18
|
+
for (const rel of files) {
|
|
19
|
+
const entry = mapping[rel];
|
|
20
|
+
if (!entry || entry.type === 'folder')
|
|
21
|
+
continue;
|
|
22
|
+
const target = resolve(stage, rel);
|
|
23
|
+
paths.set(entry.pageId, target);
|
|
24
|
+
if (rel !== path)
|
|
25
|
+
refs.set(target, readDocInfo(resolve(base, rel)));
|
|
26
|
+
}
|
|
27
|
+
const pass = anchorPass([file], paths, refs);
|
|
28
|
+
relinkPass([file], paths, wiki, pass.info);
|
|
29
|
+
const body = splitFrontmatter(readFileSync(file, 'utf8')).body.replace(/^\r?\n/, '');
|
|
30
|
+
const vault = buildVault(files, [], base), lookup = vaultResolver(path, vault);
|
|
31
|
+
const canonical = (text) => resolveWikilinks(normalizeWikiPaths(base, path, text, files), lookup);
|
|
32
|
+
return { before: canonical(body), after: canonical(after) };
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
rmSync(stage, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/sync/diff.js
CHANGED
|
@@ -3,6 +3,7 @@ import { bold, cyan, red, green, dim } from '../shared/colors.js';
|
|
|
3
3
|
import { readFileSync, statSync } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { readBaseline } from './baseline.js';
|
|
6
|
+
import { diffLinks } from './diff-links.js';
|
|
6
7
|
import { loadMapping } from './mapping.js';
|
|
7
8
|
import { mappingPath } from './metadata.js';
|
|
8
9
|
import { loadWorkspace, requireScope, inScope, inNestedWorkspace, scopePath, assertWorkspaceConnection, resolveSyncBase } from './workspace.js';
|
|
@@ -35,8 +36,11 @@ export async function documentDiff(base, input, options = {}) {
|
|
|
35
36
|
const localBody = splitFrontmatter(after).body;
|
|
36
37
|
// buildFrontmatter adds one blank separator. Keep intentional body whitespace.
|
|
37
38
|
const comparisonBody = localBody !== after ? localBody.replace(/^\r?\n/, '') : localBody;
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
const obsidian = /^ {0,3}(?:> ?)+\[![\w-]+\]|\[\^[^\]\s]+\]:|(?<!\\)\^\[|\[\[/m.test(comparisonBody);
|
|
40
|
+
const remote = htmlToMarkdown(node.html, { obsidian, pageId: entry.pageId, storage: node.storage, codeLangs: codeLanguagesFromStorage(node.storage) });
|
|
41
|
+
const comparison = diffLinks(base, path, node.title, remote, comparisonBody, obsidian, mapping);
|
|
42
|
+
return { path, ...comparison,
|
|
43
|
+
source: 'remote', canRemote: true, note: `현재 원격 v${node.version ?? '?'} → 로컬 본문 비교입니다. 원격 HTML을 근사 변환하고 연결된 문서 링크를 로컬 경로로 맞췄으며 frontmatter는 제외합니다. 과거 동기화 기준과의 비교는 아닙니다. 변경 목록에는 본문 외 제목·원격 위치 변경도 포함됩니다.` };
|
|
40
44
|
}
|
|
41
45
|
if (entry?.sourceObject)
|
|
42
46
|
return { path, before: readBaseline(base, entry.sourceObject), after, source: 'baseline', canRemote: true, note: '마지막 Pull·Push 성공 시 저장한 Markdown → 현재 로컬 문서입니다. 첨부 파일의 바이트 차이는 본문 diff에 표시하지 않습니다.' };
|
package/dist/sync/local-state.js
CHANGED
|
@@ -2,11 +2,47 @@
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { readBaseline } from './baseline.js';
|
|
6
|
+
import { buildVault, vaultPath } from '../documents/catalog.js';
|
|
7
|
+
import { WIKILINK_RE } from '../documents/anchors.js';
|
|
8
|
+
import { mapProse, splitFrontmatter } from '../documents/obsidian.js';
|
|
9
|
+
export function localStateHash(base, rel, rendered, source) {
|
|
10
|
+
const hash = createHash('sha256').update(source ?? readFileSync(resolve(base, rel)));
|
|
7
11
|
for (const asset of [...rendered.attachments].sort((a, b) => a.filename.localeCompare(b.filename))) {
|
|
8
12
|
hash.update('\0' + asset.filename + '\0');
|
|
9
13
|
hash.update(createHash('sha256').update(readFileSync(asset.abs)).digest());
|
|
10
14
|
}
|
|
11
15
|
return hash.digest('hex');
|
|
12
16
|
}
|
|
17
|
+
/** Ignore only rewritten note paths; labels, fragments, code and assets remain exact. */
|
|
18
|
+
export function matchesLocalState(base, rel, rendered, entry, files, actual = localStateHash(base, rel, rendered)) {
|
|
19
|
+
if (entry.localHash === actual)
|
|
20
|
+
return true;
|
|
21
|
+
if (!entry.localHash || !entry.sourceObject)
|
|
22
|
+
return false;
|
|
23
|
+
const before = readBaseline(base, entry.sourceObject), after = readFileSync(resolve(base, rel), 'utf8');
|
|
24
|
+
// Reusing the old source with current attachments must reproduce the stored hash.
|
|
25
|
+
if (localStateHash(base, rel, rendered, before) !== entry.localHash)
|
|
26
|
+
return false;
|
|
27
|
+
return normalizeWikiPaths(base, rel, before, files) === normalizeWikiPaths(base, rel, after, files);
|
|
28
|
+
}
|
|
29
|
+
export function normalizeWikiPaths(base, rel, text, files) {
|
|
30
|
+
const vault = buildVault(files, [], base);
|
|
31
|
+
const { body } = splitFrontmatter(text), prefix = text.slice(0, text.length - body.length);
|
|
32
|
+
return prefix + mapProse(body, part => part.replace(WIKILINK_RE, (whole, bang, target, hash, alias) => {
|
|
33
|
+
if (bang)
|
|
34
|
+
return whole;
|
|
35
|
+
const name = target.replace(/\\$/, '').trim(), path = vaultPath(vault, name, false, rel);
|
|
36
|
+
if (!path)
|
|
37
|
+
return whole;
|
|
38
|
+
// A short ambiguous name is safe only when the target is in this directory.
|
|
39
|
+
if (!name.includes('/')) {
|
|
40
|
+
const key = name.replace(/\.md$/i, '').normalize('NFC').toLowerCase();
|
|
41
|
+
const candidates = files.filter(file => file.split('/').at(-1).replace(/\.md$/i, '').normalize('NFC').toLowerCase() === key);
|
|
42
|
+
const nearby = (rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/') + 1) : '') + name.replace(/\.md$/i, '') + '.md';
|
|
43
|
+
if (candidates.length > 1 && path.normalize('NFC').toLowerCase() !== nearby.normalize('NFC').toLowerCase())
|
|
44
|
+
return whole;
|
|
45
|
+
}
|
|
46
|
+
return `[[${path}${hash ?? ''}|${alias ?? name}]]`;
|
|
47
|
+
}));
|
|
48
|
+
}
|
package/dist/sync/prepare.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import {
|
|
5
|
+
import { readDoc } from '../documents/catalog.js';
|
|
6
6
|
import { prepareMarkdown } from '../conversion/markdown.js';
|
|
7
7
|
import { within } from './workspace.js';
|
|
8
8
|
import { buildTreeRenderer } from './render.js';
|
|
@@ -12,38 +12,34 @@ export async function preparePushTree(base, rels, selected, options) {
|
|
|
12
12
|
try {
|
|
13
13
|
const chosen = new Set(selected);
|
|
14
14
|
const sourceRenderer = buildTreeRenderer(base, rels);
|
|
15
|
-
const vault =
|
|
16
|
-
|
|
15
|
+
const vault = sourceRenderer.vault;
|
|
16
|
+
const preparedDocs = { ...sourceRenderer.docs };
|
|
17
|
+
for (const rel of rels.filter(rel => chosen.has(rel))) {
|
|
17
18
|
const original = resolve(base, rel), destination = resolve(stage, rel);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const target = resolve(stage, asset);
|
|
25
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
26
|
-
copyFileSync(source, target);
|
|
27
|
-
}
|
|
19
|
+
// Preview renders actual temporary PNGs too, so attachment hashes match Push.
|
|
20
|
+
const converted = await prepareMarkdown(readFileSync(original, 'utf8'), original, base, vault, stage, false, undefined, false);
|
|
21
|
+
for (const [asset, source] of converted.copies) {
|
|
22
|
+
const target = resolve(stage, asset);
|
|
23
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
24
|
+
copyFileSync(source, target);
|
|
28
25
|
}
|
|
29
|
-
// Use the
|
|
26
|
+
// Use the source renderer as the final asset authority: conversion may
|
|
30
27
|
// miss balanced parentheses in Markdown paths or assets inside raw HTML.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
copyFileSync(asset.abs, target);
|
|
41
|
-
}
|
|
28
|
+
const doc = sourceRenderer.docs[rel];
|
|
29
|
+
for (const asset of sourceRenderer.render(rel, doc.body, doc.title).attachments) {
|
|
30
|
+
if (!within(base, asset.abs) || !existsSync(asset.abs))
|
|
31
|
+
continue;
|
|
32
|
+
const target = resolve(stage, relative(base, asset.abs));
|
|
33
|
+
if (existsSync(target))
|
|
34
|
+
continue;
|
|
35
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
36
|
+
copyFileSync(asset.abs, target);
|
|
42
37
|
}
|
|
43
38
|
mkdirSync(dirname(destination), { recursive: true });
|
|
44
|
-
writeFileSync(destination, text);
|
|
39
|
+
writeFileSync(destination, converted.text);
|
|
40
|
+
preparedDocs[rel] = readDoc(stage, rel);
|
|
45
41
|
}
|
|
46
|
-
return { ...buildTreeRenderer(stage, rels,
|
|
42
|
+
return { ...buildTreeRenderer(stage, rels, { referenceRoots: [], ...options, referenceBase: base }, preparedDocs), original: sourceRenderer, dispose };
|
|
47
43
|
}
|
|
48
44
|
catch (error) {
|
|
49
45
|
dispose();
|
package/dist/sync/pull.js
CHANGED
|
@@ -2,7 +2,7 @@ import { progressStep } from '../shared/progress.js';
|
|
|
2
2
|
import { folderMoves, relocated, folderStamp, checkFolderDestination, verifyRemoteFolderMoves } from './remote-folder-moves.js';
|
|
3
3
|
import { connectWorkspace } from './scope.js';
|
|
4
4
|
import { storeBaseline } from './baseline.js';
|
|
5
|
-
import { localStateHash } from './local-state.js';
|
|
5
|
+
import { localStateHash, matchesLocalState } from './local-state.js';
|
|
6
6
|
import { buildTreeRenderer } from './render.js';
|
|
7
7
|
/**
|
|
8
8
|
* `confluence-sync pull <pageId|url> [--out <dir>] (하위 항목 포함)`
|
|
@@ -215,7 +215,7 @@ async function pullNode(ctx, id, destDir) {
|
|
|
215
215
|
throw new PullError(`pull 실패 #${id}: ${e.message}`);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
-
function readDocInfo(file) {
|
|
218
|
+
export function readDocInfo(file) {
|
|
219
219
|
const { data, body } = splitFrontmatter(readFileSync(file, 'utf8'));
|
|
220
220
|
const headings = collectHeadings(body);
|
|
221
221
|
return {
|
|
@@ -620,7 +620,7 @@ export async function runPull(argv, options = {}) {
|
|
|
620
620
|
for (const rel of scoped) {
|
|
621
621
|
const doc = tree.docs[rel], rendered = tree.render(rel, doc.body, doc.title), entry = oldMapping[rel];
|
|
622
622
|
const clean = entry?.localHash
|
|
623
|
-
?
|
|
623
|
+
? matchesLocalState(baseDir, rel, rendered, entry, rels)
|
|
624
624
|
: !!entry?.hash && (entry.hash === docHash(doc.title, rendered)
|
|
625
625
|
|| entry.hash === docHash(legacy.docs[rel].title, legacy.render(rel, legacy.docs[rel].body, legacy.docs[rel].title))
|
|
626
626
|
|| entry.hash === docHash(doc.title, tree.render(rel, doc.body, doc.title, 'panels'))
|
package/dist/sync/push.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { progressStep } from '../shared/progress.js';
|
|
2
2
|
import { storeBaseline } from './baseline.js';
|
|
3
3
|
import { buildTreeRenderer } from './render.js';
|
|
4
|
-
import { localStateHash } from './local-state.js';
|
|
4
|
+
import { localStateHash, matchesLocalState } from './local-state.js';
|
|
5
5
|
import { loadIndex, stagedPaths, assertStaged, completeStaged } from './staging.js';
|
|
6
6
|
/** Incremental Confluence push and local preview orchestration. */
|
|
7
7
|
import { mappingPath } from './metadata.js';
|
|
@@ -144,7 +144,7 @@ export async function runPush(argv) {
|
|
|
144
144
|
const legacy = buildTreeRenderer(BASE_DIR, allRel);
|
|
145
145
|
const locallyClean = (rel) => {
|
|
146
146
|
const entry = mapping[rel];
|
|
147
|
-
return entry?.localHash ?
|
|
147
|
+
return entry?.localHash ? matchesLocalState(BASE_DIR, rel, render(rel, docs[rel].body, docs[rel].title), entry, allRel, localHashes[rel]) : !!entry?.hash && (entry.hash === hashes[rel]
|
|
148
148
|
|| entry.hash === docHash(legacy.docs[rel].title, legacy.render(rel, legacy.docs[rel].body, legacy.docs[rel].title))
|
|
149
149
|
|| entry.hash === docHash(docs[rel].title, render(rel, docs[rel].body, docs[rel].title, 'panels'))
|
|
150
150
|
|| entry.hash === docHash(docs[rel].title, render(rel, docs[rel].body, docs[rel].title, true))
|
package/dist/sync/reconcile.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import { buildFolderIndex, parentKeyOf, readDoc } from '../documents/catalog.js';
|
|
5
|
-
export function reconcilePages(base, mapping, allFiles, selected, scope) {
|
|
5
|
+
export function reconcilePages(base, mapping, allFiles, selected, scope, sourceDocs) {
|
|
6
6
|
const next = structuredClone(mapping);
|
|
7
7
|
const byId = new Map(Object.entries(mapping).map(([key, entry]) => [entry.pageId, { key, entry }]));
|
|
8
8
|
const oldFolders = buildFolderIndex(Object.keys(mapping).filter(key => key.endsWith('.md')));
|
|
9
9
|
const folders = buildFolderIndex(allFiles);
|
|
10
|
-
const docs = new Map(allFiles.map(key => [key, readDoc(base, key)]));
|
|
10
|
+
const docs = new Map(allFiles.map(key => [key, sourceDocs?.[key] ?? readDoc(base, key)]));
|
|
11
11
|
const seen = new Map();
|
|
12
12
|
for (const [key, doc] of docs) {
|
|
13
13
|
const id = doc.fm.pageId ?? mapping[key]?.pageId;
|
package/dist/sync/render.js
CHANGED
|
@@ -11,10 +11,10 @@ import { buildReferenceLookup } from './references.js';
|
|
|
11
11
|
import { relative } from 'node:path';
|
|
12
12
|
import { toStorage } from '../documents/markdown.js';
|
|
13
13
|
import { buildDocIndex, buildVault, collectAssets, vaultResolver, resolveAnchorTargets, } from '../documents/catalog.js';
|
|
14
|
-
export function buildTreeRenderer(baseDir, rels, options) {
|
|
15
|
-
const { titles, anchors, docs } = buildDocIndex(rels, baseDir);
|
|
14
|
+
export function buildTreeRenderer(baseDir, rels, options, sourceDocs) {
|
|
15
|
+
const { titles, anchors, docs } = buildDocIndex(rels, baseDir, sourceDocs);
|
|
16
16
|
// Obsidian [[wikilink]] 해석용 이름 인덱스
|
|
17
|
-
const vault = buildVault(rels, collectAssets(baseDir).map((f) => relative(baseDir, f)));
|
|
17
|
+
const vault = buildVault(rels, collectAssets(baseDir).map((f) => relative(baseDir, f).split('\\').join('/')), options?.referenceBase ?? baseDir);
|
|
18
18
|
// 누가 어느 헤딩을 가리키는지 먼저 모은다 — 그 헤딩에만 Anchor 매크로를 심는다.
|
|
19
19
|
const lookup = options ? buildReferenceLookup(options.referenceBase ?? baseDir, rels, options.referenceRoots, options.baseUrl) : null;
|
|
20
20
|
const anchorTargets = resolveAnchorTargets(docs, vault, lookup?.localTarget);
|
|
@@ -44,5 +44,5 @@ export function buildTreeRenderer(baseDir, rels, options) {
|
|
|
44
44
|
});
|
|
45
45
|
return { ...result, linkWarnings: [...warnings], referenceLinks: [...references] };
|
|
46
46
|
};
|
|
47
|
-
return { titles, docs, render };
|
|
47
|
+
return { titles, docs, render, vault };
|
|
48
48
|
}
|
package/dist/sync/staging.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { green, cyan } from '../shared/colors.js';
|
|
2
2
|
import { reconcilePages } from './reconcile.js';
|
|
3
|
-
import {
|
|
4
|
-
import { buildTreeRenderer } from './render.js';
|
|
3
|
+
import { matchesLocalState } from './local-state.js';
|
|
5
4
|
/** Document-level upload selection. Source files are never copied into the index. */
|
|
6
5
|
import { createHash, randomUUID } from 'node:crypto';
|
|
7
6
|
import { existsSync, lstatSync, readFileSync, mkdirSync, writeFileSync, renameSync, unlinkSync, realpathSync } from 'node:fs';
|
|
@@ -101,8 +100,8 @@ export async function inspectStaging(base, options = {}, only) {
|
|
|
101
100
|
const prepared = await preparePushTree(base, all, selected, { referenceRoots: options.referenceRoots ?? [], baseUrl: options.baseUrl });
|
|
102
101
|
try {
|
|
103
102
|
const originalMapping = loadMapping(mappingPath(base));
|
|
104
|
-
const mapping = reconcilePages(base, originalMapping, all, selected, requireScope(workspace));
|
|
105
|
-
const legacy =
|
|
103
|
+
const mapping = reconcilePages(base, originalMapping, all, selected, requireScope(workspace), prepared.original.docs);
|
|
104
|
+
const legacy = prepared.original;
|
|
106
105
|
const rows = selected.map(path => {
|
|
107
106
|
const doc = prepared.docs[path], rendered = prepared.render(path, doc.body, doc.title), hash = docHash(doc.title, rendered);
|
|
108
107
|
if (sourceHash(base, path) !== sources[path])
|
|
@@ -110,7 +109,7 @@ export async function inspectStaging(base, options = {}, only) {
|
|
|
110
109
|
const oldPath = doc.fm.pageId && Object.keys(originalMapping).find(key => originalMapping[key].pageId === doc.fm.pageId && !key.endsWith('/'));
|
|
111
110
|
const prior = originalMapping[path] ?? (oldPath ? originalMapping[oldPath] : undefined);
|
|
112
111
|
const reconciled = mapping[path];
|
|
113
|
-
const clean = prior?.localHash ?
|
|
112
|
+
const clean = prior?.localHash ? matchesLocalState(base, path, rendered, prior, all) : !!prior?.hash && (prior.hash === hash || prior.hash === docHash(legacy.docs[path].title, legacy.render(path, legacy.docs[path].body, legacy.docs[path].title))
|
|
114
113
|
|| prior.hash === docHash(doc.title, prepared.render(path, doc.body, doc.title, 'panels'))
|
|
115
114
|
|| prior.hash === docHash(doc.title, prepared.render(path, doc.body, doc.title, true))
|
|
116
115
|
|| prior.hash === docHash(legacy.docs[path].title, legacy.render(path, legacy.docs[path].body, legacy.docs[path].title, 'panels'))
|