confluence-md-sync 0.12.0 → 0.13.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.
|
@@ -357,6 +357,7 @@ class Converter {
|
|
|
357
357
|
toc: 'toc', children: 'children', jira: 'jira', status: 'status',
|
|
358
358
|
anchor: 'anchor', detailssummary: 'properties-report',
|
|
359
359
|
'portfolio-for-jira-plan': 'portfolio-for-jira-plan',
|
|
360
|
+
'table-excerpt-include': 'table-excerpt-include',
|
|
360
361
|
};
|
|
361
362
|
tryNativeMd(el, name) {
|
|
362
363
|
const parts = this.macroParts(el);
|
|
@@ -368,6 +369,23 @@ class Converter {
|
|
|
368
369
|
const pmap = new Map(params.map((p) => [p.name, p.value]));
|
|
369
370
|
const oneline = (v) => v !== undefined && !/[\n|{}]/.test(v) && !v.includes('}}');
|
|
370
371
|
let candidate = null;
|
|
372
|
+
if (['table-filter', 'table-excerpt', 'table-joiner'].includes(name) && richBody !== null) {
|
|
373
|
+
if (elements(el.children).filter(p => p.name === 'ac:parameter').some(p => elements(p.children).length))
|
|
374
|
+
return null;
|
|
375
|
+
if (params.some(p => !/^[\w-]+$/.test(p.name)))
|
|
376
|
+
return null;
|
|
377
|
+
const bodyMd = this.bodyMd(richBody);
|
|
378
|
+
if (bodyMd === null)
|
|
379
|
+
return null;
|
|
380
|
+
candidate = `::: ${name}${params.map(p => ` ${p.name}=${JSON.stringify(p.value)}`).join('')}\n${bodyMd}\n:::`;
|
|
381
|
+
if (this.verifyMacroMarker(el, candidate))
|
|
382
|
+
return candidate;
|
|
383
|
+
if (this.verifyTableMacro(el, candidate)) {
|
|
384
|
+
this.stats.normalized++;
|
|
385
|
+
return candidate;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
371
389
|
if ((name === 'drawio' || name === 'inc-drawio') && richBody === null) {
|
|
372
390
|
const filename = pmap.get('diagramName');
|
|
373
391
|
if (!filename || !oneline(filename) || /[/\\]/.test(filename))
|
|
@@ -449,7 +467,11 @@ class Converter {
|
|
|
449
467
|
const ph = Converter.NATIVE_PLACEHOLDER_NAME[name];
|
|
450
468
|
const attrs = [];
|
|
451
469
|
let head = '';
|
|
452
|
-
|
|
470
|
+
const nativeParams = name === 'table-excerpt-include'
|
|
471
|
+
? MACRO_EXTRACTORS[name]({ el, paramEls: elements(el.children).filter(p => p.name === 'ac:parameter'), richBody, plainBody })?.params : params;
|
|
472
|
+
if (!nativeParams)
|
|
473
|
+
return null;
|
|
474
|
+
for (const p of nativeParams) {
|
|
453
475
|
const value = /[|{}\n\r]/.test(p.value) || /^"/.test(p.value) ? JSON.stringify(p.value) : p.value;
|
|
454
476
|
if (name === 'jira' && p.name === 'key' && head === '') {
|
|
455
477
|
head = p.value;
|
|
@@ -464,7 +486,7 @@ class Converter {
|
|
|
464
486
|
continue;
|
|
465
487
|
}
|
|
466
488
|
const attrName = name === 'jira' && p.name === 'jqlQuery' ? 'jql' : p.name;
|
|
467
|
-
if (!/^[A-Za-
|
|
489
|
+
if (!/^[A-Za-z_][\w-]*$/.test(attrName))
|
|
468
490
|
return null;
|
|
469
491
|
attrs.push(`${attrName}=${value}`);
|
|
470
492
|
}
|
|
@@ -733,6 +755,36 @@ class Converter {
|
|
|
733
755
|
return false;
|
|
734
756
|
}
|
|
735
757
|
}
|
|
758
|
+
/** Accept editable GFM tables only when the surrounding macros/parameters and normalized cells match. */
|
|
759
|
+
verifyTableMacro(el, markdown) {
|
|
760
|
+
const saved = this.readable, stats = { ...this.stats };
|
|
761
|
+
try {
|
|
762
|
+
this.readable = true;
|
|
763
|
+
const render = (md) => processMacros(renderToStorage(md, { images: new Map(), files: new Map() }, { imageStyle: 'attachment', fileStyle: 'attachment', linkify: false }), this.registry).toString();
|
|
764
|
+
const normalize = (nodes) => nodes.flatMap(node => {
|
|
765
|
+
if (node.kind !== 'el')
|
|
766
|
+
return [node];
|
|
767
|
+
if (node.name === 'table') {
|
|
768
|
+
// Nested block macros require their own faithful conversion; never flatten their semantics.
|
|
769
|
+
const hasMacro = (ns) => ns.some(n => n.kind === 'el' && (n.name === 'ac:structured-macro' || hasMacro(n.children)));
|
|
770
|
+
if (hasMacro(node.children))
|
|
771
|
+
throw new Unrepresentable();
|
|
772
|
+
return parseStorage(render(this.readableTable(node)));
|
|
773
|
+
}
|
|
774
|
+
return [{ ...node, children: normalize(node.children) }];
|
|
775
|
+
});
|
|
776
|
+
return compareStorage(serializeStorage(normalize([el])), serializeStorage(normalize(parseStorage(render(markdown))))).equal;
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
if (!(error instanceof Unrepresentable))
|
|
780
|
+
throw error;
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
finally {
|
|
784
|
+
this.readable = saved;
|
|
785
|
+
Object.assign(this.stats, stats);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
736
788
|
containsMacroNamed(nodes, name) {
|
|
737
789
|
for (const n of nodes) {
|
|
738
790
|
if (n.kind !== 'el')
|
|
@@ -1424,6 +1476,12 @@ class Converter {
|
|
|
1424
1476
|
throw new Unrepresentable();
|
|
1425
1477
|
}
|
|
1426
1478
|
const textAttr = text !== undefined ? `|text=${text}` : '';
|
|
1479
|
+
if (ref.name === 'ri:user') {
|
|
1480
|
+
const identity = ref.attrs.find(([key]) => ['ri:userkey', 'ri:username', 'ri:account-id'].includes(key));
|
|
1481
|
+
if (!identity || ref.attrs.length !== 1)
|
|
1482
|
+
throw new Unrepresentable();
|
|
1483
|
+
return placeholder('user', decodeEntities(identity[1]), [['type', identity[0].slice(3)], ...(text === undefined ? [] : [['text', text]])]);
|
|
1484
|
+
}
|
|
1427
1485
|
if (ref.name === 'ri:page') {
|
|
1428
1486
|
if (!ref.attrs.every(([k]) => ['ri:content-title', 'ri:space-key', 'ri:version-at-save'].includes(k))) {
|
|
1429
1487
|
throw new Unrepresentable();
|
|
@@ -63,13 +63,7 @@ export const tableFilterPlugin = {
|
|
|
63
63
|
// Параметры: name — название выборки; hide — скрывать ли содержимое.
|
|
64
64
|
name: 'table-excerpt',
|
|
65
65
|
render: (ctx) => {
|
|
66
|
-
|
|
67
|
-
const params = [];
|
|
68
|
-
if (map.hide === 'true')
|
|
69
|
-
params.push({ name: 'hide', value: 'true' });
|
|
70
|
-
if (map.name)
|
|
71
|
-
params.push({ name: 'name', value: map.name });
|
|
72
|
-
return structuredMacro('table-excerpt', ctx.macroId, { params, richBody: ctx.body });
|
|
66
|
+
return structuredMacro('table-excerpt', ctx.macroId, { params: ctx.params, richBody: ctx.body });
|
|
73
67
|
},
|
|
74
68
|
},
|
|
75
69
|
{
|
package/dist/markdown/native.js
CHANGED
|
@@ -43,6 +43,9 @@ export const NATIVE_DIRECTIVES = {
|
|
|
43
43
|
properties: 'details',
|
|
44
44
|
expand: 'expand',
|
|
45
45
|
panel: 'panel',
|
|
46
|
+
'table-filter': 'table-filter',
|
|
47
|
+
'table-excerpt': 'table-excerpt',
|
|
48
|
+
'table-joiner': 'table-joiner',
|
|
46
49
|
};
|
|
47
50
|
/** Строчные плейсхолдеры: имя → имя макроса. */
|
|
48
51
|
export const NATIVE_PLACEHOLDERS = {
|
|
@@ -56,6 +59,7 @@ export const NATIVE_PLACEHOLDERS = {
|
|
|
56
59
|
anchor: 'anchor',
|
|
57
60
|
'properties-report': 'detailssummary',
|
|
58
61
|
'portfolio-for-jira-plan': 'portfolio-for-jira-plan',
|
|
62
|
+
'table-excerpt-include': 'table-excerpt-include',
|
|
59
63
|
};
|
|
60
64
|
/** Полный перечень макросов с нативной md-разметкой (для документации/UI). */
|
|
61
65
|
export function nativeMacroList() {
|
|
@@ -70,6 +74,8 @@ export function nativeMacroList() {
|
|
|
70
74
|
{ macro: 'details', syntax: '::: properties [id=…] [hidden=true] … :::' },
|
|
71
75
|
{ macro: 'expand', syntax: '::: expand Заголовок … :::' },
|
|
72
76
|
{ macro: 'panel', syntax: '::: panel title=… … :::' },
|
|
77
|
+
...['table-filter', 'table-excerpt', 'table-joiner'].map(macro => ({ macro, syntax: `::: ${macro} name="…" … :::` })),
|
|
78
|
+
{ macro: 'table-excerpt-include', syntax: '{{table-excerpt-include:name=…|page=…|space=…}}' },
|
|
73
79
|
{ macro: 'toc', syntax: '{{toc}} | {{toc:maxLevel=3}}' },
|
|
74
80
|
{ macro: 'children', syntax: '{{children}} | {{children:depth=2}}' },
|
|
75
81
|
{ macro: 'jira', syntax: '{{jira:KEY-1}} | {{jira:jql=…|maximumIssues=20}}' },
|
package/dist/markdown/render.js
CHANGED
|
@@ -84,7 +84,7 @@ export function renderToStorage(markdown, urls, opts = {}) {
|
|
|
84
84
|
while (markdown.includes(prefix))
|
|
85
85
|
prefix += 'X';
|
|
86
86
|
markdown = replaceInlineSyntax(markdown, ref => {
|
|
87
|
-
if (!['img', 'file', 'page', 'note'].includes(ref.id) && !(ref.id in NATIVE_PLACEHOLDERS))
|
|
87
|
+
if (!['img', 'file', 'page', 'user', 'note'].includes(ref.id) && !(ref.id in NATIVE_PLACEHOLDERS))
|
|
88
88
|
return;
|
|
89
89
|
const line = markdown.slice(markdown.lastIndexOf('\n', ref.from - 1) + 1, ref.from);
|
|
90
90
|
const cell = line.trimStart().startsWith('|');
|
|
@@ -96,6 +96,13 @@ export function renderToStorage(markdown, urls, opts = {}) {
|
|
|
96
96
|
let html = renderer.render(markdown);
|
|
97
97
|
const renderPlaceholder = (kind, body) => {
|
|
98
98
|
const { name, attrs } = parsePlaceholder(body);
|
|
99
|
+
if (kind === 'user') {
|
|
100
|
+
const map = Object.fromEntries(attrs), type = map.type || 'userkey';
|
|
101
|
+
if (!['userkey', 'username', 'account-id'].includes(type))
|
|
102
|
+
throw new Error('Unknown Confluence user identity type');
|
|
103
|
+
const body = map.text === undefined ? '' : `<ac:plain-text-link-body><![CDATA[${map.text.replace(/]]>/g, ']]]]><![CDATA[>')}]]></ac:plain-text-link-body>`;
|
|
104
|
+
return `<ac:link><ri:user ri:${type}="${escapeXmlAttr(name)}" />${body}</ac:link>`;
|
|
105
|
+
}
|
|
99
106
|
if (kind === 'page') {
|
|
100
107
|
return renderPageLink(name, attrs);
|
|
101
108
|
}
|
|
@@ -127,7 +134,7 @@ export function renderToStorage(markdown, urls, opts = {}) {
|
|
|
127
134
|
const body = renderToStorage(note.text.replace(/\r?\n/g, '\n\n'), urls, opts);
|
|
128
135
|
return renderInlineNote(body.trim(), note.attrs);
|
|
129
136
|
}
|
|
130
|
-
return ['img', 'file', 'page'].includes(ref.id) ? renderPlaceholder(ref.id, ref.body) : nativeToMarkers(ref.raw);
|
|
137
|
+
return ['img', 'file', 'page', 'user'].includes(ref.id) ? renderPlaceholder(ref.id, ref.body) : nativeToMarkers(ref.raw);
|
|
131
138
|
});
|
|
132
139
|
html = html.replace(/<p>((?:\s*<!-- MACRO:start:[^>]* -->\s*<!-- MACRO:end:[^>]* -->\s*)+)<\/p>/g, '$1');
|
|
133
140
|
// ```confluence-storage — транспорт для дословного XHTML (ac:/ri:-теги
|
package/package.json
CHANGED