confluence-md-sync 0.7.0 → 0.8.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/README.md CHANGED
@@ -318,6 +318,31 @@ const rows = readCsv(decodeText(await page.getAttachment('data.csv')));
318
318
 
319
319
  ## Macros
320
320
 
321
+ ### Native Markdown syntax (0.8.0+)
322
+
323
+ Popular macros are written (and exported back) as plain Markdown — no comment
324
+ markers, no raw XHTML. Round-trip is verified canonically per macro; anything
325
+ that doesn't fit falls back to markers/fenced blocks automatically.
326
+
327
+ | Macro | Markdown |
328
+ | --- | --- |
329
+ | `info` / `note` / `warning` / `tip` | `> [!INFO] Title?` + quoted body (GitHub-style admonition) |
330
+ | `details` (Page Properties) | `::: properties [id=…] [hidden=true]` + md-table + `:::` |
331
+ | `expand` | `::: expand Title` + body + `:::` |
332
+ | `panel` | `::: panel title=… borderColor=…` + body + `:::` |
333
+ | `toc` | `{{toc}}` / `{{toc:maxLevel=3}}` |
334
+ | `children` | `{{children}}` / `{{children:depth=2}}` |
335
+ | `jira` | `{{jira:KEY-1}}` / `{{jira:jql=project = X\|maximumIssues=20}}` |
336
+ | `status` | `{{status:Текст\|colour=Green\|subtle=true}}` |
337
+ | `anchor` | `{{anchor:name}}` |
338
+ | `detailssummary` (Page Properties Report) | `{{properties-report:cql=…\|firstcolumn=…}}` |
339
+
340
+ Inside fenced code blocks the syntax is left untouched. The machine-readable
341
+ list is exported as `nativeMacroList()`; the md→markers pass is
342
+ `nativeToMarkers()` (runs automatically inside `renderToStorage`).
343
+
344
+ ### Marker builders
345
+
321
346
  Builders return `Markdown` with comment markers; markers become
322
347
  `<ac:structured-macro>` after rendering (nested macros resolve inner-first):
323
348
 
@@ -61,6 +61,8 @@ export interface StorageToMarkdownResult {
61
61
  rawHtml: number;
62
62
  /** readable-режим: сколько узлов конвертировано с потерей оформления. */
63
63
  lossy: number;
64
+ /** Макросы, выраженные нативным md-синтаксисом (панели, ::: …, {{…}}). */
65
+ native: number;
64
66
  };
65
67
  }
66
68
  /** Конвертирует storage-фрагмент страницы в Markdown. */
@@ -79,7 +79,7 @@ class Converter {
79
79
  registry;
80
80
  images = new Set();
81
81
  files = new Set();
82
- stats = { markers: 0, fenced: 0, rawHtml: 0, lossy: 0 };
82
+ stats = { markers: 0, fenced: 0, rawHtml: 0, lossy: 0, native: 0 };
83
83
  readable;
84
84
  localFiles;
85
85
  tablesAsRecords;
@@ -290,8 +290,147 @@ class Converter {
290
290
  return escapeMdText(textContent(el.children), {}, this.readable).trim();
291
291
  }
292
292
  // ── Макросы ──────────────────────────────────────────────────────────
293
+ // ── Нативный md-синтаксис (native.ts): панели, ::: properties/expand/panel,
294
+ // {{toc}}/{{children}}/{{jira}}/{{status}}/{{anchor}}/{{properties-report}}.
295
+ // Каждый кандидат проверяется рендером и канонической сверкой (как маркеры).
296
+ static NATIVE_ADMONITION_TAG = {
297
+ info: 'INFO', note: 'NOTE', warning: 'WARNING', tip: 'TIP',
298
+ };
299
+ static NATIVE_PLACEHOLDER_NAME = {
300
+ toc: 'toc', children: 'children', jira: 'jira', status: 'status',
301
+ anchor: 'anchor', detailssummary: 'properties-report',
302
+ };
303
+ tryNativeMd(el, name) {
304
+ if (this.readable)
305
+ return null; // readable-путь остаётся прежним
306
+ const parts = this.macroParts(el);
307
+ if (parts === null)
308
+ return null;
309
+ const { params, richBody, plainBody } = parts;
310
+ if (plainBody !== null)
311
+ return null;
312
+ const pmap = new Map(params.map((p) => [p.name, p.value]));
313
+ const oneline = (v) => v !== undefined && !/[\n|{}]/.test(v) && !v.includes('}}');
314
+ let candidate = null;
315
+ const tag = Converter.NATIVE_ADMONITION_TAG[name];
316
+ if (tag && richBody !== null) {
317
+ // Панель: параметры — только title.
318
+ if (![...pmap.keys()].every((k) => k === 'title'))
319
+ return null;
320
+ const title = pmap.get('title');
321
+ if (title !== undefined && (!oneline(title) || /\[|\]/.test(title)))
322
+ return null;
323
+ const bodyMd = this.bodyMd(richBody);
324
+ if (bodyMd === null || bodyMd.trim() === '')
325
+ return null;
326
+ const quoted = bodyMd.split('\n').map((l) => (l === '' ? '>' : `> ${l}`)).join('\n');
327
+ candidate = `> [!${tag}]${title ? ' ' + title : ''}\n${quoted}`;
328
+ }
329
+ else if (name === 'details' && richBody !== null) {
330
+ // «Свойства страницы»: параметры id/hidden, тело — простая md-таблица.
331
+ if (![...pmap.keys()].every((k) => k === 'id' || k === 'hidden'))
332
+ return null;
333
+ const bodyEls = elements(richBody.children);
334
+ const nonWs = richBody.children.filter((n) => !(n.kind === 'text' && /^[ \t\r\n]*$/.test(n.raw)));
335
+ if (bodyEls.length !== 1 || nonWs.length !== 1 || bodyEls[0].name !== 'table')
336
+ return null;
337
+ let tableMd;
338
+ try {
339
+ tableMd = this.tableToMd(bodyEls[0]);
340
+ }
341
+ catch (e) {
342
+ if (e instanceof Unrepresentable)
343
+ return null;
344
+ throw e;
345
+ }
346
+ const dp = [...pmap.entries()].map(([k, v]) => (oneline(v) && !/["\s]/.test(v) ? ` ${k}=${v}` : null));
347
+ if (dp.some((x) => x === null))
348
+ return null;
349
+ candidate = `::: properties${dp.join('')}\n${tableMd}\n:::`;
350
+ }
351
+ else if (name === 'expand' && richBody !== null) {
352
+ if (![...pmap.keys()].every((k) => k === 'title'))
353
+ return null;
354
+ const title = pmap.get('title');
355
+ if (title !== undefined && (!oneline(title) || /=|"/.test(title)))
356
+ return null;
357
+ const bodyMd = this.bodyMd(richBody);
358
+ if (bodyMd === null)
359
+ return null;
360
+ candidate = `::: expand${title ? ' ' + title : ''}\n${bodyMd}\n:::`;
361
+ }
362
+ else if (Converter.NATIVE_PLACEHOLDER_NAME[name] && richBody === null) {
363
+ // Bodyless-плейсхолдеры. Части значений с | или }} не выразить — маркер.
364
+ const ph = Converter.NATIVE_PLACEHOLDER_NAME[name];
365
+ const attrs = [];
366
+ let head = '';
367
+ for (const p of params) {
368
+ if (!oneline(p.value) || p.value.includes('|'))
369
+ return null;
370
+ if (name === 'jira' && p.name === 'key' && head === '') {
371
+ head = p.value;
372
+ continue;
373
+ }
374
+ if (name === 'status' && p.name === 'title' && head === '') {
375
+ head = p.value;
376
+ continue;
377
+ }
378
+ if (name === 'anchor' && (p.name === 'name' || p.name === '') && head === '') {
379
+ head = p.value;
380
+ continue;
381
+ }
382
+ const attrName = name === 'jira' && p.name === 'jqlQuery' ? 'jql' : p.name;
383
+ if (!/^[A-Za-z-]+$/.test(attrName))
384
+ return null;
385
+ attrs.push(`${attrName}=${p.value}`);
386
+ }
387
+ const inner = [head, ...attrs].filter((s, i) => s !== '' || i > 0).join('|');
388
+ candidate = inner === '' ? `{{${ph}}}` : `{{${ph}:${inner}}}`;
389
+ }
390
+ if (candidate === null)
391
+ return null;
392
+ return this.verifyMacroMarker(el, candidate) ? candidate : null;
393
+ }
394
+ /** Разбирает macro-элемент на параметры и тела; null — посторонние дети. */
395
+ macroParts(el) {
396
+ const params = [];
397
+ let richBody = null;
398
+ let plainBody = null;
399
+ for (const child of elements(el.children)) {
400
+ if (child.name === 'ac:parameter') {
401
+ const pname = getAttr(child, 'ac:name') ?? '';
402
+ params.push({ name: pname, value: decodeEntities(textContent(child.children)) });
403
+ }
404
+ else if (child.name === 'ac:rich-text-body')
405
+ richBody = child;
406
+ else if (child.name === 'ac:plain-text-body')
407
+ plainBody = child;
408
+ else
409
+ return null;
410
+ }
411
+ return { params, richBody, plainBody };
412
+ }
413
+ /** Тело макроса → md; null, если не легло без потерь. */
414
+ bodyMd(richBody) {
415
+ const before = { images: new Set(this.images), files: new Set(this.files) };
416
+ try {
417
+ return this.blocksToMd(richBody.children).trimEnd();
418
+ }
419
+ catch (e) {
420
+ if (!(e instanceof Unrepresentable))
421
+ throw e;
422
+ this.images = before.images;
423
+ this.files = before.files;
424
+ return null;
425
+ }
426
+ }
293
427
  macroToMd(el) {
294
428
  const name = getAttr(el, 'ac:name') ?? '';
429
+ const nativeMd = this.tryNativeMd(el, name);
430
+ if (nativeMd !== null) {
431
+ this.stats.native++;
432
+ return nativeMd;
433
+ }
295
434
  const markerMd = this.tryMacroMarker(el, name);
296
435
  if (markerMd !== null) {
297
436
  this.stats.markers++;
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 { nativeToMarkers, nativeMacroList, NATIVE_ADMONITIONS, NATIVE_DIRECTIVES, NATIVE_PLACEHOLDERS, } from './markdown/native.js';
6
7
  export * from './macros/index.js';
7
8
  export { convertBpmn, convertBpmnFolder, isBpmnFile, bpmnOutputName, BPMN_FILE_RE, type BpmnConversion, type BpmnImageFormat, type ConvertBpmnFolderOptions, } from './bpmn/convert.js';
8
9
  export { fileSha256, HASH_TAG_PREFIX } from './attachments/hash.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 { nativeToMarkers, nativeMacroList, NATIVE_ADMONITIONS, NATIVE_DIRECTIVES, NATIVE_PLACEHOLDERS, } from './markdown/native.js';
8
9
  // Macros (pluggable)
9
10
  export * from './macros/index.js';
10
11
  // BPMN (optional peer dep 'bpmn-to-image' is loaded lazily on use)
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Нативный Markdown-синтаксис для популярных макросов Confluence.
3
+ * Сахар над маркерами <!-- MACRO:start/end -->: перед рендером native-
4
+ * конструкции переписываются в маркеры (nativeToMarkers), а экспорт
5
+ * (storage → md) эмитит их обратно с канонической верификацией.
6
+ *
7
+ * Перечень (см. README, раздел «Нативные макросы»):
8
+ *
9
+ * Панели (admonition в стиле GitHub):
10
+ * > [!INFO] Заголовок → info (заголовок опционален)
11
+ * > [!NOTE] / [!WARNING] / [!TIP] → note / warning / tip
12
+ * > текст панели — обычные строки цитаты после тега.
13
+ *
14
+ * Блоки-директивы (fenced ::: … :::):
15
+ * ::: properties [id=x] [hidden=true] → details («Свойства страницы»;
16
+ * тело — обычная md-таблица) работает с отчётом detailssummary
17
+ * ::: expand Заголовок → expand (разворачиваемый блок)
18
+ * ::: panel title=… borderColor=… → panel
19
+ *
20
+ * Строчные плейсхолдеры (семейство {{img:}}/{{page:}}):
21
+ * {{toc}} / {{toc:maxLevel=3}} → toc (оглавление)
22
+ * {{children}} / {{children:depth=2}} → children (дочерние страницы)
23
+ * {{jira:DR-123}} → jira (карточка задачи)
24
+ * {{jira:jql=project = DR|maximumIssues=20}} → jira (выгрузка по JQL)
25
+ * {{status:Готово|colour=Green}} → status (лейбл)
26
+ * {{anchor:имя}} → anchor (якорь)
27
+ * {{properties-report:cql=label = "x"}} → detailssummary (отчёт по
28
+ * свойствам страниц)
29
+ *
30
+ * Внутри fenced-код-блоков (``` / ~~~) синтаксис не интерпретируется.
31
+ */
32
+ /** Панели: тег admonition → имя макроса. */
33
+ export declare const NATIVE_ADMONITIONS: Record<string, string>;
34
+ /** Блоки-директивы: имя директивы → имя макроса. */
35
+ export declare const NATIVE_DIRECTIVES: Record<string, string>;
36
+ /** Строчные плейсхолдеры: имя → имя макроса. */
37
+ export declare const NATIVE_PLACEHOLDERS: Record<string, string>;
38
+ /** Полный перечень макросов с нативной md-разметкой (для документации/UI). */
39
+ export declare function nativeMacroList(): {
40
+ macro: string;
41
+ syntax: string;
42
+ }[];
43
+ /**
44
+ * Переписывает нативные конструкции в маркеры макросов. Идемпотентна для
45
+ * текста без нативного синтаксиса; содержимое fenced-код-блоков не трогает.
46
+ */
47
+ export declare function nativeToMarkers(src: string): string;
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Нативный Markdown-синтаксис для популярных макросов Confluence.
3
+ * Сахар над маркерами <!-- MACRO:start/end -->: перед рендером native-
4
+ * конструкции переписываются в маркеры (nativeToMarkers), а экспорт
5
+ * (storage → md) эмитит их обратно с канонической верификацией.
6
+ *
7
+ * Перечень (см. README, раздел «Нативные макросы»):
8
+ *
9
+ * Панели (admonition в стиле GitHub):
10
+ * > [!INFO] Заголовок → info (заголовок опционален)
11
+ * > [!NOTE] / [!WARNING] / [!TIP] → note / warning / tip
12
+ * > текст панели — обычные строки цитаты после тега.
13
+ *
14
+ * Блоки-директивы (fenced ::: … :::):
15
+ * ::: properties [id=x] [hidden=true] → details («Свойства страницы»;
16
+ * тело — обычная md-таблица) работает с отчётом detailssummary
17
+ * ::: expand Заголовок → expand (разворачиваемый блок)
18
+ * ::: panel title=… borderColor=… → panel
19
+ *
20
+ * Строчные плейсхолдеры (семейство {{img:}}/{{page:}}):
21
+ * {{toc}} / {{toc:maxLevel=3}} → toc (оглавление)
22
+ * {{children}} / {{children:depth=2}} → children (дочерние страницы)
23
+ * {{jira:DR-123}} → jira (карточка задачи)
24
+ * {{jira:jql=project = DR|maximumIssues=20}} → jira (выгрузка по JQL)
25
+ * {{status:Готово|colour=Green}} → status (лейбл)
26
+ * {{anchor:имя}} → anchor (якорь)
27
+ * {{properties-report:cql=label = "x"}} → detailssummary (отчёт по
28
+ * свойствам страниц)
29
+ *
30
+ * Внутри fenced-код-блоков (``` / ~~~) синтаксис не интерпретируется.
31
+ */
32
+ import { escapeParamValue } from '../macros/builder.js';
33
+ /** Панели: тег admonition → имя макроса. */
34
+ export const NATIVE_ADMONITIONS = {
35
+ INFO: 'info',
36
+ NOTE: 'note',
37
+ WARNING: 'warning',
38
+ TIP: 'tip',
39
+ };
40
+ /** Блоки-директивы: имя директивы → имя макроса. */
41
+ export const NATIVE_DIRECTIVES = {
42
+ properties: 'details',
43
+ expand: 'expand',
44
+ panel: 'panel',
45
+ };
46
+ /** Строчные плейсхолдеры: имя → имя макроса. */
47
+ export const NATIVE_PLACEHOLDERS = {
48
+ toc: 'toc',
49
+ children: 'children',
50
+ jira: 'jira',
51
+ status: 'status',
52
+ anchor: 'anchor',
53
+ 'properties-report': 'detailssummary',
54
+ };
55
+ /** Полный перечень макросов с нативной md-разметкой (для документации/UI). */
56
+ export function nativeMacroList() {
57
+ return [
58
+ { macro: 'info', syntax: '> [!INFO] Заголовок?' },
59
+ { macro: 'note', syntax: '> [!NOTE] Заголовок?' },
60
+ { macro: 'warning', syntax: '> [!WARNING] Заголовок?' },
61
+ { macro: 'tip', syntax: '> [!TIP] Заголовок?' },
62
+ { macro: 'details', syntax: '::: properties [id=…] [hidden=true] … :::' },
63
+ { macro: 'expand', syntax: '::: expand Заголовок … :::' },
64
+ { macro: 'panel', syntax: '::: panel title=… … :::' },
65
+ { macro: 'toc', syntax: '{{toc}} | {{toc:maxLevel=3}}' },
66
+ { macro: 'children', syntax: '{{children}} | {{children:depth=2}}' },
67
+ { macro: 'jira', syntax: '{{jira:KEY-1}} | {{jira:jql=…|maximumIssues=20}}' },
68
+ { macro: 'status', syntax: '{{status:Текст|colour=Green|subtle=true}}' },
69
+ { macro: 'anchor', syntax: '{{anchor:имя}}' },
70
+ { macro: 'detailssummary', syntax: '{{properties-report:cql=…|firstcolumn=…}}' },
71
+ ];
72
+ }
73
+ const ADMONITION_FIRST_RE = /^>\s*\[!([A-Za-z]+)\]\s*(.*)$/;
74
+ const DIRECTIVE_OPEN_RE = /^:::\s+([a-z-]+)(?:\s+(.*?))?\s*$/;
75
+ const DIRECTIVE_CLOSE_RE = /^:::\s*$/;
76
+ const PLACEHOLDER_INLINE_RE = /\{\{(toc|children|jira|status|anchor|properties-report)(?::((?:[^{}]|\{[^{])*?))?\}\}/g;
77
+ const FENCE_RE = /^\s*(`{3,}|~{3,})/;
78
+ /** Компактная пара маркеров без тела — для строчного контекста. */
79
+ function inlineMarker(macroName, params) {
80
+ const paramStr = params.length
81
+ ? ':' + params.map((p) => `${escapeParamValue(p.name)}=${escapeParamValue(p.value)}`).join(':')
82
+ : '';
83
+ return `<!-- MACRO:start:${macroName}${paramStr} --><!-- MACRO:end:${macroName} -->`;
84
+ }
85
+ function blockMarker(macroName, params, body) {
86
+ const paramStr = params.length
87
+ ? ':' + params.map((p) => `${escapeParamValue(p.name)}=${escapeParamValue(p.value)}`).join(':')
88
+ : '';
89
+ return `<!-- MACRO:start:${macroName}${paramStr} -->\n${body}\n<!-- MACRO:end:${macroName} -->`;
90
+ }
91
+ /** `a|b=c|d=e` → первый сегмент + пары; поведение как у parsePlaceholder. */
92
+ function splitAttrs(raw) {
93
+ const parts = raw.split('|');
94
+ const attrs = [];
95
+ for (const part of parts.slice(1)) {
96
+ const eq = part.indexOf('=');
97
+ if (eq === -1)
98
+ attrs.push({ name: part.trim(), value: '' });
99
+ else
100
+ attrs.push({ name: part.slice(0, eq).trim(), value: part.slice(eq + 1).trim() });
101
+ }
102
+ return { head: parts[0].trim(), attrs };
103
+ }
104
+ /** Параметры плейсхолдера → параметры макроса Confluence. */
105
+ function placeholderParams(kind, raw) {
106
+ if (raw === undefined || raw.trim() === '')
107
+ return [];
108
+ const { head, attrs } = splitAttrs(raw);
109
+ const params = [];
110
+ const headIsPair = head.includes('=');
111
+ if (headIsPair) {
112
+ const eq = head.indexOf('=');
113
+ attrs.unshift({ name: head.slice(0, eq).trim(), value: head.slice(eq + 1).trim() });
114
+ }
115
+ switch (kind) {
116
+ case 'jira':
117
+ if (!headIsPair && head !== '')
118
+ params.push({ name: 'key', value: head });
119
+ break;
120
+ case 'status':
121
+ if (!headIsPair && head !== '')
122
+ params.push({ name: 'title', value: head });
123
+ break;
124
+ case 'anchor':
125
+ if (!headIsPair && head !== '')
126
+ params.push({ name: 'name', value: head });
127
+ break;
128
+ default:
129
+ // toc/children/properties-report: только k=v-атрибуты.
130
+ if (!headIsPair && head !== '')
131
+ params.push({ name: head, value: '' });
132
+ }
133
+ for (const a of attrs) {
134
+ // jira: короткое `jql=` — алиас родного jqlQuery.
135
+ if (kind === 'jira' && a.name === 'jql')
136
+ params.push({ name: 'jqlQuery', value: a.value });
137
+ else
138
+ params.push(a);
139
+ }
140
+ return params;
141
+ }
142
+ function replaceInline(line) {
143
+ return line.replace(PLACEHOLDER_INLINE_RE, (_full, kind, raw) => inlineMarker(NATIVE_PLACEHOLDERS[kind], placeholderParams(kind, raw)));
144
+ }
145
+ /** Параметры директивы: токены `k=v` (значение можно в кавычках) + свободный текст → title. */
146
+ function directiveParams(name, rest) {
147
+ const params = [];
148
+ if (!rest)
149
+ return params;
150
+ const free = [];
151
+ const re = /([A-Za-z-]+)=("([^"]*)"|\S+)|(\S+)/g;
152
+ for (const m of rest.matchAll(re)) {
153
+ if (m[1])
154
+ params.push({ name: m[1], value: m[3] ?? m[2] });
155
+ else
156
+ free.push(m[4]);
157
+ }
158
+ if (free.length && name === 'expand')
159
+ params.unshift({ name: 'title', value: free.join(' ') });
160
+ return params;
161
+ }
162
+ /**
163
+ * Переписывает нативные конструкции в маркеры макросов. Идемпотентна для
164
+ * текста без нативного синтаксиса; содержимое fenced-код-блоков не трогает.
165
+ */
166
+ export function nativeToMarkers(src) {
167
+ const lines = src.split('\n');
168
+ const out = [];
169
+ let fence = null;
170
+ for (let i = 0; i < lines.length; i++) {
171
+ const line = lines[i];
172
+ const fm = FENCE_RE.exec(line);
173
+ if (fm) {
174
+ if (fence === null)
175
+ fence = fm[1][0];
176
+ else if (fm[1][0] === fence)
177
+ fence = null;
178
+ out.push(line);
179
+ continue;
180
+ }
181
+ if (fence !== null) {
182
+ out.push(line);
183
+ continue;
184
+ }
185
+ // ── Панели: > [!TYPE] Заголовок? ─────────────────────────────────────
186
+ const am = ADMONITION_FIRST_RE.exec(line);
187
+ const macroName = am ? NATIVE_ADMONITIONS[am[1].toUpperCase()] : undefined;
188
+ if (am && macroName) {
189
+ const body = [];
190
+ let j = i + 1;
191
+ for (; j < lines.length && /^>( |$)/.test(lines[j]); j++) {
192
+ body.push(lines[j].replace(/^> ?/, ''));
193
+ }
194
+ const params = am[2].trim() ? [{ name: 'title', value: am[2].trim() }] : [];
195
+ out.push(blockMarker(macroName, params, nativeToMarkers(body.join('\n'))));
196
+ i = j - 1;
197
+ continue;
198
+ }
199
+ // ── Директивы: ::: name … / ::: ──────────────────────────────────────
200
+ const dm = DIRECTIVE_OPEN_RE.exec(line);
201
+ const dirMacro = dm ? NATIVE_DIRECTIVES[dm[1]] : undefined;
202
+ if (dm && dirMacro) {
203
+ const body = [];
204
+ let j = i + 1;
205
+ let innerFence = null;
206
+ let closed = false;
207
+ for (; j < lines.length; j++) {
208
+ const bl = lines[j];
209
+ const bfm = FENCE_RE.exec(bl);
210
+ if (bfm) {
211
+ if (innerFence === null)
212
+ innerFence = bfm[1][0];
213
+ else if (bfm[1][0] === innerFence)
214
+ innerFence = null;
215
+ }
216
+ else if (innerFence === null && DIRECTIVE_CLOSE_RE.test(bl)) {
217
+ closed = true;
218
+ break;
219
+ }
220
+ body.push(bl);
221
+ }
222
+ if (closed) {
223
+ out.push(blockMarker(dirMacro, directiveParams(dm[1], dm[2]), nativeToMarkers(body.join('\n'))));
224
+ i = j;
225
+ continue;
226
+ }
227
+ // Незакрытая директива — оставляем как текст.
228
+ }
229
+ out.push(replaceInline(line));
230
+ }
231
+ return out.join('\n');
232
+ }
@@ -1,4 +1,5 @@
1
1
  import MarkdownIt from 'markdown-it';
2
+ import { nativeToMarkers } from './native.js';
2
3
  // xhtmlOut: true — Confluence storage format = XHTML, void-элементы
3
4
  // (<hr/>, <br/>, <img/>) обязаны быть самозакрывающимися.
4
5
  // html: true — разрешить HTML (нужно для <!-- MACRO:... --> комментариев)
@@ -106,6 +107,9 @@ export class MissingAttachmentUrlError extends Error {
106
107
  * посимвольно.
107
108
  */
108
109
  export function renderToStorage(markdown, urls, opts = {}) {
110
+ // Нативный md-синтаксис макросов (панели, ::: properties, {{toc}} …) —
111
+ // сахар над маркерами: переписываем до markdown-it (см. native.ts).
112
+ markdown = nativeToMarkers(markdown);
109
113
  const renderer = opts.linkify === false ? mdNoLinkify : md;
110
114
  let html = renderer.render(markdown);
111
115
  // ```confluence-storage — транспорт для дословного XHTML (ac:/ri:-теги
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-md-sync",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
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",