confluence-cli 2.1.6 → 2.1.8

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.
@@ -1,3 +1,5 @@
1
+ const { fenceLength, cleanupWithFences } = require('./markdown-cleanup');
2
+
1
3
  const NAMED_ENTITIES = {
2
4
  aring: 'å', auml: 'ä', ouml: 'ö',
3
5
  eacute: 'é', egrave: 'è', ecirc: 'ê', euml: 'ë',
@@ -165,58 +167,7 @@ function htmlToMarkdown(html) {
165
167
 
166
168
  markdown = markdown.replace(/&([a-zA-Z]+);/g, (match, name) => NAMED_ENTITIES[name] || match);
167
169
 
168
- // Split on fenced code boundaries so cleanup rules (indent stripping,
169
- // multi-space collapsing) don't mangle indentation-sensitive code.
170
- // Backreference matches dynamically-sized fences emitted above when the
171
- // body itself contains backticks.
172
- const segments = splitOnFences(markdown);
173
- markdown = segments
174
- .map((seg, i) => (i % 2 === 1 ? seg : cleanupOutsideFence(seg)))
175
- .join('');
176
- markdown = markdown.trim();
177
-
178
- return markdown;
179
- }
180
-
181
- // CommonMark allows fenced code with N≥3 backticks where the body contains
182
- // no run of N+ backticks. Pick the smallest N satisfying both so a code
183
- // block whose payload itself contains ``` does not close its own fence.
184
- function fenceLength(body) {
185
- let max = 0;
186
- const runs = body.match(/`+/g);
187
- if (runs) {
188
- for (const r of runs) if (r.length > max) max = r.length;
189
- }
190
- return Math.max(3, max + 1);
191
- }
192
-
193
- function splitOnFences(text) {
194
- // CommonMark: a fence opens on a line that starts with up to 3 spaces
195
- // followed by 3+ backticks, and closes on a line of equal-length
196
- // backticks followed only by whitespace. Anchoring to line boundaries
197
- // (^ / $ with m flag) prevents prose backticks (e.g. <p>literal ``` x</p>)
198
- // from being mis-paired with real fence boundaries.
199
- const result = [];
200
- const re = /^ {0,3}(`{3,})[^\n]*\n[\s\S]*?\n {0,3}\1[\t ]*$/gm;
201
- let lastIdx = 0;
202
- let m;
203
- while ((m = re.exec(text)) !== null) {
204
- result.push(text.slice(lastIdx, m.index));
205
- result.push(m[0]);
206
- lastIdx = m.index + m[0].length;
207
- }
208
- result.push(text.slice(lastIdx));
209
- return result;
210
- }
211
-
212
- function cleanupOutsideFence(text) {
213
- let out = text;
214
- out = out.replace(/[ \t]+$/gm, '');
215
- out = out.replace(/^[ \t]+(?!([`>]|[*+-] |\d+[.)] ))/gm, '');
216
- out = out.replace(/^(#{1,6}[^\n]+)\n(?!\n)/gm, '$1\n\n');
217
- out = out.replace(/\n\s*\n\s*\n+/g, '\n\n');
218
- out = out.replace(/[ \t]+/g, ' ');
219
- return out;
170
+ return cleanupWithFences(markdown);
220
171
  }
221
172
 
222
173
  module.exports = {
@@ -0,0 +1,80 @@
1
+ // Shared fence-aware markdown cleanup for storage-walker and html-to-markdown.
2
+ // Both converters need the same set of operations:
3
+ // 1. Size opening/closing fences against the entity-decoded code body so a
4
+ // payload containing literal backticks (or numeric entities that decode
5
+ // to backticks) does not close its own fence.
6
+ // 2. Split the post-conversion text on fenced code boundaries using a
7
+ // CommonMark line-anchored matcher so prose `\`\`\`` cannot be mis-paired
8
+ // with a real fence opening.
9
+ // 3. Apply a 5-step whitespace cleanup chain only outside fenced code.
10
+ //
11
+ // Keeping these helpers in one place prevents the converters from drifting
12
+ // apart again — see issue #149 for the history.
13
+
14
+ // CommonMark allows fenced code with N≥3 backticks where the body contains
15
+ // no run of N+ backticks. Pick the smallest N satisfying both. Caller must
16
+ // pass an entity-decoded body — numeric entity refs like `&#96;` are
17
+ // backticks once decoded, so sizing before decode would leave the fence
18
+ // breakable when the entities resolve.
19
+ function fenceLength(decodedBody) {
20
+ let max = 0;
21
+ const runs = decodedBody.match(/`+/g);
22
+ if (runs) {
23
+ for (const r of runs) if (r.length > max) max = r.length;
24
+ }
25
+ return Math.max(3, max + 1);
26
+ }
27
+
28
+ // Split text on fenced code boundaries. Returns an alternating sequence of
29
+ // segments where even indices are outside-fence text and odd indices are
30
+ // full fenced blocks (delimiters included).
31
+ //
32
+ // CommonMark: a fence opens on a line of up to 3 spaces + 3+ backticks and
33
+ // closes on a line of equal-length backticks followed only by whitespace.
34
+ // Anchoring to line boundaries (^ / $ with the m flag) prevents prose
35
+ // backticks (e.g. a paragraph documenting markdown syntax) from being
36
+ // mis-paired with a real fence opening.
37
+ function splitOnFences(text) {
38
+ const result = [];
39
+ const re = /^ {0,3}(`{3,})[^\n]*\n[\s\S]*?\n {0,3}\1[\t ]*$/gm;
40
+ let lastIdx = 0;
41
+ let m;
42
+ while ((m = re.exec(text)) !== null) {
43
+ result.push(text.slice(lastIdx, m.index));
44
+ result.push(m[0]);
45
+ lastIdx = m.index + m[0].length;
46
+ }
47
+ result.push(text.slice(lastIdx));
48
+ return result;
49
+ }
50
+
51
+ // Whitespace cleanup safe to apply to text that sits outside fenced code.
52
+ // Strips trailing whitespace, strips leading whitespace except where it
53
+ // signals a list/blockquote/inline-code marker, ensures a blank line after
54
+ // headers, collapses 3+ blank lines, and squashes runs of inline whitespace.
55
+ function cleanupOutsideFence(text) {
56
+ let out = text;
57
+ out = out.replace(/[ \t]+$/gm, '');
58
+ out = out.replace(/^[ \t]+(?!([`>]|[*+-] |\d+[.)] ))/gm, '');
59
+ out = out.replace(/^(#{1,6}[^\n]+)\n(?!\n)/gm, '$1\n\n');
60
+ out = out.replace(/\n\s*\n\s*\n+/g, '\n\n');
61
+ out = out.replace(/[ \t]+/g, ' ');
62
+ return out;
63
+ }
64
+
65
+ // Apply outside-fence cleanup while leaving fenced code untouched, then
66
+ // trim leading and trailing whitespace from the joined result.
67
+ function cleanupWithFences(text) {
68
+ const segments = splitOnFences(text);
69
+ return segments
70
+ .map((seg, i) => (i % 2 === 1 ? seg : cleanupOutsideFence(seg)))
71
+ .join('')
72
+ .trim();
73
+ }
74
+
75
+ module.exports = {
76
+ fenceLength,
77
+ splitOnFences,
78
+ cleanupOutsideFence,
79
+ cleanupWithFences,
80
+ };
@@ -1,5 +1,6 @@
1
1
  const { parseDocument } = require('htmlparser2');
2
2
  const { decodeHTML } = require('entities');
3
+ const { fenceLength, cleanupWithFences } = require('./markdown-cleanup');
3
4
 
4
5
  const DEFAULT_MAX_DEPTH = 256;
5
6
 
@@ -125,6 +126,8 @@ class StorageWalker {
125
126
  return '**' + this.walkNodes(node.children) + '**';
126
127
  case 'em': case 'i':
127
128
  return '*' + this.walkNodes(node.children) + '*';
129
+ case 's': case 'del':
130
+ return '~~' + this.walkNodes(node.children) + '~~';
128
131
  case 'code':
129
132
  return '`' + this.walkNodes(node.children) + '`';
130
133
  case 'br':
@@ -260,7 +263,8 @@ class StorageWalker {
260
263
  const lang = langParam ? this.getTextContent(langParam) : '';
261
264
  const plainBody = this.findChildByName(node, 'ac:plain-text-body');
262
265
  const code = plainBody ? this.getRawText(plainBody) : '';
263
- return `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
266
+ const fence = '`'.repeat(fenceLength(code));
267
+ return `\n${fence}${lang}\n${code}\n${fence}\n`;
264
268
  }
265
269
 
266
270
  handleCallout(node, marker) {
@@ -296,7 +300,8 @@ class StorageWalker {
296
300
  handleMermaid(node) {
297
301
  const plainBody = this.findChildByName(node, 'ac:plain-text-body');
298
302
  const code = plainBody ? this.getRawText(plainBody).trim() : '';
299
- return `\n\`\`\`mermaid\n${code}\n\`\`\`\n`;
303
+ const fence = '`'.repeat(fenceLength(code));
304
+ return `\n${fence}mermaid\n${code}\n${fence}\n`;
300
305
  }
301
306
 
302
307
  handleInclude(node) {
@@ -351,9 +356,17 @@ class StorageWalker {
351
356
 
352
357
  handleImage(node) {
353
358
  const riAttachment = this.findChildByName(node, 'ri:attachment');
354
- if (!riAttachment) return '';
355
- const filename = decodeEntities(riAttachment.attribs['ri:filename'] || '');
356
- return `![${filename}](${this.attachmentsDir}/${filename})`;
359
+ if (riAttachment) {
360
+ const filename = decodeEntities(riAttachment.attribs['ri:filename'] || '');
361
+ return `![${filename}](${this.attachmentsDir}/${filename})`;
362
+ }
363
+ const riUrl = this.findChildByName(node, 'ri:url');
364
+ if (riUrl) {
365
+ const url = decodeEntities(riUrl.attribs['ri:value'] || '');
366
+ if (!url) return '';
367
+ return `![](${url})`;
368
+ }
369
+ return '';
357
370
  }
358
371
 
359
372
  handleAcLink(node) {
@@ -465,24 +478,7 @@ class StorageWalker {
465
478
  }
466
479
 
467
480
  cleanup(text) {
468
- // Split on fenced code boundaries so cleanup rules (indent stripping,
469
- // multi-space collapsing) don't mangle indentation-sensitive code.
470
- // Walker only emits triple-backtick fences (see code/mermaid macros).
471
- const segments = text.split(/(```[\s\S]*?```)/g);
472
- const cleaned = segments
473
- .map((seg, i) => (i % 2 === 1 ? seg : this._cleanupOutsideFence(seg)))
474
- .join('');
475
- return cleaned.trim();
476
- }
477
-
478
- _cleanupOutsideFence(text) {
479
- let out = text;
480
- out = out.replace(/[ \t]+$/gm, '');
481
- out = out.replace(/^[ \t]+(?!([`>]|[*+-] |\d+[.)] ))/gm, '');
482
- out = out.replace(/^(#{1,6}[^\n]+)\n(?!\n)/gm, '$1\n\n');
483
- out = out.replace(/\n\s*\n\s*\n+/g, '\n\n');
484
- out = out.replace(/[ \t]+/g, ' ');
485
- return out;
481
+ return cleanupWithFences(text);
486
482
  }
487
483
  }
488
484
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "2.1.6",
3
+ "version": "2.1.8",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "confluence-cli",
9
- "version": "2.1.6",
9
+ "version": "2.1.8",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "axios": "^1.15.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "2.1.6",
3
+ "version": "2.1.8",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {