confluence-md-sync 0.8.10 → 0.10.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/dist/client/client.d.ts +1 -0
- package/dist/client/client.js +7 -2
- package/dist/export/export-page.js +9 -3
- package/dist/export/to-markdown.d.ts +2 -0
- package/dist/export/to-markdown.js +37 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/macros/index.d.ts +4 -0
- package/dist/macros/index.js +9 -1
- package/dist/macros/plugins/drawio.d.ts +6 -0
- package/dist/macros/plugins/drawio.js +24 -0
- package/dist/macros/plugins/search.d.ts +4 -0
- package/dist/macros/plugins/search.js +12 -0
- package/dist/markdown/drawio.d.ts +15 -0
- package/dist/markdown/drawio.js +28 -0
- package/dist/markdown/native.js +54 -12
- package/dist/markdown/validate.js +5 -0
- package/dist/publish/publish.js +2 -0
- package/package.json +1 -1
package/dist/client/client.d.ts
CHANGED
package/dist/client/client.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { authHeader } from './config.js';
|
|
4
|
+
function attachmentMime(name, bytes) {
|
|
5
|
+
if (/^\s*(?:<\?xml[^>]*>\s*)?<(mxfile|mxGraphModel)\b/.test(bytes.subarray(0, 4096).toString('utf8')))
|
|
6
|
+
return 'application/vnd.jgraph.mxfile';
|
|
7
|
+
return /\.png$/i.test(name) ? 'image/png' : 'application/octet-stream';
|
|
8
|
+
}
|
|
4
9
|
export class ConfluenceApiError extends Error {
|
|
5
10
|
status;
|
|
6
11
|
statusText;
|
|
@@ -230,7 +235,7 @@ export class ConfluenceClient {
|
|
|
230
235
|
const filename = basename(filePath);
|
|
231
236
|
const buf = readFileSync(filePath);
|
|
232
237
|
const form = new FormData();
|
|
233
|
-
form.append('file', new Blob([buf]), filename);
|
|
238
|
+
form.append('file', new Blob([buf], { type: attachmentMime(filename, buf) }), filename);
|
|
234
239
|
form.append('minorEdit', 'true');
|
|
235
240
|
if (comment)
|
|
236
241
|
form.append('comment', comment);
|
|
@@ -261,7 +266,7 @@ export class ConfluenceClient {
|
|
|
261
266
|
const filename = basename(filePath);
|
|
262
267
|
const buf = readFileSync(filePath);
|
|
263
268
|
const form = new FormData();
|
|
264
|
-
form.append('file', new Blob([buf]), filename);
|
|
269
|
+
form.append('file', new Blob([buf], { type: attachmentMime(filename, buf) }), filename);
|
|
265
270
|
form.append('minorEdit', 'true');
|
|
266
271
|
if (comment)
|
|
267
272
|
form.append('comment', comment);
|
|
@@ -6,10 +6,12 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
7
|
import { ConfluenceClient } from '../client/client.js';
|
|
8
8
|
import { storageToMarkdown } from './to-markdown.js';
|
|
9
|
+
import { drawioReferences, drawioAttachmentNames } from '../markdown/drawio.js';
|
|
9
10
|
export async function exportPage(pageId, opts, cfg) {
|
|
10
11
|
const client = new ConfluenceClient(cfg);
|
|
11
12
|
const page = await client.getPageStorage(pageId);
|
|
12
13
|
const converted = storageToMarkdown(page.storage, {
|
|
14
|
+
drawioNames: drawioAttachmentNames(await client.listAttachments(pageId)),
|
|
13
15
|
registry: opts.registry,
|
|
14
16
|
mode: opts.mode,
|
|
15
17
|
attachments: opts.attachments,
|
|
@@ -23,13 +25,17 @@ export async function exportPage(pageId, opts, cfg) {
|
|
|
23
25
|
const dir = join(dirname(markdownPath), 'attachments');
|
|
24
26
|
mkdirSync(dir, { recursive: true });
|
|
25
27
|
for (const name of converted.attachmentRefs) {
|
|
26
|
-
const
|
|
28
|
+
const diagram = drawioReferences(converted.markdown).find(d => d.name === name || d.preview === name);
|
|
29
|
+
const sourcePage = diagram?.pageId || pageId;
|
|
30
|
+
const [att] = await client.listAttachments(sourcePage, name);
|
|
27
31
|
if (att === undefined) {
|
|
28
32
|
console.warn(`[export] attachment '${name}' referenced by the page but not found`);
|
|
29
33
|
continue;
|
|
30
34
|
}
|
|
31
|
-
const link = att._links?.download ?? `/download/attachments/${
|
|
32
|
-
|
|
35
|
+
const link = new URL(att._links?.download ?? `/download/attachments/${sourcePage}/${encodeURIComponent(name)}`, cfg.baseUrl);
|
|
36
|
+
if (diagram?.name === name && diagram.revision)
|
|
37
|
+
link.searchParams.set('version', diagram.revision);
|
|
38
|
+
const data = await client.downloadAttachment(link.href);
|
|
33
39
|
const path = join(dir, name);
|
|
34
40
|
writeFileSync(path, data);
|
|
35
41
|
downloaded.set(name, path);
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { type MacroRegistry } from '../macros/registry.js';
|
|
23
23
|
export interface StorageToMarkdownOptions {
|
|
24
|
+
/** Known XML attachments; needed for image-mode diagrams without a .drawio suffix. */
|
|
25
|
+
drawioNames?: string[];
|
|
24
26
|
/** Реестр для проверки маркеров макросов (default: встроенный). */
|
|
25
27
|
registry?: MacroRegistry;
|
|
26
28
|
/**
|
|
@@ -39,6 +39,7 @@ export function storageToMarkdown(storage, opts = {}) {
|
|
|
39
39
|
localFiles: local,
|
|
40
40
|
tablesAsRecords: records,
|
|
41
41
|
localPrefix: opts.localPrefix ?? 'attachments/',
|
|
42
|
+
drawioNames: opts.drawioNames ?? [],
|
|
42
43
|
});
|
|
43
44
|
const markdown = conv.blocksToMd(parseStorage(storage));
|
|
44
45
|
const attachmentRefs = new Set([...conv.images, ...conv.files]);
|
|
@@ -78,6 +79,7 @@ const UNWRAP_BLOCK = new Set([
|
|
|
78
79
|
const SAFE_URL_RE = /^[A-Za-z0-9\-._~:/?#@!$&'*+,;=%]+$/;
|
|
79
80
|
class Converter {
|
|
80
81
|
registry;
|
|
82
|
+
drawioNames;
|
|
81
83
|
images = new Set();
|
|
82
84
|
files = new Set();
|
|
83
85
|
stats = { markers: 0, fenced: 0, rawHtml: 0, lossy: 0, native: 0, normalized: 0 };
|
|
@@ -87,6 +89,7 @@ class Converter {
|
|
|
87
89
|
localPrefix;
|
|
88
90
|
constructor(registry, opts) {
|
|
89
91
|
this.registry = registry;
|
|
92
|
+
this.drawioNames = new Set(opts.drawioNames);
|
|
90
93
|
this.readable = opts.readable;
|
|
91
94
|
this.localFiles = opts.localFiles;
|
|
92
95
|
this.tablesAsRecords = opts.tablesAsRecords;
|
|
@@ -333,6 +336,7 @@ class Converter {
|
|
|
333
336
|
info: 'INFO', note: 'NOTE', warning: 'WARNING', tip: 'TIP',
|
|
334
337
|
};
|
|
335
338
|
static NATIVE_PLACEHOLDER_NAME = {
|
|
339
|
+
search: 'search',
|
|
336
340
|
toc: 'toc', children: 'children', jira: 'jira', status: 'status',
|
|
337
341
|
anchor: 'anchor', detailssummary: 'properties-report',
|
|
338
342
|
'portfolio-for-jira-plan': 'portfolio-for-jira-plan',
|
|
@@ -347,6 +351,22 @@ class Converter {
|
|
|
347
351
|
const pmap = new Map(params.map((p) => [p.name, p.value]));
|
|
348
352
|
const oneline = (v) => v !== undefined && !/[\n|{}]/.test(v) && !v.includes('}}');
|
|
349
353
|
let candidate = null;
|
|
354
|
+
if ((name === 'drawio' || name === 'inc-drawio') && richBody === null) {
|
|
355
|
+
const filename = pmap.get('diagramName');
|
|
356
|
+
if (!filename || !oneline(filename) || /[/\\]/.test(filename))
|
|
357
|
+
return null;
|
|
358
|
+
if (params.some(p => !oneline(p.value) || /[|{}\n=]/.test(p.name)))
|
|
359
|
+
return null;
|
|
360
|
+
const attrs = params.filter(p => p.name !== 'diagramName').map(p => `|${p.name}=${p.value}`).join('');
|
|
361
|
+
candidate = `{{drawio:${filename}${attrs}}}`;
|
|
362
|
+
if (name === 'inc-drawio')
|
|
363
|
+
this.stats.normalized++;
|
|
364
|
+
else if (!this.readable && !this.verifyMacroMarker(el, candidate))
|
|
365
|
+
return null;
|
|
366
|
+
this.files.add(filename);
|
|
367
|
+
this.images.add(`${filename}.png`);
|
|
368
|
+
return candidate;
|
|
369
|
+
}
|
|
350
370
|
const tag = Converter.NATIVE_ADMONITION_TAG[name];
|
|
351
371
|
if (tag && richBody !== null) {
|
|
352
372
|
// Панель: параметры — только title.
|
|
@@ -413,8 +433,7 @@ class Converter {
|
|
|
413
433
|
const attrs = [];
|
|
414
434
|
let head = '';
|
|
415
435
|
for (const p of params) {
|
|
416
|
-
|
|
417
|
-
return null;
|
|
436
|
+
const value = /[|{}\n\r]/.test(p.value) || /^"/.test(p.value) ? JSON.stringify(p.value) : p.value;
|
|
418
437
|
if (name === 'jira' && p.name === 'key' && head === '') {
|
|
419
438
|
head = p.value;
|
|
420
439
|
continue;
|
|
@@ -430,7 +449,7 @@ class Converter {
|
|
|
430
449
|
const attrName = name === 'jira' && p.name === 'jqlQuery' ? 'jql' : p.name;
|
|
431
450
|
if (!/^[A-Za-z-]+$/.test(attrName))
|
|
432
451
|
return null;
|
|
433
|
-
attrs.push(`${attrName}=${
|
|
452
|
+
attrs.push(`${attrName}=${value}`);
|
|
434
453
|
}
|
|
435
454
|
const inner = [head, ...attrs].filter((s, i) => s !== '' || i > 0).join('|');
|
|
436
455
|
candidate = inner === '' ? `{{${ph}}}` : `{{${ph}:${inner}}}`;
|
|
@@ -449,7 +468,9 @@ class Converter {
|
|
|
449
468
|
for (const child of elements(el.children)) {
|
|
450
469
|
if (child.name === 'ac:parameter') {
|
|
451
470
|
const pname = getAttr(child, 'ac:name') ?? '';
|
|
452
|
-
|
|
471
|
+
const space = getAttr(el, 'ac:name') === 'search' && pname === 'spacekey'
|
|
472
|
+
? elements(child.children).find(n => n.name === 'ri:space') : undefined;
|
|
473
|
+
params.push({ name: pname, value: space ? getAttr(space, 'ri:space-key') ?? '' : decodeEntities(textContent(child.children)) });
|
|
453
474
|
}
|
|
454
475
|
else if (child.name === 'ac:rich-text-body')
|
|
455
476
|
richBody = child;
|
|
@@ -1216,6 +1237,18 @@ class Converter {
|
|
|
1216
1237
|
if (kids.length !== 1)
|
|
1217
1238
|
throw new Unrepresentable();
|
|
1218
1239
|
const ref = kids[0];
|
|
1240
|
+
const filename = getAttr(ref, 'ri:filename') ?? '';
|
|
1241
|
+
const stem = filename.replace(/\.png$/i, '');
|
|
1242
|
+
const drawioName = getAttr(el, 'ac:alt')?.match(/^drawio:(.+)$/)?.[1]
|
|
1243
|
+
?? (stem !== filename && (this.drawioNames.has(stem) || /\.drawio$/i.test(stem)) ? stem : undefined);
|
|
1244
|
+
if (drawioName && ref.name === 'ri:attachment' && !/[{}|\n/\\]/.test(drawioName)) {
|
|
1245
|
+
const preview = getAttr(ref, 'ri:filename');
|
|
1246
|
+
if (preview && !/[{}|\n/\\]/.test(preview)) {
|
|
1247
|
+
this.files.add(drawioName);
|
|
1248
|
+
this.images.add(preview);
|
|
1249
|
+
return `{{drawio:${drawioName}|format=image${preview !== `${drawioName}.png` ? `|preview=${preview}` : ''}}}`;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1219
1252
|
// attachments:'local' — картинка как HTML <img> (сохраняет размеры).
|
|
1220
1253
|
// ri:attachment → локальный путь; ri:url → внешний URL как есть.
|
|
1221
1254
|
if (this.localFiles && (ref.name === 'ri:attachment' || ref.name === 'ri:url')) {
|
|
@@ -1264,7 +1297,6 @@ class Converter {
|
|
|
1264
1297
|
if (!ref.attrs.every(([k]) => k === 'ri:filename' || k === 'ri:version-at-save')) {
|
|
1265
1298
|
throw new Unrepresentable();
|
|
1266
1299
|
}
|
|
1267
|
-
const filename = getAttr(ref, 'ri:filename') ?? '';
|
|
1268
1300
|
const attrs = [];
|
|
1269
1301
|
for (const [k] of el.attrs) {
|
|
1270
1302
|
if (!k.startsWith('ac:'))
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { authHeader, loadConfigFromEnv, type ConfluenceAuthType, type Confluence
|
|
|
3
3
|
export { Markdown } from './markdown/markdown.js';
|
|
4
4
|
export { renderToStorage, extractPlaceholders, parsePlaceholder, renameImagePlaceholders, MissingAttachmentUrlError, PLACEHOLDER_RE, type AttachmentUrls, type ExtractedPlaceholders, type PlaceholderRef, type RenderStorageOptions, } from './markdown/render.js';
|
|
5
5
|
export { validateMarkdown, MarkdownValidationError, type ValidateOptions } from './markdown/validate.js';
|
|
6
|
+
export { drawioReferences, drawioAttachmentNames, type DrawioReference } from './markdown/drawio.js';
|
|
6
7
|
export { nativeToMarkers, nativeMacroList, NATIVE_ADMONITIONS, NATIVE_DIRECTIVES, NATIVE_PLACEHOLDERS, } from './markdown/native.js';
|
|
7
8
|
export * from './macros/index.js';
|
|
8
9
|
export { convertBpmn, convertBpmnFolder, isBpmnFile, bpmnOutputName, BPMN_FILE_RE, type BpmnConversion, type BpmnImageFormat, type ConvertBpmnFolderOptions, } from './bpmn/convert.js';
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { authHeader, loadConfigFromEnv, } from './client/config.js';
|
|
|
5
5
|
export { Markdown } from './markdown/markdown.js';
|
|
6
6
|
export { renderToStorage, extractPlaceholders, parsePlaceholder, renameImagePlaceholders, MissingAttachmentUrlError, PLACEHOLDER_RE, } from './markdown/render.js';
|
|
7
7
|
export { validateMarkdown, MarkdownValidationError } from './markdown/validate.js';
|
|
8
|
+
export { drawioReferences, drawioAttachmentNames } from './markdown/drawio.js';
|
|
8
9
|
export { nativeToMarkers, nativeMacroList, NATIVE_ADMONITIONS, NATIVE_DIRECTIVES, NATIVE_PLACEHOLDERS, } from './markdown/native.js';
|
|
9
10
|
// Macros (pluggable)
|
|
10
11
|
export * from './macros/index.js';
|
package/dist/macros/index.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export { coreMacrosPlugin, extractPlainText } from './plugins/core.js';
|
|
|
6
6
|
export { tableFilterPlugin, TABLE_FILTER_DEFAULTS } from './plugins/table-filter.js';
|
|
7
7
|
export { csvTablePlugin, CSV_TABLE_DEFAULTS } from './plugins/csv-table.js';
|
|
8
8
|
export { portfolioForJiraPlanPlugin } from './plugins/portfolio-for-jira-plan.js';
|
|
9
|
+
export { drawioPlugin } from './plugins/drawio.js';
|
|
10
|
+
export { searchPlugin } from './plugins/search.js';
|
|
9
11
|
import { MacroRegistry } from './registry.js';
|
|
10
12
|
import { anchor, children, codeBlock, excerpt, excerptInclude, expand, includePage, jiraIssue, panel, status, toc } from './plugins/core.js';
|
|
11
13
|
import { tableExcerpt, tableExcerptInclude, tableFilter, tableJoiner } from './plugins/table-filter.js';
|
|
@@ -48,4 +50,6 @@ export declare const macros: {
|
|
|
48
50
|
tableJoiner: typeof tableJoiner;
|
|
49
51
|
csvTable: typeof csvTable;
|
|
50
52
|
portfolioForJiraPlan: typeof portfolioForJiraPlan;
|
|
53
|
+
drawio: (name: string, opts?: Record<string, string | undefined>) => import("../index.js").Markdown;
|
|
54
|
+
search: (query: string, options?: Record<string, string | undefined>) => import("../index.js").Markdown;
|
|
51
55
|
};
|
package/dist/macros/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export { coreMacrosPlugin, extractPlainText } from './plugins/core.js';
|
|
|
6
6
|
export { tableFilterPlugin, TABLE_FILTER_DEFAULTS } from './plugins/table-filter.js';
|
|
7
7
|
export { csvTablePlugin, CSV_TABLE_DEFAULTS } from './plugins/csv-table.js';
|
|
8
8
|
export { portfolioForJiraPlanPlugin } from './plugins/portfolio-for-jira-plan.js';
|
|
9
|
+
export { drawioPlugin } from './plugins/drawio.js';
|
|
10
|
+
export { searchPlugin } from './plugins/search.js';
|
|
9
11
|
import { MacroRegistry } from './registry.js';
|
|
10
12
|
import { coreMacrosPlugin } from './plugins/core.js';
|
|
11
13
|
import { tableFilterPlugin } from './plugins/table-filter.js';
|
|
@@ -15,13 +17,17 @@ import { anchor, children, codeBlock, excerpt, excerptInclude, expand, includePa
|
|
|
15
17
|
import { tableExcerpt, tableExcerptInclude, tableFilter, tableJoiner } from './plugins/table-filter.js';
|
|
16
18
|
import { csvTable } from './plugins/csv-table.js';
|
|
17
19
|
import { portfolioForJiraPlan } from './plugins/portfolio-for-jira-plan.js';
|
|
20
|
+
import { drawio, drawioPlugin } from './plugins/drawio.js';
|
|
21
|
+
import { search, searchPlugin } from './plugins/search.js';
|
|
18
22
|
/** Creates a registry pre-loaded with all built-in plugins. */
|
|
19
23
|
export function createDefaultRegistry() {
|
|
20
24
|
return new MacroRegistry()
|
|
21
25
|
.use(coreMacrosPlugin)
|
|
22
26
|
.use(tableFilterPlugin)
|
|
23
27
|
.use(csvTablePlugin)
|
|
24
|
-
.use(portfolioForJiraPlanPlugin)
|
|
28
|
+
.use(portfolioForJiraPlanPlugin)
|
|
29
|
+
.use(drawioPlugin)
|
|
30
|
+
.use(searchPlugin);
|
|
25
31
|
}
|
|
26
32
|
/**
|
|
27
33
|
* Глобальный реестр по умолчанию (core + table-filter). Используется,
|
|
@@ -60,4 +66,6 @@ export const macros = {
|
|
|
60
66
|
tableJoiner,
|
|
61
67
|
csvTable,
|
|
62
68
|
portfolioForJiraPlan,
|
|
69
|
+
drawio,
|
|
70
|
+
search,
|
|
63
71
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type MacroPlugin } from '../types.js';
|
|
2
|
+
/** XML keeps its attachment name, including extensionless Confluence diagrams. */
|
|
3
|
+
export declare const drawioPlugin: MacroPlugin;
|
|
4
|
+
export declare const drawio: (name: string, opts?: Record<string, string | undefined>) => import("../../index.js").Markdown;
|
|
5
|
+
/** Published attachments belong to the target page and use its latest version. */
|
|
6
|
+
export declare function unpinDrawioStorage(storage: string): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { macro } from '../builder.js';
|
|
2
|
+
import { paramMap } from '../types.js';
|
|
3
|
+
import { escapeXmlAttr, structuredMacro } from '../xml.js';
|
|
4
|
+
/** XML keeps its attachment name, including extensionless Confluence diagrams. */
|
|
5
|
+
export const drawioPlugin = {
|
|
6
|
+
name: 'drawio',
|
|
7
|
+
macros: [{ name: 'drawio', render(ctx) {
|
|
8
|
+
const params = paramMap(ctx.params);
|
|
9
|
+
const name = params.diagramName;
|
|
10
|
+
if (!name)
|
|
11
|
+
throw new Error('drawio: diagramName is required');
|
|
12
|
+
if (params.format === 'image') {
|
|
13
|
+
return `<ac:image><ri:attachment ri:filename="${escapeXmlAttr(params.preview || `${name}.png`)}" /></ac:image>`;
|
|
14
|
+
}
|
|
15
|
+
if (params.format && params.format !== 'macro')
|
|
16
|
+
throw new Error('drawio: format must be macro or image');
|
|
17
|
+
return structuredMacro('drawio', ctx.macroId, { params: ctx.params.filter(p => p.name !== 'format' && p.name !== 'preview').sort((a, b) => a.name.localeCompare(b.name)) });
|
|
18
|
+
} }],
|
|
19
|
+
};
|
|
20
|
+
export const drawio = (name, opts = {}) => macro('drawio').param('diagramName', name).withParams(opts).toMarkdown();
|
|
21
|
+
/** Published attachments belong to the target page and use its latest version. */
|
|
22
|
+
export function unpinDrawioStorage(storage) {
|
|
23
|
+
return storage.replace(/<ac:structured-macro\b[^>]*ac:name="drawio"[^>]*>[\s\S]*?<\/ac:structured-macro>/g, block => block.replace(/<ac:parameter ac:name="(?:revision|pageId)">[\s\S]*?<\/ac:parameter>/g, ''));
|
|
24
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { MacroPlugin } from '../types.js';
|
|
2
|
+
/** Search Results uses a space reference, not a text parameter, in storage. */
|
|
3
|
+
export declare const searchPlugin: MacroPlugin;
|
|
4
|
+
export declare const search: (query: string, options?: Record<string, string | undefined>) => import("../../index.js").Markdown;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { macro } from '../builder.js';
|
|
2
|
+
import { escapeXmlAttr, structuredMacro } from '../xml.js';
|
|
3
|
+
/** Search Results uses a space reference, not a text parameter, in storage. */
|
|
4
|
+
export const searchPlugin = {
|
|
5
|
+
name: 'search',
|
|
6
|
+
macros: [{ name: 'search', render: ctx => structuredMacro('search', ctx.macroId, {
|
|
7
|
+
params: [...ctx.params].sort((a, b) => a.name.localeCompare(b.name)).map(p => p.name === 'spacekey' && p.value
|
|
8
|
+
? { ...p, value: `<ri:space ri:space-key="${escapeXmlAttr(p.value)}" />`, raw: true }
|
|
9
|
+
: p),
|
|
10
|
+
}) }],
|
|
11
|
+
};
|
|
12
|
+
export const search = (query, options = {}) => macro('search').param('query', query).withParams(options).toMarkdown();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface DrawioReference {
|
|
2
|
+
name: string;
|
|
3
|
+
preview: string;
|
|
4
|
+
format: 'macro' | 'image';
|
|
5
|
+
revision?: string;
|
|
6
|
+
pageId?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function drawioAttachmentNames(attachments: {
|
|
9
|
+
title: string;
|
|
10
|
+
extensions?: {
|
|
11
|
+
mediaType?: string;
|
|
12
|
+
};
|
|
13
|
+
}[]): string[];
|
|
14
|
+
/** Skip code examples; ordinary and table-cell references remain attachments. */
|
|
15
|
+
export declare function drawioReferences(markdown: string): DrawioReference[];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it';
|
|
2
|
+
import { parsePlaceholder } from './render.js';
|
|
3
|
+
export function drawioAttachmentNames(attachments) {
|
|
4
|
+
return attachments.filter(a => a.extensions?.mediaType === 'application/vnd.jgraph.mxfile' || /\.drawio$/i.test(a.title)).map(a => a.title);
|
|
5
|
+
}
|
|
6
|
+
/** Skip code examples; ordinary and table-cell references remain attachments. */
|
|
7
|
+
export function drawioReferences(markdown) {
|
|
8
|
+
const refs = [];
|
|
9
|
+
const tokens = new MarkdownIt().parse(markdown, {});
|
|
10
|
+
for (const token of tokens)
|
|
11
|
+
for (const child of token.children ?? []) {
|
|
12
|
+
if (child.type !== 'text')
|
|
13
|
+
continue;
|
|
14
|
+
for (const match of child.content.matchAll(/\{\{drawio:([^}]+)\}\}/g)) {
|
|
15
|
+
const { name, attrs } = parsePlaceholder(match[1]);
|
|
16
|
+
const opts = Object.fromEntries(attrs);
|
|
17
|
+
if (!name || /[/\\]/.test(name) || name === '.' || name === '..')
|
|
18
|
+
throw new Error('drawio: invalid attachment filename');
|
|
19
|
+
if (opts.format && !['macro', 'image'].includes(opts.format))
|
|
20
|
+
throw new Error('drawio: format must be macro or image');
|
|
21
|
+
const preview = opts.preview || `${name}.png`;
|
|
22
|
+
if (/[/\\]/.test(preview) || preview === '.' || preview === '..')
|
|
23
|
+
throw new Error('drawio: invalid preview filename');
|
|
24
|
+
refs.push({ name, preview, format: opts.format === 'image' ? 'image' : 'macro', revision: opts.revision, pageId: opts.pageId });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return refs;
|
|
28
|
+
}
|
package/dist/markdown/native.js
CHANGED
|
@@ -45,6 +45,8 @@ export const NATIVE_DIRECTIVES = {
|
|
|
45
45
|
};
|
|
46
46
|
/** Строчные плейсхолдеры: имя → имя макроса. */
|
|
47
47
|
export const NATIVE_PLACEHOLDERS = {
|
|
48
|
+
search: 'search',
|
|
49
|
+
drawio: 'drawio',
|
|
48
50
|
toc: 'toc',
|
|
49
51
|
children: 'children',
|
|
50
52
|
jira: 'jira',
|
|
@@ -56,6 +58,8 @@ export const NATIVE_PLACEHOLDERS = {
|
|
|
56
58
|
/** Полный перечень макросов с нативной md-разметкой (для документации/UI). */
|
|
57
59
|
export function nativeMacroList() {
|
|
58
60
|
return [
|
|
61
|
+
{ macro: 'search', syntax: '{{search:query=…|spacekey=…|type=page|maxLimit=10}}' },
|
|
62
|
+
{ macro: 'drawio', syntax: '{{drawio:diagram.drawio|format=macro}} | {{drawio:diagram.drawio|format=image}}' },
|
|
59
63
|
{ macro: 'info', syntax: '> [!INFO] Заголовок?' },
|
|
60
64
|
{ macro: 'note', syntax: '> [!NOTE] Заголовок?' },
|
|
61
65
|
{ macro: 'warning', syntax: '> [!WARNING] Заголовок?' },
|
|
@@ -75,7 +79,7 @@ export function nativeMacroList() {
|
|
|
75
79
|
const ADMONITION_FIRST_RE = /^>\s*\[!([A-Za-z]+)\]\s*(.*)$/;
|
|
76
80
|
const DIRECTIVE_OPEN_RE = /^:::\s+([a-z-]+)(?:\s+(.*?))?\s*$/;
|
|
77
81
|
const DIRECTIVE_CLOSE_RE = /^:::\s*$/;
|
|
78
|
-
const PLACEHOLDER_INLINE_RE = /\{\{(toc|children|jira|status|anchor|properties-report|portfolio-for-jira-plan)(?::((?:[^{}]|\{[^{])*?))?\}\}/g;
|
|
82
|
+
const PLACEHOLDER_INLINE_RE = /\{\{(search|drawio|toc|children|jira|status|anchor|properties-report|portfolio-for-jira-plan)(?::((?:"(?:\\.|[^"\\])*"|[^{}"]|\{[^{])*?))?\}\}/g;
|
|
79
83
|
const FENCE_RE = /^\s*(`{3,}|~{3,})/;
|
|
80
84
|
/** Компактная пара маркеров без тела — для строчного контекста. */
|
|
81
85
|
function inlineMarker(macroName, params) {
|
|
@@ -92,29 +96,53 @@ function blockMarker(macroName, params, body) {
|
|
|
92
96
|
}
|
|
93
97
|
/** `a|b=c|d=e` → первый сегмент + пары; поведение как у parsePlaceholder. */
|
|
94
98
|
function splitAttrs(raw) {
|
|
95
|
-
const parts = raw.
|
|
99
|
+
const parts = raw.match(/(?:"(?:\\.|[^"\\])*"|[^|"])+/g) ?? [];
|
|
100
|
+
const value = (text) => {
|
|
101
|
+
const trimmed = text.trim();
|
|
102
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(trimmed);
|
|
105
|
+
}
|
|
106
|
+
catch { /* Unquoted legacy values remain literal. */ }
|
|
107
|
+
}
|
|
108
|
+
return trimmed;
|
|
109
|
+
};
|
|
96
110
|
const attrs = [];
|
|
97
111
|
for (const part of parts.slice(1)) {
|
|
98
112
|
const eq = part.indexOf('=');
|
|
99
113
|
if (eq === -1)
|
|
100
114
|
attrs.push({ name: part.trim(), value: '' });
|
|
101
115
|
else
|
|
102
|
-
attrs.push({ name: part.slice(0, eq).trim(), value: part.slice(eq + 1)
|
|
116
|
+
attrs.push({ name: part.slice(0, eq).trim(), value: value(part.slice(eq + 1)) });
|
|
103
117
|
}
|
|
104
|
-
return { head: parts[0]
|
|
118
|
+
return { head: parts[0]?.trim() ?? '', attrs };
|
|
105
119
|
}
|
|
106
120
|
/** Параметры плейсхолдера → параметры макроса Confluence. */
|
|
107
121
|
function placeholderParams(kind, raw) {
|
|
108
122
|
if (raw === undefined || raw.trim() === '')
|
|
109
123
|
return [];
|
|
110
124
|
const { head, attrs } = splitAttrs(raw);
|
|
125
|
+
if (kind === 'drawio')
|
|
126
|
+
return [{ name: 'diagramName', value: head }, ...attrs];
|
|
111
127
|
const params = [];
|
|
112
128
|
const headIsPair = head.includes('=');
|
|
113
129
|
if (headIsPair) {
|
|
114
130
|
const eq = head.indexOf('=');
|
|
115
|
-
|
|
131
|
+
const v = head.slice(eq + 1).trim();
|
|
132
|
+
let decoded = v;
|
|
133
|
+
if (v.startsWith('"') && v.endsWith('"')) {
|
|
134
|
+
try {
|
|
135
|
+
decoded = JSON.parse(v);
|
|
136
|
+
}
|
|
137
|
+
catch { /* Legacy unquoted values remain literal. */ }
|
|
138
|
+
}
|
|
139
|
+
attrs.unshift({ name: head.slice(0, eq).trim(), value: decoded });
|
|
116
140
|
}
|
|
117
141
|
switch (kind) {
|
|
142
|
+
case 'search':
|
|
143
|
+
if (!headIsPair && head !== '')
|
|
144
|
+
params.push({ name: 'query', value: head });
|
|
145
|
+
break;
|
|
118
146
|
case 'jira':
|
|
119
147
|
if (!headIsPair && head !== '')
|
|
120
148
|
params.push({ name: 'key', value: head });
|
|
@@ -150,12 +178,20 @@ function directiveParams(name, rest) {
|
|
|
150
178
|
if (!rest)
|
|
151
179
|
return params;
|
|
152
180
|
const free = [];
|
|
153
|
-
const re = /([A-Za-
|
|
181
|
+
const re = /([A-Za-z_][\w-]*)=("(?:\\.|[^"\\])*"|\S+)|(\S+)/g;
|
|
154
182
|
for (const m of rest.matchAll(re)) {
|
|
155
|
-
if (m[1])
|
|
156
|
-
|
|
183
|
+
if (m[1]) {
|
|
184
|
+
let value = m[2];
|
|
185
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
186
|
+
try {
|
|
187
|
+
value = JSON.parse(value);
|
|
188
|
+
}
|
|
189
|
+
catch { /* Preserve malformed legacy values as text. */ }
|
|
190
|
+
}
|
|
191
|
+
params.push({ name: m[1], value });
|
|
192
|
+
}
|
|
157
193
|
else
|
|
158
|
-
free.push(m[
|
|
194
|
+
free.push(m[3]);
|
|
159
195
|
}
|
|
160
196
|
if (free.length && name === 'expand')
|
|
161
197
|
params.unshift({ name: 'title', value: free.join(' ') });
|
|
@@ -206,6 +242,7 @@ export function nativeToMarkers(src) {
|
|
|
206
242
|
let j = i + 1;
|
|
207
243
|
let innerFence = null;
|
|
208
244
|
let closed = false;
|
|
245
|
+
let depth = 1;
|
|
209
246
|
for (; j < lines.length; j++) {
|
|
210
247
|
const bl = lines[j];
|
|
211
248
|
const bfm = FENCE_RE.exec(bl);
|
|
@@ -215,9 +252,14 @@ export function nativeToMarkers(src) {
|
|
|
215
252
|
else if (bfm[1][0] === innerFence)
|
|
216
253
|
innerFence = null;
|
|
217
254
|
}
|
|
218
|
-
else if (innerFence === null
|
|
219
|
-
|
|
220
|
-
|
|
255
|
+
else if (innerFence === null) {
|
|
256
|
+
const nested = DIRECTIVE_OPEN_RE.exec(bl);
|
|
257
|
+
if (nested && NATIVE_DIRECTIVES[nested[1]])
|
|
258
|
+
depth++;
|
|
259
|
+
else if (DIRECTIVE_CLOSE_RE.test(bl) && --depth === 0) {
|
|
260
|
+
closed = true;
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
221
263
|
}
|
|
222
264
|
body.push(bl);
|
|
223
265
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { extractPlaceholders } from './render.js';
|
|
3
|
+
import { drawioReferences } from './drawio.js';
|
|
3
4
|
export class MarkdownValidationError extends Error {
|
|
4
5
|
constructor(message) {
|
|
5
6
|
super(message);
|
|
@@ -8,6 +9,10 @@ export class MarkdownValidationError extends Error {
|
|
|
8
9
|
}
|
|
9
10
|
export function validateMarkdown(opts) {
|
|
10
11
|
const { images, files } = extractPlaceholders(opts.markdown);
|
|
12
|
+
for (const diagram of drawioReferences(opts.markdown)) {
|
|
13
|
+
files.push(diagram.name);
|
|
14
|
+
images.push(diagram.preview);
|
|
15
|
+
}
|
|
11
16
|
const tables = extractTablePlaceholders(opts.markdown);
|
|
12
17
|
const providedImages = new Set(opts.imagePaths.map((p) => basename(p)));
|
|
13
18
|
const providedFiles = new Set(opts.filePaths.map((p) => basename(p)));
|
package/dist/publish/publish.js
CHANGED
|
@@ -12,6 +12,7 @@ import { applyManagedNotice } from './notice.js';
|
|
|
12
12
|
import { processMacros } from '../macros/registry.js';
|
|
13
13
|
import { defaultMacroRegistry } from '../macros/index.js';
|
|
14
14
|
import { Markdown } from '../markdown/markdown.js';
|
|
15
|
+
import { unpinDrawioStorage } from '../macros/plugins/drawio.js';
|
|
15
16
|
// Content property с SHA-256 от рендеренного storage. Хранится отдельно
|
|
16
17
|
// от body через /rest/api/content/{id}/property/{key}, поэтому:
|
|
17
18
|
// - не попадает в HTML страницы (пользователь не видит);
|
|
@@ -200,6 +201,7 @@ export async function publishPage(opts, cfg) {
|
|
|
200
201
|
let storage = renderToStorage(markdown, urls, opts.render ?? {});
|
|
201
202
|
// 2.5. Преобразование маркеров макросов в XHTML.
|
|
202
203
|
storage = processMacros(storage, registry).toString();
|
|
204
|
+
storage = unpinDrawioStorage(storage);
|
|
203
205
|
// 2.6. Баннер «страница управляется извне». Вставляется здесь, в storage —
|
|
204
206
|
// не в markdown — поэтому источник/round-trip его не содержат. Баннер
|
|
205
207
|
// детерминирован (фиксированный macro-id), значит попадает в content-hash
|
package/package.json
CHANGED