confluence-md-sync 0.9.1 → 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.
@@ -336,6 +336,7 @@ class Converter {
336
336
  info: 'INFO', note: 'NOTE', warning: 'WARNING', tip: 'TIP',
337
337
  };
338
338
  static NATIVE_PLACEHOLDER_NAME = {
339
+ search: 'search',
339
340
  toc: 'toc', children: 'children', jira: 'jira', status: 'status',
340
341
  anchor: 'anchor', detailssummary: 'properties-report',
341
342
  'portfolio-for-jira-plan': 'portfolio-for-jira-plan',
@@ -432,8 +433,7 @@ class Converter {
432
433
  const attrs = [];
433
434
  let head = '';
434
435
  for (const p of params) {
435
- if (!oneline(p.value) || p.value.includes('|'))
436
- return null;
436
+ const value = /[|{}\n\r]/.test(p.value) || /^"/.test(p.value) ? JSON.stringify(p.value) : p.value;
437
437
  if (name === 'jira' && p.name === 'key' && head === '') {
438
438
  head = p.value;
439
439
  continue;
@@ -449,7 +449,7 @@ class Converter {
449
449
  const attrName = name === 'jira' && p.name === 'jqlQuery' ? 'jql' : p.name;
450
450
  if (!/^[A-Za-z-]+$/.test(attrName))
451
451
  return null;
452
- attrs.push(`${attrName}=${p.value}`);
452
+ attrs.push(`${attrName}=${value}`);
453
453
  }
454
454
  const inner = [head, ...attrs].filter((s, i) => s !== '' || i > 0).join('|');
455
455
  candidate = inner === '' ? `{{${ph}}}` : `{{${ph}:${inner}}}`;
@@ -468,7 +468,9 @@ class Converter {
468
468
  for (const child of elements(el.children)) {
469
469
  if (child.name === 'ac:parameter') {
470
470
  const pname = getAttr(child, 'ac:name') ?? '';
471
- params.push({ name: pname, value: decodeEntities(textContent(child.children)) });
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)) });
472
474
  }
473
475
  else if (child.name === 'ac:rich-text-body')
474
476
  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.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",