confluence-cli 2.3.0 → 2.3.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.
@@ -9,6 +9,25 @@ const CALLOUT_MARKERS = ['info', 'warning', 'note'];
9
9
  // and survives editor / formatter / lint passes that strip invisible chars.
10
10
  const STASH_DELIM = '\uE000';
11
11
 
12
+ // Inline HTML tags that markdown has no native syntax for. The walker emits
13
+ // them as raw HTML on storage\u2192markdown, so they must round-trip back through
14
+ // markdownToStorage; without this whitelist, MarkdownIt (html: false) escapes
15
+ // them to literal `<...>` and the formatting is lost.
16
+ //
17
+ // The lookahead `(?=[\s/>])` after the tag name rejects strings that look
18
+ // like markdown autolinks (e.g. `<u@example.com>`, `<sub:foo>`) \u2014 a plain
19
+ // `\b` word boundary would let these through and break linkify.
20
+ //
21
+ // The body alternation `"[^"]*"|'[^']*'|[^>]` makes the match quote-aware
22
+ // so a literal `>` inside a quoted attribute value (e.g.
23
+ // `<mark title="1>0">`) does not terminate the tag prematurely.
24
+ const PASSTHROUGH_TAG_RE = /<\/?(?:u|sub|sup|mark)(?=[\s/>])(?:"[^"]*"|'[^']*'|[^>])*>/gi;
25
+ // Single-backtick inline code spans. Block-level code (fenced + indented) is
26
+ // detected via MarkdownIt's tokenizer in `_findCodeRanges` because a regex
27
+ // can't reliably distinguish a 4-space-indented code block from a list-item
28
+ // continuation that happens to align to four spaces.
29
+ const INLINE_CODE_RE = /`[^`\n]+`/g;
30
+
12
31
  class MacroConverter {
13
32
  constructor({ isCloud = false, webUrlPrefix = '', buildUrl = null, linkStyle = null } = {}) {
14
33
  this._isCloud = isCloud;
@@ -57,13 +76,79 @@ class MacroConverter {
57
76
  }
58
77
 
59
78
  markdownToStorage(markdown) {
60
- const html = this.markdown.render(markdown);
61
- return this.htmlToConfluenceStorage(html);
79
+ return this.htmlToConfluenceStorage(this._renderMarkdownToHtml(markdown));
62
80
  }
63
81
 
64
82
  markdownToNativeStorage(markdown) {
65
- const html = this.markdown.render(markdown);
66
- return this.htmlToConfluenceStorage(html);
83
+ return this.htmlToConfluenceStorage(this._renderMarkdownToHtml(markdown));
84
+ }
85
+
86
+ // Pre-stashes whitelisted inline HTML so MarkdownIt won't escape it, renders,
87
+ // then restores. Code regions (fenced, indented, and inline) are skipped so a
88
+ // literal `<u>` typed inside a code block survives MarkdownIt's escape and
89
+ // round-trips as text rather than as a real tag — which would otherwise be
90
+ // smuggled past the escape and dropped by `convertCodeBlock`'s text-only
91
+ // child collection in html-to-storage.
92
+ _renderMarkdownToHtml(markdown) {
93
+ const codeRanges = this._findCodeRanges(markdown);
94
+ const htmlStash = [];
95
+ const stashHtml = (text) => text.replace(PASSTHROUGH_TAG_RE, (m) => {
96
+ htmlStash.push(m);
97
+ return `${STASH_DELIM}H${htmlStash.length - 1}${STASH_DELIM}`;
98
+ });
99
+
100
+ let src = '';
101
+ let pos = 0;
102
+ for (const [start, end] of codeRanges) {
103
+ src += stashHtml(markdown.slice(pos, start));
104
+ src += markdown.slice(start, end);
105
+ pos = end;
106
+ }
107
+ src += stashHtml(markdown.slice(pos));
108
+
109
+ const html = this.markdown.render(src);
110
+ return html.replace(
111
+ new RegExp(`${STASH_DELIM}H(\\d+)${STASH_DELIM}`, 'g'),
112
+ (m, i) => htmlStash[+i] ?? m,
113
+ );
114
+ }
115
+
116
+ // Returns merged, sorted character ranges covering all code regions in the
117
+ // markdown source — fenced and indented blocks via MarkdownIt's tokenizer
118
+ // (which correctly distinguishes them from list-item continuations) and
119
+ // single-backtick inline spans via regex (MarkdownIt parses these into
120
+ // `code_inline` tokens but does not expose source positions for them).
121
+ _findCodeRanges(markdown) {
122
+ const tokens = this.markdown.parse(markdown, {});
123
+ const lineStarts = [0];
124
+ for (let i = 0; i < markdown.length; i++) {
125
+ if (markdown[i] === '\n') lineStarts.push(i + 1);
126
+ }
127
+ const lineToChar = (n) => (n < lineStarts.length ? lineStarts[n] : markdown.length);
128
+
129
+ const ranges = [];
130
+ for (const tok of tokens) {
131
+ if ((tok.type === 'code_block' || tok.type === 'fence') && tok.map) {
132
+ ranges.push([lineToChar(tok.map[0]), lineToChar(tok.map[1])]);
133
+ }
134
+ }
135
+ INLINE_CODE_RE.lastIndex = 0;
136
+ let m;
137
+ while ((m = INLINE_CODE_RE.exec(markdown)) !== null) {
138
+ ranges.push([m.index, m.index + m[0].length]);
139
+ }
140
+
141
+ ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
142
+ const merged = [];
143
+ for (const r of ranges) {
144
+ const last = merged[merged.length - 1];
145
+ if (last && r[0] <= last[1]) {
146
+ last[1] = Math.max(last[1], r[1]);
147
+ } else {
148
+ merged.push([r[0], r[1]]);
149
+ }
150
+ }
151
+ return merged;
67
152
  }
68
153
 
69
154
  htmlToConfluenceStorage(html) {
@@ -200,6 +200,7 @@ class StorageWalker {
200
200
  case 'blockquote':
201
201
  return this.handleBlockquote(node);
202
202
  case 'details': case 'summary':
203
+ case 'u': case 'sub': case 'sup': case 'mark':
203
204
  return `<${tag}>` + this.walkNodes(node.children) + `</${tag}>`;
204
205
  case 'ac:structured-macro':
205
206
  return this.handleMacro(node);
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "confluence-cli",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "confluence-cli",
9
- "version": "2.3.0",
9
+ "version": "2.3.1",
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.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "A command-line interface for Atlassian Confluence with page creation and editing capabilities",
5
5
  "main": "index.js",
6
6
  "bin": {