confluence-md-sync 0.4.0 → 0.4.1
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 +10 -3
- package/dist/export/to-markdown.js +28 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,18 +180,24 @@ Attachments and page links become `{{img:...}}` / `{{file:...}}` /
|
|
|
180
180
|
| Mode | For | What it does |
|
|
181
181
|
| --- | --- | --- |
|
|
182
182
|
| `faithful` *(default)* | round-trip, editing then re-publishing | **Loss-free by construction.** Clean Markdown → macro markers → verbatim storage (raw HTML, or a ` ```confluence-storage ` fence) as a fallback. Perfect fidelity, but the raw-HTML blocks (complex tables, wrappers) render poorly in some Markdown viewers. |
|
|
183
|
-
| `readable` | reading, diffing, docs you won't publish back | **Clean Markdown, no raw HTML.** Complex tables are flattened to GFM (merged cells → filled grid, block cells → `• …` joined by `<br>`), styled spans/wrappers are unwrapped, entities decoded. Keeps the content; **drops** colours, exact merge geometry, wrappers — *not* round-trippable. |
|
|
183
|
+
| `readable` | reading, diffing, docs you won't publish back | **Clean Markdown, no raw HTML.** Complex tables are flattened to GFM (merged cells → filled grid, block cells → `• …` joined by `<br>`), styled spans/wrappers are unwrapped, entities decoded, and `<br>` in paragraphs becomes a real line break (inside table cells it stays `<br>` — GFM cells can't hold newlines). Keeps the content; **drops** colours, exact merge geometry, wrappers — *not* round-trippable. |
|
|
184
|
+
|
|
185
|
+
Images and attachments are downloaded by default (pass
|
|
186
|
+
`downloadAttachments: false` to skip). They land in `attachments/` next to
|
|
187
|
+
the Markdown, and the page body references them via `{{img:name}}` /
|
|
188
|
+
`{{file:name}}` — in both modes.
|
|
184
189
|
|
|
185
190
|
```ts
|
|
186
191
|
import { exportPage } from 'confluence-md-sync';
|
|
187
192
|
|
|
188
193
|
const { markdownPath, images, downloaded } = await exportPage(
|
|
189
194
|
'123456789',
|
|
190
|
-
{ outDir: 'exported' }, // faithful;
|
|
195
|
+
{ outDir: 'exported' }, // faithful; exported/page.md + exported/attachments/
|
|
191
196
|
cfg,
|
|
192
197
|
);
|
|
198
|
+
console.log(downloaded); // Map<filename, localPath> of saved attachments
|
|
193
199
|
|
|
194
|
-
// Readable variant for humans:
|
|
200
|
+
// Readable variant for humans (writes page.md + ./attachments/ next to it):
|
|
195
201
|
await exportPage('123456789', { outFile: 'page.md', mode: 'readable' }, cfg);
|
|
196
202
|
```
|
|
197
203
|
|
|
@@ -225,6 +231,7 @@ normalised away (Confluence itself rewrites these on every save). Use
|
|
|
225
231
|
From the CLI:
|
|
226
232
|
|
|
227
233
|
```bash
|
|
234
|
+
# attachments download to ./attachments/ next to the .md (omit --no-attachments)
|
|
228
235
|
confluence-md-sync export 123456789 --out page.md # faithful
|
|
229
236
|
confluence-md-sync export 123456789 --out page.md --readable # clean Markdown
|
|
230
237
|
confluence-md-sync roundtrip 123456789 --show-markdown # exit 2 on loss
|
|
@@ -104,7 +104,11 @@ class Converter {
|
|
|
104
104
|
const h = /^h([1-6])$/.exec(el.name);
|
|
105
105
|
try {
|
|
106
106
|
if (h && el.attrs.length === 0) {
|
|
107
|
-
|
|
107
|
+
// Заголовок однострочен по определению — в readable схлопываем
|
|
108
|
+
// возможные переводы строк (из <br>) в пробел.
|
|
109
|
+
let inline = this.inlineToMd(el.children);
|
|
110
|
+
if (this.readable)
|
|
111
|
+
inline = inline.replace(/\s*\n\s*/g, ' ');
|
|
108
112
|
if (inline.includes('\n') || inline.trim() === '')
|
|
109
113
|
throw new Unrepresentable();
|
|
110
114
|
return '#'.repeat(Number(h[1])) + ' ' + guardLineStart(inline.trim());
|
|
@@ -147,7 +151,10 @@ class Converter {
|
|
|
147
151
|
if (children.length === 0)
|
|
148
152
|
return this.fallbackBlock(el);
|
|
149
153
|
try {
|
|
150
|
-
|
|
154
|
+
// multiline: в readable-абзаце <br> становится настоящим переводом
|
|
155
|
+
// строки (см. case 'br'). guardLineStart применяем к КАЖДОЙ строке —
|
|
156
|
+
// после переноса тоже нельзя случайно начать список/заголовок.
|
|
157
|
+
const inline = this.inlineToMd(children, { multiline: true }).trim();
|
|
151
158
|
if (inline === '')
|
|
152
159
|
return this.fallbackBlock(el);
|
|
153
160
|
// Абзац, начинающийся с блочного HTML-тега, markdown-it превратит в
|
|
@@ -155,7 +162,7 @@ class Converter {
|
|
|
155
162
|
const m = /^<\/?([a-zA-Z][a-zA-Z0-9-]*)/.exec(inline);
|
|
156
163
|
if (m && CM_BLOCK_TAGS.has(m[1].toLowerCase()))
|
|
157
164
|
return this.fallbackBlock(el);
|
|
158
|
-
return guardLineStart(
|
|
165
|
+
return inline.split('\n').map((line) => guardLineStart(line)).join('\n');
|
|
159
166
|
}
|
|
160
167
|
catch (e) {
|
|
161
168
|
if (!(e instanceof Unrepresentable))
|
|
@@ -255,9 +262,10 @@ class Converter {
|
|
|
255
262
|
.join('\n');
|
|
256
263
|
}
|
|
257
264
|
// p / td / th / li / caption и прочие «инлайн-контейнеры» → инлайн.
|
|
258
|
-
|
|
265
|
+
// multiline: это свободный поток (не ячейка) — <br> станет переносом.
|
|
266
|
+
const inline = this.inlineToMd(el.children, { multiline: true }).trim();
|
|
259
267
|
if (inline !== '')
|
|
260
|
-
return guardLineStart(
|
|
268
|
+
return inline.split('\n').map((l) => guardLineStart(l)).join('\n');
|
|
261
269
|
// Совсем ничего не вышло — голый текст (может быть пустым).
|
|
262
270
|
return escapeMdText(textContent(el.children), {}, this.readable).trim();
|
|
263
271
|
}
|
|
@@ -497,7 +505,7 @@ class Converter {
|
|
|
497
505
|
content.every((n) => n.kind !== 'text' || /^[ \t\r\n]*$/.test(n.raw))) {
|
|
498
506
|
content = els[0].children;
|
|
499
507
|
}
|
|
500
|
-
const md = this.inlineToMd(content, { cell:
|
|
508
|
+
const md = this.inlineToMd(content, { cell: true }).trim();
|
|
501
509
|
if (md.includes('\n'))
|
|
502
510
|
throw new Unrepresentable();
|
|
503
511
|
// Пайпы экранируем один раз над всей ячейкой — покрывает и текст, и
|
|
@@ -590,7 +598,7 @@ class Converter {
|
|
|
590
598
|
const flush = () => {
|
|
591
599
|
if (inlineRun.length === 0)
|
|
592
600
|
return;
|
|
593
|
-
const s = this.inlineToMd(inlineRun, { cell:
|
|
601
|
+
const s = this.inlineToMd(inlineRun, { cell: true }).replace(/\s+/g, ' ').trim();
|
|
594
602
|
if (s !== '')
|
|
595
603
|
blocks.push(s);
|
|
596
604
|
inlineRun = [];
|
|
@@ -602,7 +610,7 @@ class Converter {
|
|
|
602
610
|
}
|
|
603
611
|
else if (n.kind === 'el' && n.name === 'p') {
|
|
604
612
|
flush();
|
|
605
|
-
const s = this.inlineToMd(n.children, { cell:
|
|
613
|
+
const s = this.inlineToMd(n.children, { cell: true }).replace(/\s+/g, ' ').trim();
|
|
606
614
|
if (s !== '')
|
|
607
615
|
blocks.push(s);
|
|
608
616
|
}
|
|
@@ -735,6 +743,11 @@ class Converter {
|
|
|
735
743
|
return `${ticks}${pad}${text}${pad}${ticks}`;
|
|
736
744
|
}
|
|
737
745
|
case 'br':
|
|
746
|
+
// readable: в свободном потоке (абзац/цитата) — настоящий перенос
|
|
747
|
+
// строки; в ячейке GFM-таблицы перенос невозможен (сломал бы строку
|
|
748
|
+
// таблицы) — там остаётся <br>. faithful — всегда <br/> (round-trip).
|
|
749
|
+
if (this.readable)
|
|
750
|
+
return ctx.multiline ? '\n' : ctx.cell ? '<br>' : '<br/>';
|
|
738
751
|
return '<br/>';
|
|
739
752
|
case 'a':
|
|
740
753
|
return this.linkAnchorToMd(el, ctx);
|
|
@@ -1059,21 +1072,13 @@ function escapeMdText(raw, ctx, readable = false) {
|
|
|
1059
1072
|
out += escapePlain(collapsed.slice(last), ctx, readable);
|
|
1060
1073
|
return out;
|
|
1061
1074
|
}
|
|
1062
|
-
function escapePlain(s,
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
return esc;
|
|
1070
|
-
}
|
|
1071
|
-
// Сырой U+00A0 на краю абзаца съедается trim()'ом markdown-it —
|
|
1072
|
-
// в entity-форме переживает рендер (и виден при редактировании).
|
|
1073
|
-
esc = esc.replace(/\u00A0/g, ' ');
|
|
1074
|
-
if (ctx.cell)
|
|
1075
|
-
esc = esc.replace(/\|/g, '\\|');
|
|
1076
|
-
return esc;
|
|
1075
|
+
function escapePlain(s, _ctx, readable = false) {
|
|
1076
|
+
const esc = s.replace(/[\\`*_[\]{}~]/g, (c) => '\\' + c);
|
|
1077
|
+
// Пайпы здесь НЕ трогаем — они экранируются один раз на границе ячейки
|
|
1078
|
+
// (cellMd / readableTable), иначе плейсхолдеры {{img:…|…}} двоились бы.
|
|
1079
|
+
// readable: неразрывный пробел → обычный; faithful: → entity (переживает
|
|
1080
|
+
// trim() markdown-it на краю абзаца).
|
|
1081
|
+
return esc.replace(/\u00A0/g, readable ? ' ' : ' ');
|
|
1077
1082
|
}
|
|
1078
1083
|
/** Экранирует конструкции, значимые в начале строки (#, >, -, 1. …). */
|
|
1079
1084
|
function guardLineStart(md) {
|
package/package.json
CHANGED