confluence-md-sync 0.10.1 → 0.11.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.
@@ -1246,6 +1246,16 @@ class Converter {
1246
1246
  throw new Unrepresentable();
1247
1247
  const ref = kids[0];
1248
1248
  const filename = getAttr(ref, 'ri:filename') ?? '';
1249
+ const architecture = /^(.*\.dsl)\.((?:[0-9a-f]{2})+)\.png$/.exec(filename);
1250
+ if (architecture && ref.name === 'ri:attachment' && !/[{}|\n/\\]/.test(architecture[1])) {
1251
+ const view = new TextDecoder('utf-8').decode(Uint8Array.from(architecture[2].match(/../g), b => parseInt(b, 16)));
1252
+ if (view && !/[{}|\n\uFFFD]/.test(view)) {
1253
+ this.files.add(architecture[1]);
1254
+ this.files.add(architecture[1].replace(/\.dsl$/, '.json'));
1255
+ this.images.add(filename);
1256
+ return `{{structurizr:${architecture[1]}|view=${view}}}`;
1257
+ }
1258
+ }
1249
1259
  const stem = filename.replace(/\.png$/i, '');
1250
1260
  const drawioName = getAttr(el, 'ac:alt')?.match(/^drawio:(.+)$/)?.[1]
1251
1261
  ?? (stem !== filename && (this.drawioNames.has(stem) || /\.drawio$/i.test(stem)) ? stem : undefined);
@@ -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 { structurizrPlugin } from './plugins/structurizr.js';
9
10
  export { drawioPlugin } from './plugins/drawio.js';
10
11
  export { searchPlugin } from './plugins/search.js';
11
12
  import { MacroRegistry } from './registry.js';
@@ -51,5 +52,6 @@ export declare const macros: {
51
52
  csvTable: typeof csvTable;
52
53
  portfolioForJiraPlan: typeof portfolioForJiraPlan;
53
54
  drawio: (name: string, opts?: Record<string, string | undefined>) => import("../index.js").Markdown;
55
+ structurizr: (name: string, view: string) => import("../index.js").Markdown;
54
56
  search: (query: string, options?: Record<string, string | undefined>) => import("../index.js").Markdown;
55
57
  };
@@ -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 { structurizrPlugin } from './plugins/structurizr.js';
9
10
  export { drawioPlugin } from './plugins/drawio.js';
10
11
  export { searchPlugin } from './plugins/search.js';
11
12
  import { MacroRegistry } from './registry.js';
@@ -17,6 +18,7 @@ import { anchor, children, codeBlock, excerpt, excerptInclude, expand, includePa
17
18
  import { tableExcerpt, tableExcerptInclude, tableFilter, tableJoiner } from './plugins/table-filter.js';
18
19
  import { csvTable } from './plugins/csv-table.js';
19
20
  import { portfolioForJiraPlan } from './plugins/portfolio-for-jira-plan.js';
21
+ import { structurizr, structurizrPlugin } from './plugins/structurizr.js';
20
22
  import { drawio, drawioPlugin } from './plugins/drawio.js';
21
23
  import { search, searchPlugin } from './plugins/search.js';
22
24
  /** Creates a registry pre-loaded with all built-in plugins. */
@@ -27,6 +29,7 @@ export function createDefaultRegistry() {
27
29
  .use(csvTablePlugin)
28
30
  .use(portfolioForJiraPlanPlugin)
29
31
  .use(drawioPlugin)
32
+ .use(structurizrPlugin)
30
33
  .use(searchPlugin);
31
34
  }
32
35
  /**
@@ -67,5 +70,6 @@ export const macros = {
67
70
  csvTable,
68
71
  portfolioForJiraPlan,
69
72
  drawio,
73
+ structurizr,
70
74
  search,
71
75
  };
@@ -0,0 +1,5 @@
1
+ import { type MacroPlugin } from '../types.js';
2
+ export declare function structurizrPreview(name: string, view: string): string;
3
+ /** Confluence needs no third-party plugin: the view is a PNG, sources remain attachments. */
4
+ export declare const structurizrPlugin: MacroPlugin;
5
+ export declare const structurizr: (name: string, view: string) => import("../../index.js").Markdown;
@@ -0,0 +1,17 @@
1
+ import { macro } from '../builder.js';
2
+ import { paramMap } from '../types.js';
3
+ import { escapeXmlAttr } from '../xml.js';
4
+ export function structurizrPreview(name, view) {
5
+ if (typeof name !== 'string' || !/\.dsl$/i.test(name) || /[/\\{}|]/.test(name) || !view || view.length > 120 || /[{}|\n\r]/.test(view))
6
+ throw new Error('structurizr: a .dsl attachment and view key are required');
7
+ return `${name}.${Array.from(new TextEncoder().encode(view), b => b.toString(16).padStart(2, '0')).join('')}.png`;
8
+ }
9
+ /** Confluence needs no third-party plugin: the view is a PNG, sources remain attachments. */
10
+ export const structurizrPlugin = {
11
+ name: 'structurizr',
12
+ macros: [{ name: 'structurizr', render(ctx) {
13
+ const { name, view } = paramMap(ctx.params);
14
+ return `<ac:image ac:alt="${escapeXmlAttr(`Structurizr · ${view}`)}"><ri:attachment ri:filename="${escapeXmlAttr(structurizrPreview(name, view))}" /></ac:image>`;
15
+ } }],
16
+ };
17
+ export const structurizr = (name, view) => macro('structurizr').param('name', name).param('view', view).toMarkdown();
@@ -47,6 +47,7 @@ export const NATIVE_DIRECTIVES = {
47
47
  export const NATIVE_PLACEHOLDERS = {
48
48
  search: 'search',
49
49
  drawio: 'drawio',
50
+ structurizr: 'structurizr',
50
51
  toc: 'toc',
51
52
  children: 'children',
52
53
  jira: 'jira',
@@ -59,6 +60,7 @@ export const NATIVE_PLACEHOLDERS = {
59
60
  export function nativeMacroList() {
60
61
  return [
61
62
  { macro: 'search', syntax: '{{search:query=…|spacekey=…|type=page|maxLimit=10}}' },
63
+ { macro: 'structurizr', syntax: '{{structurizr:workspace.dsl|view=containers}}' },
62
64
  { macro: 'drawio', syntax: '{{drawio:diagram.drawio|format=macro}} | {{drawio:diagram.drawio|format=image}}' },
63
65
  { macro: 'info', syntax: '> [!INFO] Заголовок?' },
64
66
  { macro: 'note', syntax: '> [!NOTE] Заголовок?' },
@@ -79,7 +81,7 @@ export function nativeMacroList() {
79
81
  const ADMONITION_FIRST_RE = /^>\s*\[!([A-Za-z]+)\]\s*(.*)$/;
80
82
  const DIRECTIVE_OPEN_RE = /^:::\s+([a-z-]+)(?:\s+(.*?))?\s*$/;
81
83
  const DIRECTIVE_CLOSE_RE = /^:::\s*$/;
82
- const PLACEHOLDER_INLINE_RE = /\{\{(search|drawio|toc|children|jira|status|anchor|properties-report|portfolio-for-jira-plan)(?::((?:"(?:\\.|[^"\\])*"|[^{}"]|\{[^{])*?))?\}\}/g;
84
+ const PLACEHOLDER_INLINE_RE = /\{\{(search|drawio|structurizr|toc|children|jira|status|anchor|properties-report|portfolio-for-jira-plan)(?::((?:"(?:\\.|[^"\\])*"|[^{}"]|\{[^{])*?))?\}\}/g;
83
85
  const FENCE_RE = /^\s*(`{3,}|~{3,})/;
84
86
  /** Компактная пара маркеров без тела — для строчного контекста. */
85
87
  function inlineMarker(macroName, params) {
@@ -122,6 +124,8 @@ function placeholderParams(kind, raw) {
122
124
  if (raw === undefined || raw.trim() === '')
123
125
  return [];
124
126
  const { head, attrs } = splitAttrs(raw);
127
+ if (kind === 'structurizr')
128
+ return [{ name: 'name', value: head }, ...attrs];
125
129
  if (kind === 'drawio')
126
130
  return [{ name: 'diagramName', value: head }, ...attrs];
127
131
  const params = [];
@@ -55,6 +55,12 @@ export interface RenderStorageOptions {
55
55
  title: string;
56
56
  space?: string;
57
57
  } | null;
58
+ /** Local Markdown assets: attachment on this page, or a canonical repository URL. */
59
+ resourceResolver?: (href: string) => {
60
+ attachment: string;
61
+ } | {
62
+ url: string;
63
+ } | null;
58
64
  }
59
65
  export declare class MissingAttachmentUrlError extends Error {
60
66
  constructor(message: string);
@@ -120,22 +120,40 @@ export function renderToStorage(markdown, urls, opts = {}) {
120
120
  // манифеста). Только markdown-ссылки (`<a href>` из markdown-it); плейсхолдеры
121
121
  // {{file:}}/{{img:}} рендерятся ниже и сюда ещё не попали. Внешние URL/якоря
122
122
  // резолвер отсекает (возвращает null) — они остаются как есть.
123
- if (opts.linkResolver) {
124
- const resolve = opts.linkResolver;
125
- html = html.replace(/<a href="([^"]*)">([\s\S]*?)<\/a>/g, (full, href, inner) => {
123
+ if (opts.resourceResolver) {
124
+ html = html.replace(/<img\b[^>]*\bsrc="([^"]*)"[^>]*\/?>/g, (full, href) => {
126
125
  const decoded = unescapeHtml(href);
127
126
  if (/^(?:[a-z][a-z0-9+.-]*:|#|\/\/)/i.test(decoded))
128
- return full; // http(s):, mailto:, #anchor
129
- const target = resolve(decoded);
127
+ return full;
128
+ const target = opts.resourceResolver(decoded);
130
129
  if (!target)
131
130
  return full;
132
- const text = unescapeHtml(inner.replace(/<[^>]+>/g, '')).trim();
133
- const attrs = [];
134
- if (target.space)
135
- attrs.push(['space', target.space]);
136
- if (text)
137
- attrs.push(['text', text]);
138
- return renderPageLink(target.title, attrs);
131
+ if ('url' in target)
132
+ return full.replace(`src="${href}"`, `src="${escapeXmlAttr(target.url)}"`);
133
+ const alt = /\balt="([^"]*)"/.exec(full)?.[1];
134
+ return `<ac:image${alt ? ` ac:alt="${escapeXmlAttr(unescapeHtml(alt))}"` : ''}><ri:attachment ri:filename="${escapeXmlAttr(target.attachment)}" /></ac:image>`;
135
+ });
136
+ }
137
+ if (opts.linkResolver || opts.resourceResolver) {
138
+ html = html.replace(/<a href="([^"]*)"(?:\s+title="[^"]*")?>([\s\S]*?)<\/a>/g, (full, href, inner) => {
139
+ const decoded = unescapeHtml(href);
140
+ if (/^(?:[a-z][a-z0-9+.-]*:|#|\/\/)/i.test(decoded))
141
+ return full; // http(s):, mailto:, #anchor
142
+ const target = opts.linkResolver?.(decoded);
143
+ const body = /<[^>]+>/.test(inner)
144
+ ? `<ac:link-body>${inner}</ac:link-body>`
145
+ : plainTextLinkBody(unescapeHtml(inner));
146
+ if (target) {
147
+ const space = target.space ? ` ri:space-key="${escapeXmlAttr(target.space)}"` : '';
148
+ const anchor = decoded.includes('#') ? ` ac:anchor="${escapeXmlAttr(decoded.slice(decoded.indexOf('#') + 1))}"` : '';
149
+ return `<ac:link${anchor}><ri:page ri:content-title="${escapeXmlAttr(target.title)}"${space} />${body}</ac:link>`;
150
+ }
151
+ const resource = opts.resourceResolver?.(decoded);
152
+ if (!resource)
153
+ return full;
154
+ if ('url' in resource)
155
+ return `<a href="${escapeXmlAttr(resource.url)}">${inner}</a>`;
156
+ return `<ac:link><ri:attachment ri:filename="${escapeXmlAttr(resource.attachment)}" />${body}</ac:link>`;
139
157
  });
140
158
  }
141
159
  return html.replace(PLACEHOLDER_RE, (_full, kind, rawBody) => {
@@ -0,0 +1,5 @@
1
+ export declare function structurizrReferences(markdown: string): {
2
+ name: string;
3
+ json: string;
4
+ preview: string;
5
+ }[];
@@ -0,0 +1,16 @@
1
+ import MarkdownIt from 'markdown-it';
2
+ import { parsePlaceholder } from './render.js';
3
+ import { structurizrPreview } from '../macros/plugins/structurizr.js';
4
+ export function structurizrReferences(markdown) {
5
+ const refs = [];
6
+ for (const token of new MarkdownIt().parse(markdown, {}))
7
+ for (const child of token.children ?? []) {
8
+ if (child.type !== 'text')
9
+ continue;
10
+ for (const match of child.content.matchAll(/\{\{structurizr:([^}]+)\}\}/g)) {
11
+ const { name, attrs } = parsePlaceholder(match[1]);
12
+ refs.push({ name, json: name.replace(/\.dsl$/, '.json'), preview: structurizrPreview(name, Object.fromEntries(attrs).view) });
13
+ }
14
+ }
15
+ return refs;
16
+ }
@@ -1,5 +1,6 @@
1
1
  import { basename } from 'node:path';
2
2
  import { extractPlaceholders } from './render.js';
3
+ import { structurizrReferences } from './structurizr.js';
3
4
  import { drawioReferences } from './drawio.js';
4
5
  export class MarkdownValidationError extends Error {
5
6
  constructor(message) {
@@ -13,6 +14,10 @@ export function validateMarkdown(opts) {
13
14
  files.push(diagram.name);
14
15
  images.push(diagram.preview);
15
16
  }
17
+ for (const diagram of structurizrReferences(opts.markdown)) {
18
+ files.push(diagram.name, diagram.json);
19
+ images.push(diagram.preview);
20
+ }
16
21
  const tables = extractTablePlaceholders(opts.markdown);
17
22
  const providedImages = new Set(opts.imagePaths.map((p) => basename(p)));
18
23
  const providedFiles = new Set(opts.filePaths.map((p) => basename(p)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "confluence-md-sync",
3
- "version": "0.10.1",
3
+ "version": "0.11.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",