confluence-md-sync 0.8.9 → 0.9.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.
@@ -20,6 +20,7 @@ export interface ConfluenceAttachment {
20
20
  };
21
21
  extensions?: {
22
22
  fileSize?: number;
23
+ mediaType?: string;
23
24
  };
24
25
  /**
25
26
  * `_links.download` — относительный путь скачивания, который Confluence
@@ -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 [att] = await client.listAttachments(pageId, name);
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/${pageId}/${encodeURIComponent(name)}`;
32
- const data = await client.downloadAttachment(link);
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;
@@ -147,10 +150,38 @@ class Converter {
147
150
  // tables:'records' — любую таблицу разворачиваем в записи.
148
151
  if (this.tablesAsRecords)
149
152
  return this.recordsTable(el);
150
- // Простую таблицу — в чистый GFM (оба режима). Сложную tableToMd
151
- // отклоняет: faithful → сырой HTML, readable → readableFallback
152
- // (lossy++ и разворот в GFM через readableTable).
153
- return this.tableToMd(el);
153
+ try {
154
+ return this.tableToMd(el);
155
+ }
156
+ catch (e) {
157
+ if (!(e instanceof Unrepresentable))
158
+ throw e;
159
+ // Строгая GFM-конвертация отклонила таблицу: то ли из-за косметики
160
+ // (Confluence лепит class="wrapped" style="width:…%" на КАЖДУЮ
161
+ // таблицу, а первую строку часто оформляет жирным <td> вместо <th>),
162
+ // то ли из-за реальных объединений (colspan/rowspan). В faithful без
163
+ // этой ветки ЛЮБАЯ такая таблица падала в дословный <table> HTML —
164
+ // нормализуем в GFM тем же путём, что readable (buildTableGrid не
165
+ // смотрит на атрибуты/семантику заголовка): оформление и объединения
166
+ // теряются при повторной публикации, но данные — md-таблица, не теги.
167
+ if (!this.readable) {
168
+ const saved = this.readable;
169
+ this.readable = true;
170
+ try {
171
+ const norm = this.readableTable(el);
172
+ this.stats.normalized++;
173
+ return norm;
174
+ }
175
+ catch (e2) {
176
+ if (!(e2 instanceof Unrepresentable))
177
+ throw e2;
178
+ }
179
+ finally {
180
+ this.readable = saved;
181
+ }
182
+ }
183
+ throw e;
184
+ }
154
185
  }
155
186
  if (el.name === 'ac:structured-macro')
156
187
  return this.macroToMd(el);
@@ -319,6 +350,22 @@ class Converter {
319
350
  const pmap = new Map(params.map((p) => [p.name, p.value]));
320
351
  const oneline = (v) => v !== undefined && !/[\n|{}]/.test(v) && !v.includes('}}');
321
352
  let candidate = null;
353
+ if ((name === 'drawio' || name === 'inc-drawio') && richBody === null) {
354
+ const filename = pmap.get('diagramName');
355
+ if (!filename || !oneline(filename) || /[/\\]/.test(filename))
356
+ return null;
357
+ if (params.some(p => !oneline(p.value) || /[|{}\n=]/.test(p.name)))
358
+ return null;
359
+ const attrs = params.filter(p => p.name !== 'diagramName').map(p => `|${p.name}=${p.value}`).join('');
360
+ candidate = `{{drawio:${filename}${attrs}}}`;
361
+ if (name === 'inc-drawio')
362
+ this.stats.normalized++;
363
+ else if (!this.readable && !this.verifyMacroMarker(el, candidate))
364
+ return null;
365
+ this.files.add(filename);
366
+ this.images.add(`${filename}.png`);
367
+ return candidate;
368
+ }
322
369
  const tag = Converter.NATIVE_ADMONITION_TAG[name];
323
370
  if (tag && richBody !== null) {
324
371
  // Панель: параметры — только title.
@@ -1188,6 +1235,18 @@ class Converter {
1188
1235
  if (kids.length !== 1)
1189
1236
  throw new Unrepresentable();
1190
1237
  const ref = kids[0];
1238
+ const filename = getAttr(ref, 'ri:filename') ?? '';
1239
+ const stem = filename.replace(/\.png$/i, '');
1240
+ const drawioName = getAttr(el, 'ac:alt')?.match(/^drawio:(.+)$/)?.[1]
1241
+ ?? (stem !== filename && (this.drawioNames.has(stem) || /\.drawio$/i.test(stem)) ? stem : undefined);
1242
+ if (drawioName && ref.name === 'ri:attachment' && !/[{}|\n/\\]/.test(drawioName)) {
1243
+ const preview = getAttr(ref, 'ri:filename');
1244
+ if (preview && !/[{}|\n/\\]/.test(preview)) {
1245
+ this.files.add(drawioName);
1246
+ this.images.add(preview);
1247
+ return `{{drawio:${drawioName}|format=image${preview !== `${drawioName}.png` ? `|preview=${preview}` : ''}}}`;
1248
+ }
1249
+ }
1191
1250
  // attachments:'local' — картинка как HTML <img> (сохраняет размеры).
1192
1251
  // ri:attachment → локальный путь; ri:url → внешний URL как есть.
1193
1252
  if (this.localFiles && (ref.name === 'ri:attachment' || ref.name === 'ri:url')) {
@@ -1236,7 +1295,6 @@ class Converter {
1236
1295
  if (!ref.attrs.every(([k]) => k === 'ri:filename' || k === 'ri:version-at-save')) {
1237
1296
  throw new Unrepresentable();
1238
1297
  }
1239
- const filename = getAttr(ref, 'ri:filename') ?? '';
1240
1298
  const attrs = [];
1241
1299
  for (const [k] of el.attrs) {
1242
1300
  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';
@@ -6,6 +6,7 @@ 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';
9
10
  import { MacroRegistry } from './registry.js';
10
11
  import { anchor, children, codeBlock, excerpt, excerptInclude, expand, includePage, jiraIssue, panel, status, toc } from './plugins/core.js';
11
12
  import { tableExcerpt, tableExcerptInclude, tableFilter, tableJoiner } from './plugins/table-filter.js';
@@ -48,4 +49,5 @@ export declare const macros: {
48
49
  tableJoiner: typeof tableJoiner;
49
50
  csvTable: typeof csvTable;
50
51
  portfolioForJiraPlan: typeof portfolioForJiraPlan;
52
+ drawio: (name: string, opts?: Record<string, string | undefined>) => import("../index.js").Markdown;
51
53
  };
@@ -6,6 +6,7 @@ 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';
9
10
  import { MacroRegistry } from './registry.js';
10
11
  import { coreMacrosPlugin } from './plugins/core.js';
11
12
  import { tableFilterPlugin } from './plugins/table-filter.js';
@@ -15,13 +16,15 @@ import { anchor, children, codeBlock, excerpt, excerptInclude, expand, includePa
15
16
  import { tableExcerpt, tableExcerptInclude, tableFilter, tableJoiner } from './plugins/table-filter.js';
16
17
  import { csvTable } from './plugins/csv-table.js';
17
18
  import { portfolioForJiraPlan } from './plugins/portfolio-for-jira-plan.js';
19
+ import { drawio, drawioPlugin } from './plugins/drawio.js';
18
20
  /** Creates a registry pre-loaded with all built-in plugins. */
19
21
  export function createDefaultRegistry() {
20
22
  return new MacroRegistry()
21
23
  .use(coreMacrosPlugin)
22
24
  .use(tableFilterPlugin)
23
25
  .use(csvTablePlugin)
24
- .use(portfolioForJiraPlanPlugin);
26
+ .use(portfolioForJiraPlanPlugin)
27
+ .use(drawioPlugin);
25
28
  }
26
29
  /**
27
30
  * Глобальный реестр по умолчанию (core + table-filter). Используется,
@@ -60,4 +63,5 @@ export const macros = {
60
63
  tableJoiner,
61
64
  csvTable,
62
65
  portfolioForJiraPlan,
66
+ drawio,
63
67
  };
@@ -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,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
+ }
@@ -45,6 +45,7 @@ export const NATIVE_DIRECTIVES = {
45
45
  };
46
46
  /** Строчные плейсхолдеры: имя → имя макроса. */
47
47
  export const NATIVE_PLACEHOLDERS = {
48
+ drawio: 'drawio',
48
49
  toc: 'toc',
49
50
  children: 'children',
50
51
  jira: 'jira',
@@ -56,6 +57,7 @@ export const NATIVE_PLACEHOLDERS = {
56
57
  /** Полный перечень макросов с нативной md-разметкой (для документации/UI). */
57
58
  export function nativeMacroList() {
58
59
  return [
60
+ { macro: 'drawio', syntax: '{{drawio:diagram.drawio|format=macro}} | {{drawio:diagram.drawio|format=image}}' },
59
61
  { macro: 'info', syntax: '> [!INFO] Заголовок?' },
60
62
  { macro: 'note', syntax: '> [!NOTE] Заголовок?' },
61
63
  { macro: 'warning', syntax: '> [!WARNING] Заголовок?' },
@@ -75,7 +77,7 @@ export function nativeMacroList() {
75
77
  const ADMONITION_FIRST_RE = /^>\s*\[!([A-Za-z]+)\]\s*(.*)$/;
76
78
  const DIRECTIVE_OPEN_RE = /^:::\s+([a-z-]+)(?:\s+(.*?))?\s*$/;
77
79
  const DIRECTIVE_CLOSE_RE = /^:::\s*$/;
78
- const PLACEHOLDER_INLINE_RE = /\{\{(toc|children|jira|status|anchor|properties-report|portfolio-for-jira-plan)(?::((?:[^{}]|\{[^{])*?))?\}\}/g;
80
+ const PLACEHOLDER_INLINE_RE = /\{\{(drawio|toc|children|jira|status|anchor|properties-report|portfolio-for-jira-plan)(?::((?:[^{}]|\{[^{])*?))?\}\}/g;
79
81
  const FENCE_RE = /^\s*(`{3,}|~{3,})/;
80
82
  /** Компактная пара маркеров без тела — для строчного контекста. */
81
83
  function inlineMarker(macroName, params) {
@@ -108,6 +110,8 @@ function placeholderParams(kind, raw) {
108
110
  if (raw === undefined || raw.trim() === '')
109
111
  return [];
110
112
  const { head, attrs } = splitAttrs(raw);
113
+ if (kind === 'drawio')
114
+ return [{ name: 'diagramName', value: head }, ...attrs];
111
115
  const params = [];
112
116
  const headIsPair = head.includes('=');
113
117
  if (headIsPair) {
@@ -206,6 +210,7 @@ export function nativeToMarkers(src) {
206
210
  let j = i + 1;
207
211
  let innerFence = null;
208
212
  let closed = false;
213
+ let depth = 1;
209
214
  for (; j < lines.length; j++) {
210
215
  const bl = lines[j];
211
216
  const bfm = FENCE_RE.exec(bl);
@@ -215,9 +220,14 @@ export function nativeToMarkers(src) {
215
220
  else if (bfm[1][0] === innerFence)
216
221
  innerFence = null;
217
222
  }
218
- else if (innerFence === null && DIRECTIVE_CLOSE_RE.test(bl)) {
219
- closed = true;
220
- break;
223
+ else if (innerFence === null) {
224
+ const nested = DIRECTIVE_OPEN_RE.exec(bl);
225
+ if (nested && NATIVE_DIRECTIVES[nested[1]])
226
+ depth++;
227
+ else if (DIRECTIVE_CLOSE_RE.test(bl) && --depth === 0) {
228
+ closed = true;
229
+ break;
230
+ }
221
231
  }
222
232
  body.push(bl);
223
233
  }
@@ -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)));
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-md-sync",
3
- "version": "0.8.9",
3
+ "version": "0.9.1",
4
4
  "description": "Publish Markdown to Confluence (Data Center & Cloud): idempotent page sync, attachment dedup, tables and a pluggable macro system",
5
5
  "keywords": [
6
6
  "confluence",