confluence-md-sync 0.9.1 → 0.10.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.
@@ -140,6 +140,14 @@ class Converter {
140
140
  if (meaningful.length === 1 && meaningful[0].kind === 'el' && meaningful[0].name === 'ac:structured-macro') {
141
141
  return this.macroToMd(meaningful[0]);
142
142
  }
143
+ if (meaningful.length > 1 && meaningful.every(n => n.kind === 'el' && n.name === 'ac:structured-macro')) {
144
+ const native = meaningful.map(n => this.tryNativeMd(n, getAttr(n, 'ac:name') ?? ''));
145
+ if (native.every(s => s?.startsWith('{{') && !s.includes('\n'))) {
146
+ this.stats.native += native.length;
147
+ this.stats.normalized++; // A paragraph around block macros is redundant in Confluence.
148
+ return native.join(' ');
149
+ }
150
+ }
143
151
  return this.paragraphMd(el.children);
144
152
  }
145
153
  if (el.name === 'hr')
@@ -336,6 +344,7 @@ class Converter {
336
344
  info: 'INFO', note: 'NOTE', warning: 'WARNING', tip: 'TIP',
337
345
  };
338
346
  static NATIVE_PLACEHOLDER_NAME = {
347
+ search: 'search',
339
348
  toc: 'toc', children: 'children', jira: 'jira', status: 'status',
340
349
  anchor: 'anchor', detailssummary: 'properties-report',
341
350
  'portfolio-for-jira-plan': 'portfolio-for-jira-plan',
@@ -432,8 +441,7 @@ class Converter {
432
441
  const attrs = [];
433
442
  let head = '';
434
443
  for (const p of params) {
435
- if (!oneline(p.value) || p.value.includes('|'))
436
- return null;
444
+ const value = /[|{}\n\r]/.test(p.value) || /^"/.test(p.value) ? JSON.stringify(p.value) : p.value;
437
445
  if (name === 'jira' && p.name === 'key' && head === '') {
438
446
  head = p.value;
439
447
  continue;
@@ -449,7 +457,7 @@ class Converter {
449
457
  const attrName = name === 'jira' && p.name === 'jqlQuery' ? 'jql' : p.name;
450
458
  if (!/^[A-Za-z-]+$/.test(attrName))
451
459
  return null;
452
- attrs.push(`${attrName}=${p.value}`);
460
+ attrs.push(`${attrName}=${value}`);
453
461
  }
454
462
  const inner = [head, ...attrs].filter((s, i) => s !== '' || i > 0).join('|');
455
463
  candidate = inner === '' ? `{{${ph}}}` : `{{${ph}:${inner}}}`;
@@ -468,7 +476,9 @@ class Converter {
468
476
  for (const child of elements(el.children)) {
469
477
  if (child.name === 'ac:parameter') {
470
478
  const pname = getAttr(child, 'ac:name') ?? '';
471
- params.push({ name: pname, value: decodeEntities(textContent(child.children)) });
479
+ const space = getAttr(el, 'ac:name') === 'search' && pname === 'spacekey'
480
+ ? elements(child.children).find(n => n.name === 'ri:space') : undefined;
481
+ params.push({ name: pname, value: space ? getAttr(space, 'ri:space-key') ?? '' : decodeEntities(textContent(child.children)) });
472
482
  }
473
483
  else if (child.name === 'ac:rich-text-body')
474
484
  richBody = child;
@@ -7,6 +7,7 @@ export { tableFilterPlugin, TABLE_FILTER_DEFAULTS } from './plugins/table-filter
7
7
  export { csvTablePlugin, CSV_TABLE_DEFAULTS } from './plugins/csv-table.js';
8
8
  export { portfolioForJiraPlanPlugin } from './plugins/portfolio-for-jira-plan.js';
9
9
  export { drawioPlugin } from './plugins/drawio.js';
10
+ export { searchPlugin } from './plugins/search.js';
10
11
  import { MacroRegistry } from './registry.js';
11
12
  import { anchor, children, codeBlock, excerpt, excerptInclude, expand, includePage, jiraIssue, panel, status, toc } from './plugins/core.js';
12
13
  import { tableExcerpt, tableExcerptInclude, tableFilter, tableJoiner } from './plugins/table-filter.js';
@@ -50,4 +51,5 @@ export declare const macros: {
50
51
  csvTable: typeof csvTable;
51
52
  portfolioForJiraPlan: typeof portfolioForJiraPlan;
52
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;
53
55
  };
@@ -7,6 +7,7 @@ export { tableFilterPlugin, TABLE_FILTER_DEFAULTS } from './plugins/table-filter
7
7
  export { csvTablePlugin, CSV_TABLE_DEFAULTS } from './plugins/csv-table.js';
8
8
  export { portfolioForJiraPlanPlugin } from './plugins/portfolio-for-jira-plan.js';
9
9
  export { drawioPlugin } from './plugins/drawio.js';
10
+ export { searchPlugin } from './plugins/search.js';
10
11
  import { MacroRegistry } from './registry.js';
11
12
  import { coreMacrosPlugin } from './plugins/core.js';
12
13
  import { tableFilterPlugin } from './plugins/table-filter.js';
@@ -17,6 +18,7 @@ import { tableExcerpt, tableExcerptInclude, tableFilter, tableJoiner } from './p
17
18
  import { csvTable } from './plugins/csv-table.js';
18
19
  import { portfolioForJiraPlan } from './plugins/portfolio-for-jira-plan.js';
19
20
  import { drawio, drawioPlugin } from './plugins/drawio.js';
21
+ import { search, searchPlugin } from './plugins/search.js';
20
22
  /** Creates a registry pre-loaded with all built-in plugins. */
21
23
  export function createDefaultRegistry() {
22
24
  return new MacroRegistry()
@@ -24,7 +26,8 @@ export function createDefaultRegistry() {
24
26
  .use(tableFilterPlugin)
25
27
  .use(csvTablePlugin)
26
28
  .use(portfolioForJiraPlanPlugin)
27
- .use(drawioPlugin);
29
+ .use(drawioPlugin)
30
+ .use(searchPlugin);
28
31
  }
29
32
  /**
30
33
  * Глобальный реестр по умолчанию (core + table-filter). Используется,
@@ -64,4 +67,5 @@ export const macros = {
64
67
  csvTable,
65
68
  portfolioForJiraPlan,
66
69
  drawio,
70
+ search,
67
71
  };
@@ -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();
@@ -45,6 +45,7 @@ export const NATIVE_DIRECTIVES = {
45
45
  };
46
46
  /** Строчные плейсхолдеры: имя → имя макроса. */
47
47
  export const NATIVE_PLACEHOLDERS = {
48
+ search: 'search',
48
49
  drawio: 'drawio',
49
50
  toc: 'toc',
50
51
  children: 'children',
@@ -57,6 +58,7 @@ export const NATIVE_PLACEHOLDERS = {
57
58
  /** Полный перечень макросов с нативной md-разметкой (для документации/UI). */
58
59
  export function nativeMacroList() {
59
60
  return [
61
+ { macro: 'search', syntax: '{{search:query=…|spacekey=…|type=page|maxLimit=10}}' },
60
62
  { macro: 'drawio', syntax: '{{drawio:diagram.drawio|format=macro}} | {{drawio:diagram.drawio|format=image}}' },
61
63
  { macro: 'info', syntax: '> [!INFO] Заголовок?' },
62
64
  { macro: 'note', syntax: '> [!NOTE] Заголовок?' },
@@ -77,7 +79,7 @@ export function nativeMacroList() {
77
79
  const ADMONITION_FIRST_RE = /^>\s*\[!([A-Za-z]+)\]\s*(.*)$/;
78
80
  const DIRECTIVE_OPEN_RE = /^:::\s+([a-z-]+)(?:\s+(.*?))?\s*$/;
79
81
  const DIRECTIVE_CLOSE_RE = /^:::\s*$/;
80
- const PLACEHOLDER_INLINE_RE = /\{\{(drawio|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;
81
83
  const FENCE_RE = /^\s*(`{3,}|~{3,})/;
82
84
  /** Компактная пара маркеров без тела — для строчного контекста. */
83
85
  function inlineMarker(macroName, params) {
@@ -94,16 +96,26 @@ function blockMarker(macroName, params, body) {
94
96
  }
95
97
  /** `a|b=c|d=e` → первый сегмент + пары; поведение как у parsePlaceholder. */
96
98
  function splitAttrs(raw) {
97
- const parts = raw.split('|');
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
+ };
98
110
  const attrs = [];
99
111
  for (const part of parts.slice(1)) {
100
112
  const eq = part.indexOf('=');
101
113
  if (eq === -1)
102
114
  attrs.push({ name: part.trim(), value: '' });
103
115
  else
104
- attrs.push({ name: part.slice(0, eq).trim(), value: part.slice(eq + 1).trim() });
116
+ attrs.push({ name: part.slice(0, eq).trim(), value: value(part.slice(eq + 1)) });
105
117
  }
106
- return { head: parts[0].trim(), attrs };
118
+ return { head: parts[0]?.trim() ?? '', attrs };
107
119
  }
108
120
  /** Параметры плейсхолдера → параметры макроса Confluence. */
109
121
  function placeholderParams(kind, raw) {
@@ -116,9 +128,21 @@ function placeholderParams(kind, raw) {
116
128
  const headIsPair = head.includes('=');
117
129
  if (headIsPair) {
118
130
  const eq = head.indexOf('=');
119
- attrs.unshift({ name: head.slice(0, eq).trim(), value: head.slice(eq + 1).trim() });
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 });
120
140
  }
121
141
  switch (kind) {
142
+ case 'search':
143
+ if (!headIsPair && head !== '')
144
+ params.push({ name: 'query', value: head });
145
+ break;
122
146
  case 'jira':
123
147
  if (!headIsPair && head !== '')
124
148
  params.push({ name: 'key', value: head });
@@ -154,12 +178,20 @@ function directiveParams(name, rest) {
154
178
  if (!rest)
155
179
  return params;
156
180
  const free = [];
157
- const re = /([A-Za-z-]+)=("([^"]*)"|\S+)|(\S+)/g;
181
+ const re = /([A-Za-z_][\w-]*)=("(?:\\.|[^"\\])*"|\S+)|(\S+)/g;
158
182
  for (const m of rest.matchAll(re)) {
159
- if (m[1])
160
- params.push({ name: m[1], value: m[3] ?? m[2] });
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
+ }
161
193
  else
162
- free.push(m[4]);
194
+ free.push(m[3]);
163
195
  }
164
196
  if (free.length && name === 'expand')
165
197
  params.unshift({ name: 'title', value: free.join(' ') });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-md-sync",
3
- "version": "0.9.1",
3
+ "version": "0.10.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",