confluence-cli 2.5.0 → 2.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/lib/html-to-storage.js +51 -0
- package/lib/macro-converter.js +16 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/lib/html-to-storage.js
CHANGED
|
@@ -21,6 +21,8 @@ class HtmlDepthExceededError extends Error {
|
|
|
21
21
|
// whatever shape the source had (markdown-it emits them without a slash).
|
|
22
22
|
const VOID_TAGS = new Set(['hr']);
|
|
23
23
|
const CALLOUT_MARKERS = ['info', 'warning', 'note'];
|
|
24
|
+
// these tags are wrapped into Confluence HTML macro
|
|
25
|
+
const HTML_MACRO_TAGS = new Set(['svg', 'div']);
|
|
24
26
|
|
|
25
27
|
// Phrasing-content tags that trigger the `<li>` / `<th>` / `<td>` `<p>`-wrap
|
|
26
28
|
// quirk: if an item contains only inline children and no text-node newline,
|
|
@@ -218,6 +220,36 @@ function convertBlockquote(node, ctx) {
|
|
|
218
220
|
</ac:structured-macro>`;
|
|
219
221
|
}
|
|
220
222
|
|
|
223
|
+
// `<details><summary>` becomes expand macro. If no summary child is found,
|
|
224
|
+
// falls through to plain HTML.
|
|
225
|
+
function convertDetails(node, ctx) {
|
|
226
|
+
const children = node.children || [];
|
|
227
|
+
let summaryNode = null;
|
|
228
|
+
let bodyNodes = [];
|
|
229
|
+
|
|
230
|
+
for (const child of children) {
|
|
231
|
+
if (child.type === 'tag' && child.name === 'summary') {
|
|
232
|
+
summaryNode = child;
|
|
233
|
+
} else if (!isWhitespaceOnly(child)) {
|
|
234
|
+
bodyNodes.push(child);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!summaryNode) {
|
|
239
|
+
return `<details${renderAttrs(node.attribs)}>${walkChildren(node, ctx)}</details>`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const titleHtml = walkChildren(summaryNode, ctx);
|
|
243
|
+
const cleanTitle = titleHtml.replace(/<[^>]+>/g, '').trim();
|
|
244
|
+
|
|
245
|
+
const bodyHtml = bodyNodes
|
|
246
|
+
.map((c) => walkNode(c, ctx))
|
|
247
|
+
.join('')
|
|
248
|
+
.trim();
|
|
249
|
+
|
|
250
|
+
return `<ac:structured-macro ac:name="expand"><ac:parameter ac:name="title">${cleanTitle}</ac:parameter><ac:rich-text-body>${bodyHtml}</ac:rich-text-body></ac:structured-macro>`;
|
|
251
|
+
}
|
|
252
|
+
|
|
221
253
|
// Strict `<pre><code>` adjacency only — `<pre>` with whitespace siblings or
|
|
222
254
|
// any other shape falls through as plain `<pre>`. The body needs manual
|
|
223
255
|
// entity decode because the parser keeps entities raw and CDATA is opaque
|
|
@@ -243,6 +275,20 @@ function convertCodeBlock(node, ctx) {
|
|
|
243
275
|
return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${language}</ac:parameter><ac:plain-text-body><![CDATA[${body}]]></ac:plain-text-body></ac:structured-macro>`;
|
|
244
276
|
}
|
|
245
277
|
|
|
278
|
+
// Wrap allowlisted HTML tags (svg, div) in Confluence HTML macro with CDATA.
|
|
279
|
+
// Used for embedding custom HTML that Confluence doesn't natively support.
|
|
280
|
+
function convertHtmlBlock(node, ctx) {
|
|
281
|
+
const { randomUUID } = require('crypto');
|
|
282
|
+
const inner = walkChildren(node, ctx);
|
|
283
|
+
const attrsStr = renderAttrs(node.attribs);
|
|
284
|
+
const openTag = `<${node.name}${attrsStr}>`;
|
|
285
|
+
const closeTag = `</${node.name}>`;
|
|
286
|
+
const htmlContent = openTag + inner + closeTag;
|
|
287
|
+
const safeContent = htmlContent.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
288
|
+
const macroId = randomUUID();
|
|
289
|
+
return `<ac:structured-macro ac:name="html" ac:schema-version="1" ac:macro-id="${macroId}"><ac:plain-text-body><![CDATA[${safeContent}]]></ac:plain-text-body></ac:structured-macro>`;
|
|
290
|
+
}
|
|
291
|
+
|
|
246
292
|
// Re-escape literal `"` inside attribute values. htmlparser2 with
|
|
247
293
|
// `decodeEntities: false` keeps source-escaped entities intact, but a
|
|
248
294
|
// single-quoted source attribute (`<a title='he said "hi"'>`) lands a
|
|
@@ -359,6 +405,8 @@ function dispatchTag(node, ctx) {
|
|
|
359
405
|
return convertLink(node, ctx);
|
|
360
406
|
case 'blockquote':
|
|
361
407
|
return convertBlockquote(node, ctx);
|
|
408
|
+
case 'details':
|
|
409
|
+
return convertDetails(node, ctx);
|
|
362
410
|
case 'table':
|
|
363
411
|
case 'thead':
|
|
364
412
|
case 'tbody':
|
|
@@ -375,6 +423,9 @@ function dispatchTag(node, ctx) {
|
|
|
375
423
|
if (VOID_TAGS.has(node.name)) {
|
|
376
424
|
return `<${node.name}${renderAttrs(node.attribs)} />`;
|
|
377
425
|
}
|
|
426
|
+
if (HTML_MACRO_TAGS.has(node.name)) {
|
|
427
|
+
return convertHtmlBlock(node, ctx);
|
|
428
|
+
}
|
|
378
429
|
return `<${node.name}${renderAttrs(node.attribs)}>${walkChildren(node, ctx)}</${node.name}>`;
|
|
379
430
|
}
|
|
380
431
|
}
|
package/lib/macro-converter.js
CHANGED
|
@@ -21,7 +21,9 @@ const STASH_DELIM = '\uE000';
|
|
|
21
21
|
// The body alternation `"[^"]*"|'[^']*'|[^>]` makes the match quote-aware
|
|
22
22
|
// so a literal `>` inside a quoted attribute value (e.g.
|
|
23
23
|
// `<mark title="1>0">`) does not terminate the tag prematurely.
|
|
24
|
-
const PASSTHROUGH_TAG_RE = /<\/?(?:u|sub|sup|mark)(?=[\s/>])(?:"[^"]*"|'[^']*'|[^>])*>/gi;
|
|
24
|
+
const PASSTHROUGH_TAG_RE = /<\/?(?:u|sub|sup|mark|details|summary)(?=[\s/>])(?:"[^"]*"|'[^']*'|[^>])*>/gi;
|
|
25
|
+
// Block-level HTML elements that should pass through WITHOUT markdown processing of their content.
|
|
26
|
+
const PASSTHROUGH_BLOCK_RE = /<(svg|div)(?:\s[^>]*)?>[\s\S]*?<\/\1>/gi;
|
|
25
27
|
// Single-backtick inline code spans. Block-level code (fenced + indented) is
|
|
26
28
|
// detected via MarkdownIt's tokenizer in `_findCodeRanges` because a regex
|
|
27
29
|
// can't reliably distinguish a 4-space-indented code block from a list-item
|
|
@@ -92,10 +94,19 @@ class MacroConverter {
|
|
|
92
94
|
_renderMarkdownToHtml(markdown) {
|
|
93
95
|
const codeRanges = this._findCodeRanges(markdown);
|
|
94
96
|
const htmlStash = [];
|
|
95
|
-
const stashHtml = (text) =>
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const stashHtml = (text) => {
|
|
98
|
+
// block-level HTML (svg, div with all content) must be stashed before inline tags to avoid matching the closing tag as inline HTML
|
|
99
|
+
let result = text.replace(PASSTHROUGH_BLOCK_RE, (m) => {
|
|
100
|
+
htmlStash.push(m);
|
|
101
|
+
return `${STASH_DELIM}H${htmlStash.length - 1}${STASH_DELIM}`;
|
|
102
|
+
});
|
|
103
|
+
// Then stash inline HTML tags
|
|
104
|
+
result = result.replace(PASSTHROUGH_TAG_RE, (m) => {
|
|
105
|
+
htmlStash.push(m);
|
|
106
|
+
return `${STASH_DELIM}H${htmlStash.length - 1}${STASH_DELIM}`;
|
|
107
|
+
});
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
99
110
|
|
|
100
111
|
let src = '';
|
|
101
112
|
let pos = 0;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "confluence-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "confluence-cli",
|
|
9
|
-
"version": "2.
|
|
9
|
+
"version": "2.6.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"axios": "~1.15.2",
|