confluence-md-sync 0.2.0 → 0.3.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 +126 -21
- package/dist/cli.d.ts +4 -2
- package/dist/cli.js +76 -28
- package/dist/export/canonical.d.ts +38 -0
- package/dist/export/canonical.js +272 -0
- package/dist/export/export-page.d.ts +31 -0
- package/dist/export/export-page.js +41 -0
- package/dist/export/roundtrip.d.ts +30 -0
- package/dist/export/roundtrip.js +32 -0
- package/dist/export/to-markdown.d.ts +37 -0
- package/dist/export/to-markdown.js +845 -0
- package/dist/export/xhtml.d.ts +52 -0
- package/dist/export/xhtml.js +230 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +8 -1
- package/dist/macros/builder.d.ts +7 -0
- package/dist/macros/builder.js +23 -2
- package/dist/macros/index.d.ts +2 -1
- package/dist/macros/index.js +2 -1
- package/dist/macros/plugins/core.js +3 -0
- package/dist/macros/plugins/table-filter.d.ts +8 -0
- package/dist/macros/plugins/table-filter.js +35 -3
- package/dist/macros/registry.d.ts +11 -0
- package/dist/macros/registry.js +45 -1
- package/dist/markdown/render.d.ts +30 -1
- package/dist/markdown/render.js +83 -13
- package/dist/publish/publish.d.ts +17 -0
- package/dist/publish/publish.js +74 -14
- package/dist/publish/remote.d.ts +25 -0
- package/dist/publish/remote.js +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,8 @@ typed tables back, attach files — idempotently, with a **pluggable macro
|
|
|
10
10
|
system**. ESM-only, zero-config CI.
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npm install confluence-md-sync
|
|
13
|
+
npm install confluence-md-sync # as a library
|
|
14
|
+
npm install -g confluence-md-sync # as a CLI: `confluence-md-sync …`
|
|
14
15
|
```
|
|
15
16
|
|
|
16
17
|
## Highlights
|
|
@@ -23,7 +24,13 @@ npm install confluence-md-sync
|
|
|
23
24
|
- **Fail before write** — placeholders, files and macro markers are validated
|
|
24
25
|
up front; Confluence is never touched on a broken input.
|
|
25
26
|
- **Pluggable macros** — built-in `core` + `table-filter` plugins, extend
|
|
26
|
-
with your own in a few lines.
|
|
27
|
+
with your own in a few lines; unknown macros pass through unchanged.
|
|
28
|
+
- **Flexible sources** — images/files accept relative paths, absolute paths
|
|
29
|
+
or `http(s)` URLs; URLs on your Confluence host are fetched with the
|
|
30
|
+
configured token.
|
|
31
|
+
- **Loss-free round-trip** — export a page to Markdown and publish it back;
|
|
32
|
+
what Markdown can't express is kept verbatim, verified by canonical
|
|
33
|
+
storage comparison.
|
|
27
34
|
|
|
28
35
|
## Configuration
|
|
29
36
|
|
|
@@ -48,10 +55,11 @@ Markdown + three placeholder kinds:
|
|
|
48
55
|
```markdown
|
|
49
56
|
# Отчёт за месяц
|
|
50
57
|
|
|
51
|
-
{{img:
|
|
58
|
+
{{img:chart.png}} <!-- inline image from images[] -->
|
|
59
|
+
{{img:flow.bpmn}} <!-- BPMN diagram — rendered to PNG automatically -->
|
|
52
60
|
Исходник: {{file:data.csv}} <!-- attachment link from files[] -->
|
|
53
61
|
|
|
54
|
-
{{table:summary}}
|
|
62
|
+
{{table:summary}} <!-- table injected from tables[] -->
|
|
55
63
|
```
|
|
56
64
|
|
|
57
65
|
```ts
|
|
@@ -66,7 +74,10 @@ const summary = renderMarkdownTable(rows, [
|
|
|
66
74
|
await publishPage({
|
|
67
75
|
pageId: '123456789',
|
|
68
76
|
markdownPath: 'docs/report.md',
|
|
69
|
-
images: [
|
|
77
|
+
images: [
|
|
78
|
+
'build/chart.png',
|
|
79
|
+
'docs/flow.bpmn', // converted to flow.png on the fly (see BPMN section)
|
|
80
|
+
],
|
|
70
81
|
files: ['build/data.csv'],
|
|
71
82
|
tables: [{
|
|
72
83
|
name: 'summary',
|
|
@@ -93,25 +104,47 @@ const { storage } = await publishPage({ pageId, markdownPath, dryRun: true }, cf
|
|
|
93
104
|
|
|
94
105
|
// Inline content instead of a file
|
|
95
106
|
await publishPage({ pageId, markdown: '# Generated\n\ntext' }, cfg);
|
|
107
|
+
|
|
108
|
+
// Remote sources — downloaded at publish time; the filename for {{img:...}}
|
|
109
|
+
// comes from the URL path. URLs on the configured Confluence host are
|
|
110
|
+
// fetched with your token, so you can pull attachments from other pages.
|
|
111
|
+
await publishPage({
|
|
112
|
+
pageId,
|
|
113
|
+
markdownPath,
|
|
114
|
+
images: ['https://confluence.example.com/download/attachments/999/flow.bpmn'],
|
|
115
|
+
files: ['https://files.example.com/exports/выгрузка.csv'],
|
|
116
|
+
downloadDir: 'build', // optional; default: temp dir per run
|
|
117
|
+
}, cfg);
|
|
96
118
|
```
|
|
97
119
|
|
|
98
120
|
## BPMN diagrams out of the box
|
|
99
121
|
|
|
100
|
-
Pass a `.bpmn` file as an image
|
|
122
|
+
Pass a `.bpmn` file as an image (a path or an `http(s)` URL) — it is
|
|
123
|
+
rendered to PNG at publish time
|
|
101
124
|
(headless Chromium via [bpmn-to-image](https://npmjs.com/package/bpmn-to-image),
|
|
102
|
-
an optional peer dependency)
|
|
125
|
+
an optional peer dependency).
|
|
126
|
+
|
|
127
|
+
Setup — install the converter **and** force a current puppeteer:
|
|
103
128
|
|
|
104
129
|
```bash
|
|
105
|
-
npm install -D bpmn-to-image
|
|
130
|
+
npm install -D bpmn-to-image puppeteer
|
|
106
131
|
```
|
|
107
132
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
```jsonc
|
|
134
|
+
// package.json — required: bpmn-to-image pins puppeteer 21, whose bundled
|
|
135
|
+
// Chromium fails to launch on recent OSes ("socket hang up"). The override
|
|
136
|
+
// must reference the direct dependency ("$puppeteer"), otherwise `npm ci`
|
|
137
|
+
// fails with "Override for puppeteer conflicts with direct dependency".
|
|
138
|
+
{
|
|
139
|
+
"devDependencies": {
|
|
140
|
+
"bpmn-to-image": "^0.7.0",
|
|
141
|
+
"puppeteer": "^24.0.0"
|
|
142
|
+
},
|
|
143
|
+
"overrides": {
|
|
144
|
+
"puppeteer": "$puppeteer"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
115
148
|
|
|
116
149
|
```markdown
|
|
117
150
|
Процесс выпуска релиза:
|
|
@@ -138,6 +171,59 @@ import { convertBpmnFolder } from 'confluence-md-sync';
|
|
|
138
171
|
await convertBpmnFolder({ srcDir: 'docs/diagrams', outDir: 'build' });
|
|
139
172
|
```
|
|
140
173
|
|
|
174
|
+
## Export a page back to Markdown (round-trip)
|
|
175
|
+
|
|
176
|
+
Pull an existing page by ID and turn its storage format into Markdown —
|
|
177
|
+
without losing markup. The conversion is **loss-free by construction**: a
|
|
178
|
+
three-tier policy renders each node as clean Markdown where possible,
|
|
179
|
+
macro markers where a macro round-trips, and verbatim storage (raw HTML,
|
|
180
|
+
or a ` ```confluence-storage ` fence) as a fallback. Attachments and page
|
|
181
|
+
links become `{{img:...}}` / `{{file:...}}` / `{{page:...}}` placeholders.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { exportPage } from 'confluence-md-sync';
|
|
185
|
+
|
|
186
|
+
const { markdownPath, images, downloaded } = await exportPage(
|
|
187
|
+
'123456789',
|
|
188
|
+
{ outDir: 'exported' }, // writes exported/page.md + exported/attachments/
|
|
189
|
+
cfg,
|
|
190
|
+
);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Round-trip means *edit the Markdown, then publish it straight back*:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
await publishPage({
|
|
197
|
+
pageId: '123456789',
|
|
198
|
+
markdownPath: 'exported/page.md',
|
|
199
|
+
images: ['exported/attachments/diagram.png'],
|
|
200
|
+
// exported markup uses native attachment/link references, not URLs:
|
|
201
|
+
render: { imageStyle: 'attachment', fileStyle: 'attachment', linkify: false },
|
|
202
|
+
}, cfg);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Verify a page survives the trip with no markup loss (writes nothing):
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import { roundTripPage } from 'confluence-md-sync';
|
|
209
|
+
|
|
210
|
+
const r = await roundTripPage('123456789', cfg);
|
|
211
|
+
console.log(r.equal, r.stats); // true { markers, fenced, rawHtml }
|
|
212
|
+
// r.diffs lists any canonical differences when equal === false
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Equivalence is **canonical**, not byte-for-byte: `ac:macro-id`,
|
|
216
|
+
schema-version, attribute/parameter order and entity vs literal forms are
|
|
217
|
+
normalised away (Confluence itself rewrites these on every save). Use
|
|
218
|
+
`compareStorage(a, b)` directly to diff two storage fragments.
|
|
219
|
+
|
|
220
|
+
From the CLI:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
confluence-md-sync export 123456789 --out-dir exported
|
|
224
|
+
confluence-md-sync roundtrip 123456789 --show-markdown # exit 2 on loss
|
|
225
|
+
```
|
|
226
|
+
|
|
141
227
|
## Read pages and tables
|
|
142
228
|
|
|
143
229
|
```ts
|
|
@@ -191,6 +277,7 @@ macros.tableExcerptInclude('name', 'Source Page');
|
|
|
191
277
|
// Table Filter and Charts app
|
|
192
278
|
macros.tableExcerpt(md, 'name', /* hide */ true);
|
|
193
279
|
macros.tableFilter(md, { totalrow: ',,Sum' });
|
|
280
|
+
macros.tableJoiner(includesMd, "SELECT * FROM T1 LEFT JOIN T2 ON …"); // multiline SQL ok
|
|
194
281
|
```
|
|
195
282
|
|
|
196
283
|
Or write markers by hand right in Markdown:
|
|
@@ -201,6 +288,10 @@ Any **markdown**, including nested macros.
|
|
|
201
288
|
<!-- MACRO:end:expand -->
|
|
202
289
|
```
|
|
203
290
|
|
|
291
|
+
Any macro name works out of the box — one that isn't registered passes
|
|
292
|
+
through to `<ac:structured-macro>` as-is (params + rich-text body). Disable
|
|
293
|
+
with `registry.passthroughUnknownMacros(false)`.
|
|
294
|
+
|
|
204
295
|
### Custom macro plugin
|
|
205
296
|
|
|
206
297
|
```ts
|
|
@@ -221,11 +312,24 @@ splitting; `pageLinkValue()` builds `<ac:link><ri:page/>` parameter values.
|
|
|
221
312
|
|
|
222
313
|
## CLI
|
|
223
314
|
|
|
315
|
+
Installed globally (`npm i -g confluence-md-sync`) the `confluence-md-sync`
|
|
316
|
+
command is on your PATH; otherwise prefix the examples with `npx`. The
|
|
317
|
+
instance URL and token come from the environment.
|
|
318
|
+
|
|
224
319
|
```bash
|
|
225
|
-
|
|
320
|
+
export CONFLUENCE_BASE_URL='https://confluence.example.com'
|
|
321
|
+
export CONFLUENCE_TOKEN='<your-PAT>'
|
|
322
|
+
|
|
323
|
+
# Download a page to an exact .md path (add --no-attachments for the md only)
|
|
324
|
+
confluence-md-sync export 123456789 --out ./page.md
|
|
325
|
+
|
|
326
|
+
confluence-md-sync publish docs/page.md --page-id 123456789 \
|
|
226
327
|
--image build/flow.png --file build/data.csv --label docs
|
|
227
328
|
|
|
228
|
-
|
|
329
|
+
confluence-md-sync publish docs/page.md --space DOCS --title "Моя страница" --dry-run
|
|
330
|
+
|
|
331
|
+
confluence-md-sync export 123456789 --out-dir exported # page.md + attachments/
|
|
332
|
+
confluence-md-sync roundtrip 123456789 # exit 2 if markup would be lost
|
|
229
333
|
```
|
|
230
334
|
|
|
231
335
|
## CI publish plans
|
|
@@ -260,7 +364,8 @@ src/
|
|
|
260
364
|
├── macros/ registry, builder, plugins (core, table-filter)
|
|
261
365
|
├── attachments/ sha256 dedup, sidecar source hashes, version history
|
|
262
366
|
├── pages/ Page/Table object model, table parse & render
|
|
263
|
-
├── publish/ idempotent publish pipeline, runPublish plans
|
|
367
|
+
├── publish/ idempotent publish pipeline, runPublish plans, remote sources
|
|
368
|
+
├── export/ storage parser, storage→Markdown, canonical compare, round-trip
|
|
264
369
|
├── wrapper.ts confluence() facade
|
|
265
370
|
├── csv.ts CSV helpers (subpath export ./csv)
|
|
266
371
|
└── cli.ts confluence-md-sync CLI
|
|
@@ -268,9 +373,9 @@ src/
|
|
|
268
373
|
|
|
269
374
|
## Roadmap
|
|
270
375
|
|
|
271
|
-
Cloud API v2 backend · page-tree sync from a directory ·
|
|
272
|
-
|
|
273
|
-
|
|
376
|
+
Cloud API v2 backend · page-tree sync from a directory · Mermaid/PlantUML
|
|
377
|
+
fences → images · fenced code → `code` macro · HTTP retry/backoff ·
|
|
378
|
+
comments API · dry-run diff preview.
|
|
274
379
|
|
|
275
380
|
Issues and PRs welcome.
|
|
276
381
|
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* CLI: три подкоманды.
|
|
4
4
|
*
|
|
5
5
|
* confluence-md-sync publish page.md --page-id 12345 \
|
|
6
6
|
* --image build/p1.png --file data.csv --label docs --dry-run
|
|
7
|
-
*
|
|
8
7
|
* confluence-md-sync publish page.md --space DOCS --title "My Page" --parent-id 777
|
|
9
8
|
*
|
|
9
|
+
* confluence-md-sync export 12345 --out-dir ./exported
|
|
10
|
+
* confluence-md-sync roundtrip 12345 # проверка без потерь, ничего не пишет
|
|
11
|
+
*
|
|
10
12
|
* Конфиг — из env: CONFLUENCE_BASE_URL, CONFLUENCE_TOKEN (или CONFLUENCE_PAT),
|
|
11
13
|
* CONFLUENCE_USERNAME, CONFLUENCE_AUTH_TYPE (bearer|basic).
|
|
12
14
|
*/
|
package/dist/cli.js
CHANGED
|
@@ -1,35 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* CLI: три подкоманды.
|
|
4
4
|
*
|
|
5
5
|
* confluence-md-sync publish page.md --page-id 12345 \
|
|
6
6
|
* --image build/p1.png --file data.csv --label docs --dry-run
|
|
7
|
-
*
|
|
8
7
|
* confluence-md-sync publish page.md --space DOCS --title "My Page" --parent-id 777
|
|
9
8
|
*
|
|
9
|
+
* confluence-md-sync export 12345 --out-dir ./exported
|
|
10
|
+
* confluence-md-sync roundtrip 12345 # проверка без потерь, ничего не пишет
|
|
11
|
+
*
|
|
10
12
|
* Конфиг — из env: CONFLUENCE_BASE_URL, CONFLUENCE_TOKEN (или CONFLUENCE_PAT),
|
|
11
13
|
* CONFLUENCE_USERNAME, CONFLUENCE_AUTH_TYPE (bearer|basic).
|
|
12
14
|
*/
|
|
13
15
|
import { parseArgs } from 'node:util';
|
|
14
16
|
import { loadConfigFromEnv } from './client/config.js';
|
|
15
17
|
import { publishPage } from './publish/publish.js';
|
|
16
|
-
|
|
18
|
+
import { exportPage } from './export/export-page.js';
|
|
19
|
+
import { roundTripPage } from './export/roundtrip.js';
|
|
20
|
+
const HELP = `confluence-md-sync — Markdown ⇄ Confluence
|
|
17
21
|
|
|
18
22
|
Usage:
|
|
19
|
-
confluence-md-sync publish
|
|
23
|
+
confluence-md-sync publish <markdown-file> [options]
|
|
24
|
+
confluence-md-sync export <page-id> [--out <file> | --out-dir <dir>] [--no-attachments]
|
|
25
|
+
confluence-md-sync roundtrip <page-id> [--show-markdown]
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
publish options:
|
|
22
28
|
--page-id <id> Target page ID
|
|
23
29
|
--space <key> Space key (with --title, when page ID is unknown)
|
|
24
30
|
--title <title> Page title (rename, or lookup/create key with --space)
|
|
25
31
|
--parent-id <id> Parent page for page creation
|
|
26
|
-
--image <
|
|
27
|
-
--file <
|
|
32
|
+
--image <src> Attach an image (repeatable), referenced as {{img:name.png}}
|
|
33
|
+
--file <src> Attach a file (repeatable), referenced as {{file:name.csv}}
|
|
34
|
+
<src> is a relative/absolute path or an http(s) URL;
|
|
35
|
+
URLs on the Confluence host are fetched with your token
|
|
28
36
|
--label <name> Ensure label on the page (repeatable)
|
|
29
37
|
--message <text> Page version comment
|
|
30
38
|
--no-create Fail instead of creating a missing page
|
|
31
39
|
--dry-run Render and validate without writing to Confluence
|
|
32
|
-
|
|
40
|
+
|
|
41
|
+
export options:
|
|
42
|
+
--out <file> Write the Markdown to exactly this path (attachments,
|
|
43
|
+
if any, go to attachments/ next to it)
|
|
44
|
+
--out-dir <dir> Output directory (default: ./<page-id>); writes page.md
|
|
45
|
+
and attachments/
|
|
46
|
+
--no-attachments Do not download referenced attachments
|
|
47
|
+
|
|
48
|
+
roundtrip options:
|
|
49
|
+
--show-markdown Print the intermediate Markdown to stdout
|
|
33
50
|
|
|
34
51
|
Environment:
|
|
35
52
|
CONFLUENCE_BASE_URL e.g. https://confluence.example.com
|
|
@@ -51,33 +68,64 @@ async function main() {
|
|
|
51
68
|
message: { type: 'string' },
|
|
52
69
|
'no-create': { type: 'boolean' },
|
|
53
70
|
'dry-run': { type: 'boolean' },
|
|
71
|
+
out: { type: 'string' },
|
|
72
|
+
'out-dir': { type: 'string' },
|
|
73
|
+
'no-attachments': { type: 'boolean' },
|
|
74
|
+
'show-markdown': { type: 'boolean' },
|
|
54
75
|
help: { type: 'boolean' },
|
|
55
76
|
},
|
|
56
77
|
});
|
|
57
|
-
|
|
78
|
+
const [command, arg] = positionals;
|
|
79
|
+
if (values.help || command === undefined) {
|
|
58
80
|
console.log(HELP);
|
|
59
81
|
process.exit(values.help ? 0 : 1);
|
|
60
82
|
}
|
|
61
|
-
const [command, markdownPath] = positionals;
|
|
62
|
-
if (command !== 'publish' || !markdownPath) {
|
|
63
|
-
console.error(`Unknown command or missing file. See --help.`);
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
83
|
const cfg = loadConfigFromEnv();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
84
|
+
if (command === 'publish') {
|
|
85
|
+
if (!arg)
|
|
86
|
+
throw new Error('publish: markdown file is required');
|
|
87
|
+
const result = await publishPage({
|
|
88
|
+
pageId: values['page-id'],
|
|
89
|
+
spaceKey: values.space,
|
|
90
|
+
title: values.title,
|
|
91
|
+
parentPageId: values['parent-id'],
|
|
92
|
+
markdownPath: arg,
|
|
93
|
+
images: values.image ?? [],
|
|
94
|
+
files: values.file ?? [],
|
|
95
|
+
labels: values.label,
|
|
96
|
+
versionMessage: values.message,
|
|
97
|
+
createIfMissing: values['no-create'] ? false : undefined,
|
|
98
|
+
dryRun: values['dry-run'],
|
|
99
|
+
}, cfg);
|
|
100
|
+
console.log(`[cli] ${result.updated ? 'published' : 'unchanged'}: page ${result.pageId} v${result.version}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (command === 'export') {
|
|
104
|
+
if (!arg)
|
|
105
|
+
throw new Error('export: page id is required');
|
|
106
|
+
const result = await exportPage(arg, {
|
|
107
|
+
outFile: values.out,
|
|
108
|
+
outDir: values['out-dir'],
|
|
109
|
+
downloadAttachments: !values['no-attachments'],
|
|
110
|
+
}, cfg);
|
|
111
|
+
console.log(`[cli] exported page ${result.pageId} "${result.title}" v${result.version} → ${result.markdownPath}` +
|
|
112
|
+
` (${result.downloaded.size} attachment(s), ${result.stats.fenced} raw storage block(s))`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (command === 'roundtrip') {
|
|
116
|
+
if (!arg)
|
|
117
|
+
throw new Error('roundtrip: page id is required');
|
|
118
|
+
const result = await roundTripPage(arg, cfg);
|
|
119
|
+
if (values['show-markdown'])
|
|
120
|
+
console.log(result.markdown);
|
|
121
|
+
console.log(`[cli] roundtrip page ${arg} "${result.title}": ${result.equal ? 'OK — no markup loss' : 'DIFFERENCES FOUND'}` +
|
|
122
|
+
` (markers=${result.stats.markers}, fenced=${result.stats.fenced}, rawHtml=${result.stats.rawHtml})`);
|
|
123
|
+
for (const d of result.diffs)
|
|
124
|
+
console.error(` ${d.path}: ${d.message}`);
|
|
125
|
+
process.exit(result.equal ? 0 : 2);
|
|
126
|
+
}
|
|
127
|
+
console.error(`Unknown command '${command}'. See --help.`);
|
|
128
|
+
process.exit(1);
|
|
81
129
|
}
|
|
82
130
|
main().catch((err) => {
|
|
83
131
|
console.error(`[cli] failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Канонизация storage-деревьев и сравнение «без потери разметки».
|
|
3
|
+
*
|
|
4
|
+
* Побайтовое равенство storage → md → storage недостижимо и не нужно:
|
|
5
|
+
* Confluence сам нормализует storage при каждом сохранении, ac:macro-id
|
|
6
|
+
* генерируется заново, markdown-it меняет форму сущностей. Критерий
|
|
7
|
+
* эквивалентности: XML-деревья равны после канонизации —
|
|
8
|
+
* - ac:macro-id / ac:schema-version отброшены;
|
|
9
|
+
* - сущности декодированы, пробельные последовательности схлопнуты;
|
|
10
|
+
* - пробельные text-узлы между блочными элементами удалены;
|
|
11
|
+
* - краевые пробелы вынесены из инлайн-форматирования
|
|
12
|
+
* (<strong>a </strong>b ≡ <strong>a</strong> b);
|
|
13
|
+
* - параметры макросов отсортированы по имени;
|
|
14
|
+
* - <img src=…> ≡ <ac:image><ri:url ri:value=…/></ac:image>;
|
|
15
|
+
* - атрибуты отсортированы по имени.
|
|
16
|
+
*/
|
|
17
|
+
import { type XNode } from './xhtml.js';
|
|
18
|
+
/** Канонический узел: element | text (после нормализации). */
|
|
19
|
+
export type CNode = {
|
|
20
|
+
kind: 'el';
|
|
21
|
+
name: string;
|
|
22
|
+
attrs: Record<string, string>;
|
|
23
|
+
children: CNode[];
|
|
24
|
+
} | {
|
|
25
|
+
kind: 'text';
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function canonicalize(nodes: XNode[]): CNode[];
|
|
29
|
+
export interface StorageDiff {
|
|
30
|
+
path: string;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
export interface CompareResult {
|
|
34
|
+
equal: boolean;
|
|
35
|
+
diffs: StorageDiff[];
|
|
36
|
+
}
|
|
37
|
+
/** Сравнивает два storage-фрагмента с точностью до канонизации. */
|
|
38
|
+
export declare function compareStorage(a: string, b: string, maxDiffs?: number): CompareResult;
|