@wonseok-han/confluence-sync 1.4.0 → 1.6.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/README.md +64 -33
- package/dist/cli/args.js +14 -0
- package/dist/{help.js → cli/help.js} +11 -3
- package/dist/{init.js → cli/init.js} +1 -1
- package/dist/cli/main.js +38 -0
- package/dist/{confluence.js → confluence/client.js} +1 -1
- package/dist/{config.js → confluence/config.js} +1 -1
- package/dist/{convert.js → conversion/convert.js} +4 -4
- package/dist/{footnotes.js → conversion/footnotes.js} +1 -1
- package/dist/{pdf-pages.js → conversion/pdf-pages.js} +1 -1
- package/dist/{repair.js → conversion/repair.js} +2 -2
- package/dist/conversion/worker.js +9 -0
- package/dist/{docs.js → documents/catalog.js} +3 -1
- package/dist/{markdown.js → documents/markdown.js} +11 -5
- package/dist/documents/mermaid.js +13 -0
- package/dist/{pull.js → sync/pull.js} +8 -8
- package/dist/sync/push.js +302 -0
- package/dist/sync/references.js +93 -0
- package/dist/sync/render.js +47 -0
- package/dist/sync.js +2 -348
- package/dist/web/assets/Monaco-LICENSE.txt +21 -0
- package/dist/web/assets/Monaco-ThirdPartyNotices.txt +448 -0
- package/dist/web/assets/app.css +1 -0
- package/dist/web/assets/app.js +939 -0
- package/dist/web/assets/editor.worker.js +30 -0
- package/dist/web/folder-dialog.js +44 -0
- package/dist/web/output.js +139 -0
- package/dist/web/server.js +346 -0
- package/dist/web/sync.js +226 -0
- package/dist/web/views/help.js +33 -0
- package/dist/web/views/sync-panel.js +26 -0
- package/dist/web/views/workspace.js +39 -0
- package/package.json +5 -2
- package/dist/render.js +0 -27
- /package/dist/{attachments.js → conversion/attachments.js} +0 -0
- /package/dist/{pdf-renderer.js → conversion/pdf-renderer.js} +0 -0
- /package/dist/{anchors.js → documents/anchors.js} +0 -0
- /package/dist/{html2md.js → documents/html-to-markdown.js} +0 -0
- /package/dist/{obsidian.js → documents/obsidian.js} +0 -0
- /package/dist/{colors.js → shared/colors.js} +0 -0
- /package/dist/{ignore.js → sync/ignore.js} +0 -0
- /package/dist/{mapping.js → sync/mapping.js} +0 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ confluence-sync --version
|
|
|
20
20
|
confluence-sync --help
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Node.js 22.13 이상이 필요합니다.
|
|
24
24
|
|
|
25
25
|
> 짧은 이름 **`csync`** 도 같이 설치됩니다 — 아래 예시의 `confluence-sync` 는 전부 `csync` 로 바꿔 쓸 수 있습니다.
|
|
26
26
|
|
|
@@ -42,6 +42,43 @@ confluence-sync pull --space --out ./docs # 스페이스 전체를 .md 로
|
|
|
42
42
|
|
|
43
43
|
---
|
|
44
44
|
|
|
45
|
+
## 웹에서 변환·동기화하기
|
|
46
|
+
|
|
47
|
+
화면 상단의 **사용 도움말**에서 변환·동기화 순서, 참조 링크, 덮어쓰기와 로그 설명을 확인할 수 있습니다.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
csync web # 홈 폴더에서 시작, 브라우저 자동 열기
|
|
51
|
+
csync web --base ~/Documents # 원하는 폴더에서 시작
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
1. **변환 폴더 선택…**으로 시스템 폴더 다이얼로그를 열어 Vault나 문서 묶음의 기준 폴더를 지정합니다. 경로를 붙여 넣고 **열기**를 눌러도 됩니다.
|
|
55
|
+
2. 파일 이름을 검색하면 기준 폴더의 모든 하위 폴더에서 `.md` 문서를 찾고 상대 경로를 함께 표시합니다. 검색어를 지우면 현재 폴더 목록으로 돌아갑니다. 문서 트리에서 폴더 옆 화살표는 하위 항목을 펼치거나 접고, 폴더·파일 이름은 변환 대상을 선택합니다. 폴더 이름을 선택하면 하위 Markdown 전체, 파일 이름을 선택하면 그 문서 하나가 대상입니다. 펼침·접기는 선택한 대상을 바꾸지 않습니다.
|
|
56
|
+
3. **출력 폴더 선택…**으로 저장 위치를 지정합니다. 새 폴더 경로를 입력할 수도 있으며, 비워두면 기준 폴더 옆에 `폴더명-converted-XXXXXX`를 만듭니다.
|
|
57
|
+
4. **미리보기**를 누르면 본문 비교 화면에 삭제·추가된 부분이 색상으로 표시됩니다. 나란히/인라인 비교와 이전·다음 변경 이동을 지원합니다.
|
|
58
|
+
5. **변환 후 저장**을 누르고 **결과 폴더 열기**로 확인합니다.
|
|
59
|
+
|
|
60
|
+
웹에서 파일 하나를 선택하면 출력 폴더 바로 아래에 저장합니다. 폴더 전체를 선택하면 모든 하위 Markdown을 변환하고, 선택한 폴더 내부의 상대 경로만 출력 폴더 아래에 유지합니다. 링크 해석 기준은 단일 파일의 부모 폴더 또는 선택한 폴더입니다. CLI의 `--base` 동작은 그대로입니다. 원본과 겹치는 출력 경로는 거부하며, 기존 출력 파일과 내용이 같으면 재사용하므로 같은 폴더에 연속 저장할 수 있습니다. 이름은 같지만 내용이 다르면 대상 파일 목록을 보여주고 덮어쓸지 확인합니다. 취소하면 저장하지 않으며, 확인한 기존 파일이 변경되면 다시 확인합니다. **다른 문서 선택**을 누르면 기준 폴더와 변환 설정을 유지한 채 다음 문서를 고를 수 있습니다. 선택한 대상의 문서와 참조 첨부를 내보냅니다. 단일 파일 모드에서는 링크로 참조된 다른 문서를 함께 복사하지 않습니다. 폴더 변환의 본문 비교는 최대 20개 문서(문서당 200KB 이하)를 선택해서 확인하며, 실제 저장은 전체 문서에 적용됩니다.
|
|
61
|
+
|
|
62
|
+
변환 미리보기는 원본 파일을 변경하지 않습니다. 폴더 선택창을 사용할 수 없는 환경에서는 경로를 직접 입력할 수 있습니다. Linux에서 폴더 선택창을 사용하려면 `zenity`가 필요합니다.
|
|
63
|
+
|
|
64
|
+
웹은 `http://localhost:4318` 또는 `http://127.0.0.1:4318`에서 열 수 있습니다. 두 주소의 브라우저 저장소는 별개이므로 기존 경로 설정을 유지하려면 같은 주소로 접속하세요. 변환과 동기화 미리보기는 Confluence 인증 없이 사용할 수 있습니다. 내 컴퓨터의 파일을 탐색하므로 개인용 로컬 실행을 전제로 합니다. `--port 4320`으로 포트를 바꾸거나 `--no-open`으로 자동 열기를 끌 수 있습니다. 종료는 터미널에서 `Ctrl+C`입니다.
|
|
65
|
+
|
|
66
|
+
변환 탭과 동기화 탭은 기준 폴더·탐색 위치·선택 문서를 각각 기억합니다. 예를 들어 변환은 Obsidian Vault를 기준으로 하고, 동기화는 변환 결과 폴더를 기준으로 지정할 수 있습니다. 각 탭의 경로와 출력 경로·설정 파일 경로·작업 옵션은 같은 브라우저의 로컬 저장소에 보관되어 탭 전환과 새로고침 후에도 유지됩니다. 삭제되거나 열 수 없는 경로는 안내 후 선택을 해제하거나 시작 폴더로 돌아갑니다. 인증 토큰과 동기화 실행 승인은 저장하지 않습니다.
|
|
67
|
+
|
|
68
|
+
### Confluence 동기화 탭
|
|
69
|
+
|
|
70
|
+
1. 왼쪽에서 기준 폴더를 열고 **Confluence 동기화** 탭을 선택합니다.
|
|
71
|
+
2. **선택 문서 / 현재 폴더 / 기준 폴더 전체** 중 업로드 범위를 고릅니다.
|
|
72
|
+
3. **설정 확인**으로 서버·계정·스페이스·상위 페이지를 확인합니다. 기준 폴더의 `.env`를 자동으로 사용하며, 다른 `.env` 파일 경로를 직접 지정할 수도 있습니다. 기준 폴더에 `.env`가 없으면 웹 서버 실행 환경을 사용합니다. 지정한 파일은 다른 환경과 섞지 않으며, 토큰 값은 화면에 표시하지 않습니다.
|
|
73
|
+
4. **동기화 미리보기**로 기존 CLI의 `--dry-run` 결과(신규·변경·동일·계층)를 확인합니다. 이 단계는 외부 API를 호출하지 않으므로 서버 연결·권한을 확인하는 기능은 아닙니다.
|
|
74
|
+
5. **Confluence에 동기화**를 누르면 기존 CLI와 동일하게 신규 페이지를 만들고 변경된 페이지·첨부를 업로드합니다. `.confluence-syncignore`와 기준 폴더의 `.confluence-sync.json`을 사용합니다.
|
|
75
|
+
|
|
76
|
+
진행 내역은 화면에 표시되며, 같은 서버가 실행 중이면 새로고침 후에도 마지막 작업 상태를 확인할 수 있습니다. 대상·옵션·문서·설정이 바뀌면 다시 미리보기해야 합니다. 실패 시 일부 문서는 이미 반영됐을 수 있으므로 로그와 미리보기를 확인한 뒤 재실행하세요. 웹에서는 `--rebuild`(전체 삭제 후 재생성), 강제 갱신 및 pull을 제공하지 않습니다.
|
|
77
|
+
|
|
78
|
+
참조 폴더를 여러 개 지정하는 방법과 링크 검색 규칙은 [다른 동기화 폴더의 문서 링크](#다른-동기화-폴더의-문서-링크)를 참고하세요.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
45
82
|
## 설정
|
|
46
83
|
|
|
47
84
|
Confluence 접속 정보를 담은 `.env` 가 **실행 위치(cwd)** 에 있어야 합니다(셸 환경변수도 가능).
|
|
@@ -130,11 +167,31 @@ docs/ → [CONFLUENCE_PARENT_ID 또는 공간 최상위]
|
|
|
130
167
|
|
|
131
168
|
- 페이지 제목은 **frontmatter `title` → 첫 `# 제목` → 파일명** 순으로 정해지고, 본문은 storage format 으로 변환됩니다.
|
|
132
169
|
- 코드블록 → **code 매크로**, 표·리스트·헤딩은 그대로.
|
|
170
|
+
- `mermaid` 코드블록 뒤에는 **Mermaid diagram** 매크로를 자동 삽입합니다. Confluence에 [Atlassian Labs Mermaid Diagrams Viewer](https://marketplace.atlassian.com/apps/1232887/mermaid-diagrams-viewer)가 설치되어 있어야 하며, 원본 코드도 함께 표시됩니다. 자동 연결은 등장 순서를 사용하므로 페이지의 모든 Mermaid 코드는 `mermaid` 코드블록으로 작성하세요.
|
|
133
171
|
- **내부 `.md` 링크**와 **`[[wikilink]]`** → Confluence 페이지 링크로 자동 변환.
|
|
134
172
|
- **섹션 링크** `[§1 개요](#1-개요)` · `[§6](./04-구현체.md#6-zkp)` · `[[문서#헤딩 텍스트]]` → Confluence 앵커 링크. [아래 참조](#섹션-링크-앵커)
|
|
135
173
|
- **로컬 이미지**와 **`![[embed]]`** → 페이지 첨부로 업로드 후 참조 (같은 파일명은 새 버전으로 갱신). 외부 URL 이미지는 그대로.
|
|
136
174
|
- **YAML frontmatter** 는 메타데이터로만 쓰이고 본문에 새지 않습니다.
|
|
137
175
|
|
|
176
|
+
### 다른 동기화 폴더의 문서 링크
|
|
177
|
+
|
|
178
|
+
`--reference-root`를 반복해서 지정하면 다른 동기화 폴더에 이미 게시된 문서로 연결할 수 있습니다.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
csync --base ./confluence-docs/project \
|
|
182
|
+
--reference-root ./confluence-docs/DBP \
|
|
183
|
+
--reference-root ./confluence-docs/shared \
|
|
184
|
+
./confluence-docs/project/guide.md --dry-run
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
위키링크 `[[원본/긴/경로/문서명|표시 이름]]`와 Markdown 링크 `[표시 이름](../이전/문서명.md)` 모두 지원합니다. 현재 루트의 정확한 경로를 먼저 찾고, 참조 루트의 정확한 경로, 전체 후보의 파일명 순으로 검색합니다. 파일명 검색 결과가 하나일 때만 연결하며 별칭은 유지합니다. 현재 루트의 문서는 페이지 제목으로, 참조 문서는 `<참조 루트>/.confluence-sync.json`의 페이지 ID를 사용한 URL로 연결합니다.
|
|
188
|
+
|
|
189
|
+
참조 폴더는 같은 Confluence 서버의 문서여야 합니다. 참조 폴더의 `.env`에 서버 주소가 있으면 현재 서버와 일치하는지 검사합니다. `.env`가 없으면 현재 서버의 매핑으로 간주합니다. 참조 문서와 매핑은 읽기만 하며 함께 업로드하지 않습니다. 제외 규칙도 적용합니다. 페이지 ID의 실제 서버 존재 여부는 미리보기에서 검사하지 않습니다.
|
|
190
|
+
|
|
191
|
+
중복 파일명, 대상 문서 누락, 게시 매핑 누락은 CLI 출력과 웹 동기화 미리보기에 경고로 표시하고 링크 원문은 유지합니다. 경고는 업로드를 차단하지 않으므로 실행 전에 확인하세요. 다른 루트의 섹션 링크는 기존 앵커를 사용하며 참조 페이지에 앵커를 새로 만들지 않습니다.
|
|
192
|
+
|
|
193
|
+
웹에서는 **Confluence 동기화 → 참조 문서 폴더**에 한 줄씩 입력하거나 **참조 폴더 추가…**로 선택합니다. 설정은 새로고침 후에도 유지됩니다. 참조 폴더의 문서·매핑·설정이 변경되면 다시 미리보기해야 합니다. `convert`에는 이 옵션이 필요하지 않습니다.
|
|
194
|
+
|
|
138
195
|
### 섹션 링크 (앵커)
|
|
139
196
|
|
|
140
197
|
문서는 표준 마크다운 그대로 두면 됩니다. 이 도구가 Confluence 쪽 앵커를 만들어 맞춰 줍니다.
|
|
@@ -154,8 +211,8 @@ docs/ → [CONFLUENCE_PARENT_ID 또는 공간 최상위]
|
|
|
154
211
|
|
|
155
212
|
```
|
|
156
213
|
$ confluence-sync --base ./docs --dry-run
|
|
157
|
-
[변경]
|
|
158
|
-
⚠ 대응 헤딩 없는 앵커
|
|
214
|
+
[변경] guide/usage.md → "사용 안내" (…, 내부링크: 36, 섹션링크: 30, 앵커: 15, 이미지: 0)
|
|
215
|
+
⚠ 대응 헤딩 없는 앵커 guide/reference.md #없는-헤딩
|
|
159
216
|
```
|
|
160
217
|
|
|
161
218
|
`섹션링크` 는 이 문서가 **가리키는** 링크 수, `앵커` 는 이 문서에 **심긴** 앵커 수입니다(다른 문서가 가리키는 것 포함).
|
|
@@ -341,37 +398,11 @@ Confluence에서 다시 내려받은 문서의 복원은 보장하지 않습니
|
|
|
341
398
|
|
|
342
399
|
---
|
|
343
400
|
|
|
344
|
-
##
|
|
345
|
-
|
|
346
|
-
`npm test`로 전체 테스트, `npm run test:coverage`로 커버리지를 확인합니다. 테스트는 실제 Confluence에 접속하지 않습니다.
|
|
347
|
-
|
|
348
|
-
```bash
|
|
349
|
-
git clone https://github.com/wonseok-han/confluence-sync.git
|
|
350
|
-
cd confluence-sync
|
|
351
|
-
npm install
|
|
352
|
-
npm run build
|
|
353
|
-
```
|
|
354
|
-
|
|
355
|
-
소스에서 바로 실행할 때는 `npm run` 스크립트를 쓰고, 인자는 **`--` 뒤에** 둡니다.
|
|
356
|
-
|
|
357
|
-
| 글로벌 | 로컬 |
|
|
358
|
-
| --- | --- |
|
|
359
|
-
| `confluence-sync` | `npm run sync` |
|
|
360
|
-
| `confluence-sync --dry-run` | `npm run sync:dry` |
|
|
361
|
-
| `confluence-sync --list` | `npm run list -- --base ./docs` |
|
|
362
|
-
|
|
363
|
-
## 릴리스
|
|
364
|
-
|
|
365
|
-
`v*` 태그를 푸시하면 GitHub Actions 가 빌드 → npm 배포 → GitHub Release 까지 자동 처리합니다.
|
|
366
|
-
|
|
367
|
-
```bash
|
|
368
|
-
npm version patch # 또는 minor / major
|
|
369
|
-
git push && git push --tags
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
워크플로는 `package.json` 버전과 태그가 일치하는지 검증한 뒤 배포합니다. 변경 이력은 [CHANGELOG.md](CHANGELOG.md) 를 참고하세요.
|
|
401
|
+
## 기여 및 변경 이력
|
|
373
402
|
|
|
374
|
-
|
|
403
|
+
- 버그 신고와 기능 제안: [GitHub Issues](https://github.com/wonseok-han/confluence-sync/issues)
|
|
404
|
+
- 개발 환경과 테스트: [기여 안내](CONTRIBUTING.md)
|
|
405
|
+
- 버전별 변경 사항: [CHANGELOG](CHANGELOG.md)
|
|
375
406
|
|
|
376
407
|
## 라이선스
|
|
377
408
|
|
package/dist/cli/args.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Positional arguments shared by command dispatch and push selection. */
|
|
2
|
+
export function positionalArgs(argv) {
|
|
3
|
+
const values = new Set(['--base', '--reference-root', '--mapping', '--exclude', '--to', '--out', '--port']);
|
|
4
|
+
const result = [];
|
|
5
|
+
for (let i = 0; i < argv.length; i++) {
|
|
6
|
+
if (values.has(argv[i])) {
|
|
7
|
+
i++;
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
if (!argv[i].startsWith('-'))
|
|
11
|
+
result.push(argv[i]);
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/** --help / --version 출력. */
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { bold, cyan, dim } from '
|
|
5
|
-
/** 설치된 패키지(package.json)의 버전을 읽는다(dist·src 어느 쪽에서 실행해도
|
|
4
|
+
import { bold, cyan, dim } from '../shared/colors.js';
|
|
5
|
+
/** 설치된 패키지(package.json)의 버전을 읽는다(dist·src 어느 쪽에서 실행해도 ../../package.json). */
|
|
6
6
|
export function readPkgVersion() {
|
|
7
7
|
try {
|
|
8
|
-
const pkgPath = fileURLToPath(new URL('
|
|
8
|
+
const pkgPath = fileURLToPath(new URL('../../package.json', import.meta.url));
|
|
9
9
|
return JSON.parse(readFileSync(pkgPath, 'utf8')).version ?? 'unknown';
|
|
10
10
|
}
|
|
11
11
|
catch {
|
|
@@ -21,12 +21,20 @@ ${dim('짧은 이름')} ${o('csync')} ${dim('로도 실행할 수 있습니다(
|
|
|
21
21
|
|
|
22
22
|
${h('사용법:')}
|
|
23
23
|
${o('confluence-sync')} [옵션] [경로...] 문서를 동기화(md → Confluence)
|
|
24
|
+
${o('confluence-sync web')} 문서 변환·Confluence 동기화 웹 화면(로컬 전용)
|
|
24
25
|
${o('confluence-sync init')} 대화형으로 .env 설정 파일 생성
|
|
25
26
|
${o('confluence-sync pull <pageId|url>')} Confluence 페이지/폴더를 .md 로 가져오기(역방향)
|
|
26
27
|
${o('confluence-sync convert --to <형식>')} 이미 받은 .md 의 링크 표기를 바꾸기(로컬 전용)
|
|
27
28
|
${o('confluence-sync --help | --version')}
|
|
28
29
|
|
|
30
|
+
${h('web 옵션:')}
|
|
31
|
+
${o('--base <dir>')} 처음 열 폴더(기본: 홈 폴더). 화면에서 기준 폴더를 바꿀 수 있음
|
|
32
|
+
${o('--port <number>')} 웹 포트(기본: 4318)
|
|
33
|
+
${o('--no-open')} 브라우저를 자동으로 열지 않음
|
|
34
|
+
localhost / 127.0.0.1 로컬 주소에서 실행. 화면에서 출력 폴더 지정 가능. 원본 유지.
|
|
35
|
+
|
|
29
36
|
${h('동기화 옵션:')}
|
|
37
|
+
${o('--reference-root <dir>')} 동기화 링크 참조 폴더(반복 가능, 참조 문서는 업로드하지 않음)
|
|
30
38
|
${o('--base <dir>')} 동기화 루트 (또는 env CONFLUENCE_SYNC_BASE). 미지정 시 중단
|
|
31
39
|
${o('--mapping <path>')} 매핑 파일 위치 (기본: <base>/.confluence-sync.json)
|
|
32
40
|
${o('--exclude <glob>')} 동기화 제외 패턴(반복 가능). <base>/.confluence-syncignore(.gitignore 방식)도 사용
|
|
@@ -6,7 +6,7 @@ import { existsSync, writeFileSync } from 'node:fs';
|
|
|
6
6
|
import { resolve } from 'node:path';
|
|
7
7
|
import { createInterface } from 'node:readline/promises';
|
|
8
8
|
import { stdin as input, stdout as output } from 'node:process';
|
|
9
|
-
import { bold, cyan, dim, green, red, yellow } from '
|
|
9
|
+
import { bold, cyan, dim, green, red, yellow } from '../shared/colors.js';
|
|
10
10
|
/** 일반 한 줄 입력(에코 표시). 매 질문마다 readline 을 새로 열고 닫아 stdin 경쟁을 피한다. */
|
|
11
11
|
async function promptLine(query) {
|
|
12
12
|
const rl = createInterface({ input, output });
|
package/dist/cli/main.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** CLI command dispatch. Feature modules do not run on import. */
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import { positionalArgs } from './args.js';
|
|
4
|
+
import { printHelp, readPkgVersion } from './help.js';
|
|
5
|
+
import { runInit } from './init.js';
|
|
6
|
+
import { runConvert } from '../conversion/convert.js';
|
|
7
|
+
import { runPull } from '../sync/pull.js';
|
|
8
|
+
import { runPush } from '../sync/push.js';
|
|
9
|
+
const argv = process.argv.slice(2);
|
|
10
|
+
try {
|
|
11
|
+
if (argv.includes('--help') || argv.includes('-h'))
|
|
12
|
+
printHelp();
|
|
13
|
+
else if (argv.includes('--version') || argv.includes('-v'))
|
|
14
|
+
console.log(readPkgVersion());
|
|
15
|
+
else {
|
|
16
|
+
switch (positionalArgs(argv)[0]) {
|
|
17
|
+
case 'init':
|
|
18
|
+
await runInit(argv);
|
|
19
|
+
break;
|
|
20
|
+
case 'convert':
|
|
21
|
+
await runConvert(argv);
|
|
22
|
+
break;
|
|
23
|
+
case 'pull':
|
|
24
|
+
await runPull(argv);
|
|
25
|
+
break;
|
|
26
|
+
case 'web': {
|
|
27
|
+
const { runWeb } = await import('../web/server.js');
|
|
28
|
+
await runWeb(argv);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
default: await runPush(argv);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readFileSync, existsSync } from 'node:fs';
|
|
6
6
|
import { relative } from 'node:path';
|
|
7
|
-
import { red, yellow, dim } from '
|
|
7
|
+
import { red, yellow, dim } from '../shared/colors.js';
|
|
8
8
|
export function createClient(cfg, opts) {
|
|
9
9
|
const authHeader = () => 'Basic ' + Buffer.from(`${cfg.email}:${cfg.token}`).toString('base64');
|
|
10
10
|
async function api(path, init) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 환경변수(.env / 셸) 로딩과 필수값 검증.
|
|
3
3
|
* dotenv 로딩은 진입점(sync.ts)에서 `import 'dotenv/config'` 로 먼저 수행된다.
|
|
4
4
|
*/
|
|
5
|
-
import { red, cyan } from '
|
|
5
|
+
import { red, cyan } from '../shared/colors.js';
|
|
6
6
|
export function readEnv() {
|
|
7
7
|
return {
|
|
8
8
|
baseUrl: process.env.CONFLUENCE_BASE_URL,
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
import { attachmentMapper } from './attachments.js';
|
|
13
13
|
import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync, copyFileSync, realpathSync } from 'node:fs';
|
|
14
14
|
import { resolve, relative, dirname, basename, join, isAbsolute } from 'node:path';
|
|
15
|
-
import { collectMarkdown, collectAssets, buildVault, vaultResolver } from '
|
|
16
|
-
import { linksToWikilinks, resolveWikilinks, splitFrontmatter } from '
|
|
17
|
-
import { buildAnchorIndex } from '
|
|
15
|
+
import { collectMarkdown, collectAssets, buildVault, vaultResolver } from '../documents/catalog.js';
|
|
16
|
+
import { linksToWikilinks, resolveWikilinks, splitFrontmatter } from '../documents/obsidian.js';
|
|
17
|
+
import { buildAnchorIndex } from '../documents/anchors.js';
|
|
18
18
|
import { footnotesToLinks, linksToFootnotes } from './footnotes.js';
|
|
19
19
|
import { extractPdfPages, restorePdfReferences, mapMarkdownText } from './pdf-pages.js';
|
|
20
20
|
import { repairMarkdown, totalFixes } from './repair.js';
|
|
21
|
-
import { bold, cyan, dim, gray, green, red, yellow } from '
|
|
21
|
+
import { bold, cyan, dim, gray, green, red, yellow } from '../shared/colors.js';
|
|
22
22
|
function optVal(argv, name) {
|
|
23
23
|
const i = argv.indexOf(name);
|
|
24
24
|
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import MarkdownIt from 'markdown-it';
|
|
2
2
|
import footnote from 'markdown-it-footnote';
|
|
3
|
-
import { collectHeadings } from '
|
|
3
|
+
import { collectHeadings } from '../documents/anchors.js';
|
|
4
4
|
import { mapMarkdownText, replaceGeneratedBlocks } from './pdf-pages.js';
|
|
5
5
|
/** 각주 확장을 일반 제목 앵커 + 위첨자 Markdown 링크로 내보낸다. */
|
|
6
6
|
export function footnotesToLinks(source) {
|
|
@@ -3,7 +3,7 @@ import { readFileSync, existsSync, realpathSync } from 'node:fs';
|
|
|
3
3
|
import { resolve, relative, dirname } from 'node:path';
|
|
4
4
|
import { renderPdfPages } from './pdf-renderer.js';
|
|
5
5
|
import MarkdownIt from 'markdown-it';
|
|
6
|
-
import { collectHeadings } from '
|
|
6
|
+
import { collectHeadings } from '../documents/anchors.js';
|
|
7
7
|
const encode = (value) => Buffer.from(JSON.stringify(value)).toString('base64');
|
|
8
8
|
const decode = (value) => JSON.parse(Buffer.from(value, 'base64').toString());
|
|
9
9
|
const parser = new MarkdownIt({ html: true });
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* 스페이스를 통째로 다시 받지 않고도 같은 결과를 얻도록, 그 흔적들만 골라 되돌린다.
|
|
6
6
|
* 파일을 덮어쓰므로 **확실한 것만** 고치고, 애매하면 손대지 않는다.
|
|
7
7
|
*/
|
|
8
|
-
import { relaxEscapes } from '
|
|
9
|
-
import { splitFrontmatter } from '
|
|
8
|
+
import { relaxEscapes } from '../documents/html-to-markdown.js';
|
|
9
|
+
import { splitFrontmatter } from '../documents/obsidian.js';
|
|
10
10
|
const emptyStats = () => ({ cssJunk: 0, codeLang: 0, trailingWs: 0, escapes: 0, tightList: 0, dupTitle: 0 });
|
|
11
11
|
export const totalFixes = (s) => Object.values(s).reduce((a, b) => a + b, 0);
|
|
12
12
|
const FENCE = /^\s*(?:```|~~~)/;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Keep CLI exits and CPU-heavy PDF rendering out of the web server process.
|
|
2
|
+
import { runConvert } from './convert.js';
|
|
3
|
+
try {
|
|
4
|
+
await runConvert(['convert', ...process.argv.slice(2)]);
|
|
5
|
+
}
|
|
6
|
+
catch (error) {
|
|
7
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
}
|
|
@@ -89,7 +89,7 @@ function normalizeRel(dir, dest) {
|
|
|
89
89
|
* Confluence 가 헤딩에 붙이는 id 에 기대지 않고 Anchor 매크로로 이름을 심을 것이므로,
|
|
90
90
|
* 심을 곳을 알아야 한다. 아무도 가리키지 않는 헤딩에는 매크로를 넣지 않는다(페이지를 깨끗하게 유지).
|
|
91
91
|
*/
|
|
92
|
-
export function resolveAnchorTargets(docs, vault) {
|
|
92
|
+
export function resolveAnchorTargets(docs, vault, localTarget) {
|
|
93
93
|
const out = {};
|
|
94
94
|
for (const [rel, doc] of Object.entries(docs)) {
|
|
95
95
|
const dir = dirname(rel) === '.' ? '' : dirname(rel);
|
|
@@ -107,6 +107,8 @@ export function resolveAnchorTargets(docs, vault) {
|
|
|
107
107
|
target = null;
|
|
108
108
|
slug = decodeAnchor(la.fragment).toLowerCase();
|
|
109
109
|
}
|
|
110
|
+
if (localTarget && la.dest)
|
|
111
|
+
target = localTarget(rel, la.dest, la.wiki);
|
|
110
112
|
if (!target || !slug)
|
|
111
113
|
continue;
|
|
112
114
|
// 실제로 존재하는 헤딩만 — 없는 앵커는 markdown.ts 가 경고로 잡는다
|
|
@@ -11,6 +11,7 @@ import { createHash } from 'node:crypto';
|
|
|
11
11
|
import MarkdownIt from 'markdown-it';
|
|
12
12
|
import { resolveWikilinks } from './obsidian.js';
|
|
13
13
|
import { decodeAnchor, anchorMacro } from './anchors.js';
|
|
14
|
+
import { mermaidMacro } from './mermaid.js';
|
|
14
15
|
export const escapeXml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
15
16
|
export const hashOf = (s) => createHash('sha256').update(s).digest('hex');
|
|
16
17
|
/** markdown-it 이 퍼센트 인코딩한 링크/이미지 경로를 실제 경로로 디코드(실패 시 원본). */
|
|
@@ -25,7 +26,7 @@ const decodePath = (s) => {
|
|
|
25
26
|
const md = new MarkdownIt({ html: true, linkify: true, breaks: false });
|
|
26
27
|
const emptyCtx = () => ({
|
|
27
28
|
fileDir: '', titleIndex: {}, anchorIndex: {}, selfRel: '', selfTitle: '',
|
|
28
|
-
anchorNames: new Set(), bodySlugs: [], headingSeq: 0, anchorsPlaced: 0,
|
|
29
|
+
anchorNames: new Set(), bodySlugs: [], headingSeq: 0, mermaidSeq: 0, anchorsPlaced: 0,
|
|
29
30
|
images: [], internalLinks: 0, anchorLinks: 0, deadAnchors: [], linkStack: [], linkedTitles: new Set(),
|
|
30
31
|
});
|
|
31
32
|
// 렌더 규칙은 md 인스턴스에 한 번만 등록되므로, 현재 변환 컨텍스트를 모듈 변수로 공유한다.
|
|
@@ -76,7 +77,8 @@ md.renderer.rules.heading_open = (tokens, idx, opts, _env, self) => {
|
|
|
76
77
|
};
|
|
77
78
|
md.renderer.rules.fence = (tokens, idx) => {
|
|
78
79
|
const t = tokens[idx];
|
|
79
|
-
const
|
|
80
|
+
const infoLang = (t.info || '').trim().split(/\s+/)[0];
|
|
81
|
+
const lang = infoLang.toLowerCase() === 'mermaid' ? 'mermaid' : infoLang;
|
|
80
82
|
// 별도 호스팅한 PDF.js 등 웹 뷰어를 Confluence iFrame 매크로로 표시.
|
|
81
83
|
if (lang === 'confluence-iframe') {
|
|
82
84
|
const url = t.content.trim();
|
|
@@ -85,11 +87,14 @@ md.renderer.rules.fence = (tokens, idx) => {
|
|
|
85
87
|
return `<ac:structured-macro ac:name="iframe"><ac:parameter ac:name="url">${escapeXml(url)}</ac:parameter><ac:parameter ac:name="width">100%</ac:parameter><ac:parameter ac:name="height">900</ac:parameter></ac:structured-macro>\n`;
|
|
86
88
|
}
|
|
87
89
|
const safe = t.content.split(']]>').join(']]]]><![CDATA[>');
|
|
88
|
-
const langParam = lang ? `<ac:parameter ac:name="language">${lang}</ac:parameter>` : '';
|
|
89
|
-
|
|
90
|
+
const langParam = lang ? `<ac:parameter ac:name="language">${escapeXml(lang)}</ac:parameter>` : '';
|
|
91
|
+
const code = `<ac:structured-macro ac:name="code">${langParam}<ac:plain-text-body><![CDATA[${safe}]]></ac:plain-text-body></ac:structured-macro>\n`;
|
|
92
|
+
return code + (lang === 'mermaid' ? mermaidMacro(ctx.selfRel, ctx.mermaidSeq++) : '');
|
|
90
93
|
};
|
|
91
94
|
md.renderer.rules.link_open = (tokens, idx, opts, _env, self) => {
|
|
92
|
-
const
|
|
95
|
+
const originalHref = tokens[idx].attrGet('href') || '';
|
|
96
|
+
const href = ctx.rewriteHref?.(originalHref) ?? originalHref;
|
|
97
|
+
tokens[idx].attrSet('href', href);
|
|
93
98
|
const t = resolveInternalLink(href);
|
|
94
99
|
// 페이지도 앵커도 못 정하면 Confluence 링크로 만들 게 없다 → 원문 <a> 유지
|
|
95
100
|
if (t && (t.title || t.anchor)) {
|
|
@@ -144,6 +149,7 @@ export function toStorage(markdown, rel, titleIndex, base, opts = {}) {
|
|
|
144
149
|
baseDir = base;
|
|
145
150
|
ctx = {
|
|
146
151
|
...emptyCtx(),
|
|
152
|
+
rewriteHref: opts.rewriteHref,
|
|
147
153
|
fileDir: dirname(rel) === '.' ? '' : dirname(rel),
|
|
148
154
|
titleIndex,
|
|
149
155
|
anchorIndex: opts.anchorIndex ?? {},
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
// Atlassian Labs Mermaid Diagrams Viewer (Marketplace production deployment).
|
|
3
|
+
// Confirmed against the installed macro's editor/clipboard attributes.
|
|
4
|
+
export const MERMAID_EXTENSION_KEY = '23392b90-4271-4239-98ca-a3e96c663cbb/63d4d207-ac2f-4273-865c-0240d37f044a/static/mermaid-diagram';
|
|
5
|
+
/** Auto-detect pairs Mermaid code blocks and viewer macros in document order. */
|
|
6
|
+
export function mermaidMacro(document, ordinal) {
|
|
7
|
+
// Random IDs would change the storage hash on every push. Include the ordinal
|
|
8
|
+
// so repeated identical diagrams still have distinct macro instances.
|
|
9
|
+
const hex = createHash('sha256').update(`csync:mermaid:${document}:${ordinal}`).digest('hex');
|
|
10
|
+
const id = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-a${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
11
|
+
const node = `<ac:adf-node type="extension"><ac:adf-attribute key="extension-type">com.atlassian.ecosystem</ac:adf-attribute><ac:adf-attribute key="extension-key">${MERMAID_EXTENSION_KEY}</ac:adf-attribute><ac:adf-attribute key="parameters"><ac:adf-parameter key="local-id">${id}</ac:adf-parameter><ac:adf-parameter key="extension-id">ari:cloud:ecosystem::extension/${MERMAID_EXTENSION_KEY}</ac:adf-parameter><ac:adf-parameter key="extension-title">Mermaid diagram</ac:adf-parameter></ac:adf-attribute><ac:adf-attribute key="text">Mermaid diagram</ac:adf-attribute><ac:adf-attribute key="layout">default</ac:adf-attribute><ac:adf-attribute key="local-id">${id}</ac:adf-attribute></ac:adf-node>`;
|
|
12
|
+
return `<ac:adf-extension>${node}<ac:adf-fallback>${node}</ac:adf-fallback></ac:adf-extension>\n`;
|
|
13
|
+
}
|
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
|
|
11
11
|
import { resolve, join, dirname, basename, relative } from 'node:path';
|
|
12
|
-
import { readEnv, requireEnv } from '
|
|
13
|
-
import { createClient } from '
|
|
14
|
-
import { htmlToMarkdown, codeLanguagesFromStorage } from '
|
|
15
|
-
import { buildFrontmatter, splitFrontmatter } from '
|
|
16
|
-
import { collectHeadings, matchConfluenceAnchor } from '
|
|
17
|
-
import { collectMarkdown } from '
|
|
12
|
+
import { readEnv, requireEnv } from '../confluence/config.js';
|
|
13
|
+
import { createClient } from '../confluence/client.js';
|
|
14
|
+
import { htmlToMarkdown, codeLanguagesFromStorage } from '../documents/html-to-markdown.js';
|
|
15
|
+
import { buildFrontmatter, splitFrontmatter } from '../documents/obsidian.js';
|
|
16
|
+
import { collectHeadings, matchConfluenceAnchor } from '../documents/anchors.js';
|
|
17
|
+
import { collectMarkdown } from '../documents/catalog.js';
|
|
18
18
|
import { buildTreeRenderer } from './render.js';
|
|
19
|
-
import { docHash } from '
|
|
19
|
+
import { docHash } from '../documents/markdown.js';
|
|
20
20
|
import { buildIgnorer } from './ignore.js';
|
|
21
21
|
import { loadMapping, saveMapping, MAPPING_FILE } from './mapping.js';
|
|
22
|
-
import { cyan, dim, green, red, yellow } from '
|
|
22
|
+
import { cyan, dim, green, red, yellow } from '../shared/colors.js';
|
|
23
23
|
// 첨부 이미지는 항상 <md위치>/attachments/<문서명>/ 하위에 저장한다.
|
|
24
24
|
const ASSETS_DIR = 'attachments';
|
|
25
25
|
/** 마크다운 링크 중 Confluence 페이지를 가리키는 것: [text](https://.../pages/<id>/...) */
|