@repo-toolkit/confluence 0.8.0 → 0.9.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.
Files changed (4) hide show
  1. package/cli.js +172 -8
  2. package/index.d.ts +34 -2
  3. package/index.js +163 -6
  4. package/package.json +2 -2
package/cli.js CHANGED
@@ -319,9 +319,31 @@ var LT = "<";
319
319
  var GT = ">";
320
320
  var QUOT = """;
321
321
  var APOS = "'";
322
- function markdownToStorage(markdown) {
323
- const lines = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
322
+ var MERMAID_PLACEHOLDER_PREFIX = '<ac:structured-macro ac:name="mermaid-placeholder" data-mermaid-id="';
323
+ var MERMAID_PLACEHOLDER_RE_STRICT = /<ac:structured-macro ac:name="mermaid-placeholder" data-mermaid-id="([^"]+)"><\/ac:structured-macro>/g;
324
+ function mermaidPlaceholderRe() {
325
+ return new RegExp(MERMAID_PLACEHOLDER_RE_STRICT.source, "g");
326
+ }
327
+ function renderMermaidPlaceholder(id) {
328
+ return `${MERMAID_PLACEHOLDER_PREFIX}${escapeXmlAttribute(id)}"></ac:structured-macro>`;
329
+ }
330
+ function markdownToStorage(markdown, options = {}) {
331
+ const renderHtmlBlocks = options.renderHtmlBlocks === true;
332
+ let lines = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
333
+ if (lines.length > 0 && lines[0] === "---") {
334
+ let closeIndex = -1;
335
+ for (let j = 1; j < lines.length; j += 1) {
336
+ if (lines[j] === "---" || lines[j] === "...") {
337
+ closeIndex = j;
338
+ break;
339
+ }
340
+ }
341
+ if (closeIndex !== -1) {
342
+ lines = lines.slice(closeIndex + 1);
343
+ }
344
+ }
324
345
  const out = [];
346
+ const mermaidBlocks = [];
325
347
  let i = 0;
326
348
  while (i < lines.length) {
327
349
  const line = lines[i];
@@ -344,7 +366,16 @@ function markdownToStorage(markdown) {
344
366
  i += 1;
345
367
  }
346
368
  i += 1;
347
- out.push(renderCodeBlock(buf.join("\n"), lang));
369
+ const code = buf.join("\n");
370
+ if (lang === "mermaid") {
371
+ const id = `mermaid-${mermaidBlocks.length + 1}`;
372
+ mermaidBlocks.push({ id, source: code });
373
+ out.push(renderMermaidPlaceholder(id));
374
+ } else if (lang === "html" && renderHtmlBlocks) {
375
+ out.push(renderHtmlBlock(code));
376
+ } else {
377
+ out.push(renderCodeBlock(code, lang));
378
+ }
348
379
  continue;
349
380
  }
350
381
  if (/^\s{0,3}(?:-|\*|\+)\s+/.test(line) || /^\s{0,3}\d+\.\s+/.test(line)) {
@@ -381,7 +412,7 @@ function markdownToStorage(markdown) {
381
412
  const renderedPara = renderInline(para.join(LINE_BREAK_SENTINEL));
382
413
  out.push(`<p>${renderedPara.split(LINE_BREAK_SENTINEL).join(STORAGE_LINE_BREAK)}</p>`);
383
414
  }
384
- return { html: out.join("\n") };
415
+ return { html: out.join("\n"), mermaidBlocks };
385
416
  }
386
417
  function isLikelyListTerminator(lines, currentIndex) {
387
418
  for (let k = currentIndex + 1; k < lines.length; k += 1) {
@@ -408,6 +439,9 @@ function renderCodeBlock(code, _lang) {
408
439
  const titleAttr = escapeXmlAttribute(lang);
409
440
  return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${titleAttr}</ac:parameter><ac:plain-text-body><![CDATA[${escapeCdataTerminator(code)}]]></ac:plain-text-body></ac:structured-macro>`;
410
441
  }
442
+ function renderHtmlBlock(code) {
443
+ return `<ac:structured-macro ac:name="html"><ac:plain-text-body><![CDATA[${escapeCdataTerminator(code)}]]></ac:plain-text-body></ac:structured-macro>`;
444
+ }
411
445
  function escapeCdataTerminator(text) {
412
446
  return text.replace(/]]>/g, "]]]]><![CDATA[>");
413
447
  }
@@ -614,6 +648,115 @@ function decodePlaceholder(value) {
614
648
  return value.replace(new RegExp(AMP2, "g"), "&").replace(new RegExp(QUOT2, "g"), '"').replace(new RegExp(LT2, "g"), "<").replace(new RegExp(GT2, "g"), ">");
615
649
  }
616
650
 
651
+ // src/mermaid.ts
652
+ import { spawn, spawnSync } from "child_process";
653
+ import { mkdtemp, rm, writeFile } from "fs/promises";
654
+ import { tmpdir } from "os";
655
+ import { join as join2 } from "path";
656
+ async function rewriteMermaidBlocks(html, blocks, pageId, client, options = {}) {
657
+ const fallbacks = [];
658
+ const uploaded = [];
659
+ if (blocks.length === 0) {
660
+ return { html, fallbacks, uploaded };
661
+ }
662
+ const existing = await client.getAttachments(pageId);
663
+ const existingByName = /* @__PURE__ */ new Map();
664
+ for (const a of existing) {
665
+ const name = a.filename ?? a.title;
666
+ if (name) {
667
+ existingByName.set(name, a);
668
+ }
669
+ }
670
+ const renderHook = options.renderHook ?? defaultRenderHook;
671
+ let mmdcAvailable;
672
+ if (options.renderHook) {
673
+ mmdcAvailable = true;
674
+ } else if (options.available !== void 0) {
675
+ mmdcAvailable = options.available;
676
+ } else {
677
+ mmdcAvailable = await isMmdcAvailable(options.mmdcPath);
678
+ }
679
+ const placeholderToAttachment = /* @__PURE__ */ new Map();
680
+ for (const block of blocks) {
681
+ if (!mmdcAvailable) {
682
+ fallbacks.push(block.id);
683
+ continue;
684
+ }
685
+ const workDir = await mkdtemp(join2(tmpdir(), "rt-mermaid-"));
686
+ try {
687
+ const inPath = join2(workDir, "diagram.mmd");
688
+ const outPath = join2(workDir, "diagram.svg");
689
+ await writeFile(inPath, block.source, "utf8");
690
+ await renderHook(block.source, outPath);
691
+ const filename = escapeAttachmentFilename(`${block.id}.svg`);
692
+ let attachment = existingByName.get(filename);
693
+ if (attachment && attachment.id) {
694
+ attachment = await client.updateAttachmentData(
695
+ pageId,
696
+ attachment.id,
697
+ outPath,
698
+ `Updated via repo-toolkit-confluence`
699
+ );
700
+ } else {
701
+ attachment = await client.uploadAttachment(pageId, outPath, `Uploaded via repo-toolkit-confluence`);
702
+ }
703
+ uploaded.push({ id: block.id, attachment });
704
+ placeholderToAttachment.set(block.id, filename);
705
+ } catch {
706
+ fallbacks.push(block.id);
707
+ } finally {
708
+ await rm(workDir, { recursive: true, force: true }).catch(() => {
709
+ });
710
+ }
711
+ }
712
+ const remainingIds = new Set(fallbacks);
713
+ const re = mermaidPlaceholderRe();
714
+ const replaced = html.replace(re, (full, id) => {
715
+ const filename = placeholderToAttachment.get(id);
716
+ if (filename) {
717
+ return renderAttachmentMacro2(filename);
718
+ }
719
+ if (remainingIds.has(id)) {
720
+ const block = blocks.find((b) => b.id === id);
721
+ if (block) {
722
+ return renderCodeBlock(block.source, "mermaid");
723
+ }
724
+ }
725
+ return full;
726
+ });
727
+ return { html: replaced, fallbacks, uploaded };
728
+ }
729
+ function renderAttachmentMacro2(filename) {
730
+ const safe = escapeAttachmentFilename(filename);
731
+ return `<ac:image><ri:attachment ri:filename="${escapeXmlAttribute(safe)}" /></ac:image>`;
732
+ }
733
+ async function isMmdcAvailable(override) {
734
+ if (override) {
735
+ return true;
736
+ }
737
+ return spawnSync("sh", ["-c", "command -v mmdc"], { stdio: "ignore" }).status === 0;
738
+ }
739
+ async function defaultRenderHook(source, outFile) {
740
+ return new Promise((resolve3, reject) => {
741
+ const child = spawn("mmdc", ["-i", "-", "-o", outFile, "-t", "default", "-b", "transparent"], {
742
+ stdio: ["pipe", "pipe", "pipe"]
743
+ });
744
+ let stderr = "";
745
+ child.stderr?.on("data", (chunk) => {
746
+ stderr += chunk.toString();
747
+ });
748
+ child.on("error", (err) => reject(err));
749
+ child.on("close", (code) => {
750
+ if (code === 0) {
751
+ resolve3();
752
+ } else {
753
+ reject(new Error(`mmdc exited with code ${code}${stderr ? `: ${stderr}` : ""}`));
754
+ }
755
+ });
756
+ child.stdin?.end(source);
757
+ });
758
+ }
759
+
617
760
  // src/index.ts
618
761
  function resolveConfluenceSyncPlan(options = {}) {
619
762
  const cwd = resolve2(options.cwd ?? process.cwd());
@@ -651,7 +794,8 @@ function resolveConfluenceSyncPlan(options = {}) {
651
794
  parentPageId: options.parentPageId ?? "",
652
795
  versionMessage: options.versionMessage ?? "Synced via repo-toolkit-confluence",
653
796
  skipUnchanged: options.skipUnchanged ?? true,
654
- dryRun: options.dryRun ?? false
797
+ dryRun: options.dryRun ?? false,
798
+ renderHtmlBlocks: options.renderHtmlBlocks === true
655
799
  };
656
800
  }
657
801
  async function syncConfluenceToDocs(options = {}) {
@@ -696,9 +840,20 @@ async function syncEntry(entry, plan, client, cache, log) {
696
840
  const page2 = await cache.findOrCreate(title, currentParentId);
697
841
  const pageId = page2.id;
698
842
  const markdown = readFileSync2(entry.absolute, "utf8");
699
- const { html } = markdownToStorage(markdown);
843
+ const { html, mermaidBlocks } = markdownToStorage(markdown, {
844
+ renderHtmlBlocks: plan.renderHtmlBlocks
845
+ });
700
846
  const markdownDir = dirname(entry.absolute);
701
847
  let body = html;
848
+ if (mermaidBlocks.length > 0) {
849
+ const mermaidResult = await rewriteMermaidBlocks(body, mermaidBlocks, pageId, client);
850
+ body = mermaidResult.html;
851
+ if (mermaidResult.fallbacks.length > 0) {
852
+ log(
853
+ `mermaid: ${mermaidResult.fallbacks.length} block(s) not rendered (mmdc unavailable or failed); emitted as code macros`
854
+ );
855
+ }
856
+ }
702
857
  LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
703
858
  if (LOCAL_IMAGE_PLACEHOLDER_RE.test(body)) {
704
859
  const result = await rewriteImagesToAttachments(body, pageId, client, { markdownDir });
@@ -776,6 +931,7 @@ var SPECS = [
776
931
  { name: "version-message" },
777
932
  { name: "skip-unchanged", boolean: true, negatable: true },
778
933
  { name: "dry-run", boolean: true },
934
+ { name: "render-html-blocks", boolean: true },
779
935
  INTERACTIVE_FLAG
780
936
  ];
781
937
  function printHelp() {
@@ -802,6 +958,8 @@ Options:
802
958
  --skip-unchanged Skip pages whose body is unchanged (default: true)
803
959
  --no-skip-unchanged Re-upload every page even when unchanged
804
960
  --dry-run Walk the doc tree and print the plan without API calls
961
+ --render-html-blocks Render \`\`\`html fenced blocks as inline HTML via the
962
+ Confluence html macro (default: false; emits as code box)
805
963
  -i, --interactive (reserved; not currently interactive)
806
964
  -h, --help Show this help message
807
965
  `);
@@ -814,7 +972,8 @@ var ENV_INPUT_MAP = [
814
972
  ["INPUT_CONFLUENCE-BASE-URL", "baseUrl"],
815
973
  ["INPUT_SPACE-KEY", "spaceKey"],
816
974
  ["INPUT_PARENT-PAGE-ID", "parentPageId"],
817
- ["INPUT_VERSION-MESSAGE", "versionMessage"]
975
+ ["INPUT_VERSION-MESSAGE", "versionMessage"],
976
+ ["INPUT_RENDER-HTML-BLOCKS", "renderHtmlBlocks"]
818
977
  ];
819
978
  function buildOptions(result) {
820
979
  if (!result) {
@@ -835,6 +994,7 @@ function buildOptions(result) {
835
994
  if (values["version-message"]) options.versionMessage = values["version-message"];
836
995
  if (values["skip-unchanged"] !== void 0) options.skipUnchanged = values["skip-unchanged"] === "true";
837
996
  if (values["dry-run"] !== void 0) options.dryRun = true;
997
+ if (values["render-html-blocks"] !== void 0) options.renderHtmlBlocks = true;
838
998
  return options;
839
999
  }
840
1000
  function optionsFromEnv() {
@@ -842,7 +1002,11 @@ function optionsFromEnv() {
842
1002
  for (const [envName, key] of ENV_INPUT_MAP) {
843
1003
  const value = process.env[envName];
844
1004
  if (typeof value === "string" && value.length > 0) {
845
- options[key] = value;
1005
+ if (key === "renderHtmlBlocks") {
1006
+ options[key] = value === "true" || value === "1";
1007
+ } else {
1008
+ options[key] = value;
1009
+ }
846
1010
  }
847
1011
  }
848
1012
  return options;
package/index.d.ts CHANGED
@@ -111,10 +111,20 @@ declare function readDocTree(root: string, depth?: number): Promise<DocTree>;
111
111
  declare function isMarkdownName(name: string): boolean;
112
112
  declare function titleFromSegment(segment: string): string;
113
113
 
114
+ interface MermaidBlock {
115
+ id: string;
116
+ source: string;
117
+ }
114
118
  interface MarkdownConvertResult {
115
119
  html: string;
120
+ mermaidBlocks: MermaidBlock[];
121
+ }
122
+ interface MarkdownConvertOptions {
123
+ /** Render ```html fenced blocks as inline HTML via the Confluence `html` macro instead of a code box. Default: false. */
124
+ renderHtmlBlocks?: boolean;
116
125
  }
117
- declare function markdownToStorage(markdown: string): MarkdownConvertResult;
126
+ declare function markdownToStorage(markdown: string, options?: MarkdownConvertOptions): MarkdownConvertResult;
127
+ declare function renderHtmlBlock(code: string): string;
118
128
  declare function renderInline(text: string): string;
119
129
  declare const LOCAL_IMAGE_PLACEHOLDER_RE: RegExp;
120
130
  declare function isRemoteUrl(src: string): boolean;
@@ -134,6 +144,25 @@ interface RewriteOptions {
134
144
  }
135
145
  declare function rewriteImagesToAttachments(html: string, pageId: string, client: ConfluenceClient, options: RewriteOptions): Promise<RewriteResult>;
136
146
 
147
+ interface MermaidRewriteResult {
148
+ html: string;
149
+ /** Placeholders that could not be rendered (mmdc missing or render failure). Fallback source is in the original code macro. */
150
+ fallbacks: string[];
151
+ uploaded: ReadonlyArray<{
152
+ id: string;
153
+ attachment: Attachment;
154
+ }>;
155
+ }
156
+ interface MermaidRewriteOptions {
157
+ /** Override the mmdc binary path; otherwise discovered via PATH. */
158
+ mmdcPath?: string;
159
+ /** Override the render command for tests (must write a valid SVG at outFile). */
160
+ renderHook?: (source: string, outFile: string) => Promise<void>;
161
+ /** Force-enable or force-disable rendering regardless of PATH detection. When true, skips the mmdc probe. */
162
+ available?: boolean;
163
+ }
164
+ declare function rewriteMermaidBlocks(html: string, blocks: MermaidBlock[], pageId: string, client: ConfluenceClient, options?: MermaidRewriteOptions): Promise<MermaidRewriteResult>;
165
+
137
166
  declare const INTERACTIVE_FLAG: FlagSpec;
138
167
  interface ConfluenceSyncOptions {
139
168
  /** Documentation root folder (relative to `cwd`, or absolute). Required. */
@@ -154,6 +183,8 @@ interface ConfluenceSyncOptions {
154
183
  versionMessage?: string;
155
184
  /** Skip uploads that would have no markdown changes (default: true). */
156
185
  skipUnchanged?: boolean;
186
+ /** Render ```html fenced blocks as inline HTML via the Confluence `html` macro instead of a code box (default: false). */
187
+ renderHtmlBlocks?: boolean;
157
188
  /** Dry-run: walk the tree and print the plan but make no API calls. */
158
189
  dryRun?: boolean;
159
190
  /** Custom Confluence client instance (testing). When supplied, `username`/`apiToken`/`baseUrl` are ignored. */
@@ -172,8 +203,9 @@ interface ConfluenceSyncPlan {
172
203
  versionMessage: string;
173
204
  skipUnchanged: boolean;
174
205
  dryRun: boolean;
206
+ renderHtmlBlocks: boolean;
175
207
  }
176
208
  declare function resolveConfluenceSyncPlan(options?: ConfluenceSyncOptions): ConfluenceSyncPlan;
177
209
  declare function syncConfluenceToDocs(options?: ConfluenceSyncOptions): Promise<void>;
178
210
 
179
- export { type Attachment, ConfluenceApiError, ConfluenceClient, type ConfluenceClientOptions, type ConfluenceSyncOptions, type ConfluenceSyncPlan, type DocEntry, type DocTree, INTERACTIVE_FLAG, LOCAL_IMAGE_PLACEHOLDER_RE, type Page, type PageBody, type PageVersion, escapeAttachmentFilename, escapeXmlAttribute, isMarkdownName, isRemoteUrl, markdownToStorage, readDocTree, renderInline, resolveConfluenceSyncPlan, resolveConfluenceSyncPlan as resolveSyncPlan, rewriteImagesToAttachments, syncConfluenceToDocs, titleFromSegment };
211
+ export { type Attachment, ConfluenceApiError, ConfluenceClient, type ConfluenceClientOptions, type ConfluenceSyncOptions, type ConfluenceSyncPlan, type DocEntry, type DocTree, INTERACTIVE_FLAG, LOCAL_IMAGE_PLACEHOLDER_RE, type MarkdownConvertOptions, type MarkdownConvertResult, type MermaidBlock, type MermaidRewriteOptions, type MermaidRewriteResult, type Page, type PageBody, type PageVersion, escapeAttachmentFilename, escapeXmlAttribute, isMarkdownName, isRemoteUrl, markdownToStorage, readDocTree, renderHtmlBlock, renderInline, resolveConfluenceSyncPlan, resolveConfluenceSyncPlan as resolveSyncPlan, rewriteImagesToAttachments, rewriteMermaidBlocks, syncConfluenceToDocs, titleFromSegment };
package/index.js CHANGED
@@ -314,9 +314,31 @@ var LT = "&lt;";
314
314
  var GT = "&gt;";
315
315
  var QUOT = "&quot;";
316
316
  var APOS = "&#39;";
317
- function markdownToStorage(markdown) {
318
- const lines = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
317
+ var MERMAID_PLACEHOLDER_PREFIX = '<ac:structured-macro ac:name="mermaid-placeholder" data-mermaid-id="';
318
+ var MERMAID_PLACEHOLDER_RE_STRICT = /<ac:structured-macro ac:name="mermaid-placeholder" data-mermaid-id="([^"]+)"><\/ac:structured-macro>/g;
319
+ function mermaidPlaceholderRe() {
320
+ return new RegExp(MERMAID_PLACEHOLDER_RE_STRICT.source, "g");
321
+ }
322
+ function renderMermaidPlaceholder(id) {
323
+ return `${MERMAID_PLACEHOLDER_PREFIX}${escapeXmlAttribute(id)}"></ac:structured-macro>`;
324
+ }
325
+ function markdownToStorage(markdown, options = {}) {
326
+ const renderHtmlBlocks = options.renderHtmlBlocks === true;
327
+ let lines = markdown.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
328
+ if (lines.length > 0 && lines[0] === "---") {
329
+ let closeIndex = -1;
330
+ for (let j = 1; j < lines.length; j += 1) {
331
+ if (lines[j] === "---" || lines[j] === "...") {
332
+ closeIndex = j;
333
+ break;
334
+ }
335
+ }
336
+ if (closeIndex !== -1) {
337
+ lines = lines.slice(closeIndex + 1);
338
+ }
339
+ }
319
340
  const out = [];
341
+ const mermaidBlocks = [];
320
342
  let i = 0;
321
343
  while (i < lines.length) {
322
344
  const line = lines[i];
@@ -339,7 +361,16 @@ function markdownToStorage(markdown) {
339
361
  i += 1;
340
362
  }
341
363
  i += 1;
342
- out.push(renderCodeBlock(buf.join("\n"), lang));
364
+ const code = buf.join("\n");
365
+ if (lang === "mermaid") {
366
+ const id = `mermaid-${mermaidBlocks.length + 1}`;
367
+ mermaidBlocks.push({ id, source: code });
368
+ out.push(renderMermaidPlaceholder(id));
369
+ } else if (lang === "html" && renderHtmlBlocks) {
370
+ out.push(renderHtmlBlock(code));
371
+ } else {
372
+ out.push(renderCodeBlock(code, lang));
373
+ }
343
374
  continue;
344
375
  }
345
376
  if (/^\s{0,3}(?:-|\*|\+)\s+/.test(line) || /^\s{0,3}\d+\.\s+/.test(line)) {
@@ -376,7 +407,7 @@ function markdownToStorage(markdown) {
376
407
  const renderedPara = renderInline(para.join(LINE_BREAK_SENTINEL));
377
408
  out.push(`<p>${renderedPara.split(LINE_BREAK_SENTINEL).join(STORAGE_LINE_BREAK)}</p>`);
378
409
  }
379
- return { html: out.join("\n") };
410
+ return { html: out.join("\n"), mermaidBlocks };
380
411
  }
381
412
  function isLikelyListTerminator(lines, currentIndex) {
382
413
  for (let k = currentIndex + 1; k < lines.length; k += 1) {
@@ -403,6 +434,9 @@ function renderCodeBlock(code, _lang) {
403
434
  const titleAttr = escapeXmlAttribute(lang);
404
435
  return `<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">${titleAttr}</ac:parameter><ac:plain-text-body><![CDATA[${escapeCdataTerminator(code)}]]></ac:plain-text-body></ac:structured-macro>`;
405
436
  }
437
+ function renderHtmlBlock(code) {
438
+ return `<ac:structured-macro ac:name="html"><ac:plain-text-body><![CDATA[${escapeCdataTerminator(code)}]]></ac:plain-text-body></ac:structured-macro>`;
439
+ }
406
440
  function escapeCdataTerminator(text) {
407
441
  return text.replace(/]]>/g, "]]]]><![CDATA[>");
408
442
  }
@@ -609,6 +643,115 @@ function decodePlaceholder(value) {
609
643
  return value.replace(new RegExp(AMP2, "g"), "&").replace(new RegExp(QUOT2, "g"), '"').replace(new RegExp(LT2, "g"), "<").replace(new RegExp(GT2, "g"), ">");
610
644
  }
611
645
 
646
+ // src/mermaid.ts
647
+ import { spawn, spawnSync } from "child_process";
648
+ import { mkdtemp, rm, writeFile } from "fs/promises";
649
+ import { tmpdir } from "os";
650
+ import { join as join2 } from "path";
651
+ async function rewriteMermaidBlocks(html, blocks, pageId, client, options = {}) {
652
+ const fallbacks = [];
653
+ const uploaded = [];
654
+ if (blocks.length === 0) {
655
+ return { html, fallbacks, uploaded };
656
+ }
657
+ const existing = await client.getAttachments(pageId);
658
+ const existingByName = /* @__PURE__ */ new Map();
659
+ for (const a of existing) {
660
+ const name = a.filename ?? a.title;
661
+ if (name) {
662
+ existingByName.set(name, a);
663
+ }
664
+ }
665
+ const renderHook = options.renderHook ?? defaultRenderHook;
666
+ let mmdcAvailable;
667
+ if (options.renderHook) {
668
+ mmdcAvailable = true;
669
+ } else if (options.available !== void 0) {
670
+ mmdcAvailable = options.available;
671
+ } else {
672
+ mmdcAvailable = await isMmdcAvailable(options.mmdcPath);
673
+ }
674
+ const placeholderToAttachment = /* @__PURE__ */ new Map();
675
+ for (const block of blocks) {
676
+ if (!mmdcAvailable) {
677
+ fallbacks.push(block.id);
678
+ continue;
679
+ }
680
+ const workDir = await mkdtemp(join2(tmpdir(), "rt-mermaid-"));
681
+ try {
682
+ const inPath = join2(workDir, "diagram.mmd");
683
+ const outPath = join2(workDir, "diagram.svg");
684
+ await writeFile(inPath, block.source, "utf8");
685
+ await renderHook(block.source, outPath);
686
+ const filename = escapeAttachmentFilename(`${block.id}.svg`);
687
+ let attachment = existingByName.get(filename);
688
+ if (attachment && attachment.id) {
689
+ attachment = await client.updateAttachmentData(
690
+ pageId,
691
+ attachment.id,
692
+ outPath,
693
+ `Updated via repo-toolkit-confluence`
694
+ );
695
+ } else {
696
+ attachment = await client.uploadAttachment(pageId, outPath, `Uploaded via repo-toolkit-confluence`);
697
+ }
698
+ uploaded.push({ id: block.id, attachment });
699
+ placeholderToAttachment.set(block.id, filename);
700
+ } catch {
701
+ fallbacks.push(block.id);
702
+ } finally {
703
+ await rm(workDir, { recursive: true, force: true }).catch(() => {
704
+ });
705
+ }
706
+ }
707
+ const remainingIds = new Set(fallbacks);
708
+ const re = mermaidPlaceholderRe();
709
+ const replaced = html.replace(re, (full, id) => {
710
+ const filename = placeholderToAttachment.get(id);
711
+ if (filename) {
712
+ return renderAttachmentMacro2(filename);
713
+ }
714
+ if (remainingIds.has(id)) {
715
+ const block = blocks.find((b) => b.id === id);
716
+ if (block) {
717
+ return renderCodeBlock(block.source, "mermaid");
718
+ }
719
+ }
720
+ return full;
721
+ });
722
+ return { html: replaced, fallbacks, uploaded };
723
+ }
724
+ function renderAttachmentMacro2(filename) {
725
+ const safe = escapeAttachmentFilename(filename);
726
+ return `<ac:image><ri:attachment ri:filename="${escapeXmlAttribute(safe)}" /></ac:image>`;
727
+ }
728
+ async function isMmdcAvailable(override) {
729
+ if (override) {
730
+ return true;
731
+ }
732
+ return spawnSync("sh", ["-c", "command -v mmdc"], { stdio: "ignore" }).status === 0;
733
+ }
734
+ async function defaultRenderHook(source, outFile) {
735
+ return new Promise((resolve3, reject) => {
736
+ const child = spawn("mmdc", ["-i", "-", "-o", outFile, "-t", "default", "-b", "transparent"], {
737
+ stdio: ["pipe", "pipe", "pipe"]
738
+ });
739
+ let stderr = "";
740
+ child.stderr?.on("data", (chunk) => {
741
+ stderr += chunk.toString();
742
+ });
743
+ child.on("error", (err) => reject(err));
744
+ child.on("close", (code) => {
745
+ if (code === 0) {
746
+ resolve3();
747
+ } else {
748
+ reject(new Error(`mmdc exited with code ${code}${stderr ? `: ${stderr}` : ""}`));
749
+ }
750
+ });
751
+ child.stdin?.end(source);
752
+ });
753
+ }
754
+
612
755
  // src/index.ts
613
756
  var INTERACTIVE_FLAG = { name: "interactive", aliases: ["i"], boolean: true };
614
757
  function resolveConfluenceSyncPlan(options = {}) {
@@ -647,7 +790,8 @@ function resolveConfluenceSyncPlan(options = {}) {
647
790
  parentPageId: options.parentPageId ?? "",
648
791
  versionMessage: options.versionMessage ?? "Synced via repo-toolkit-confluence",
649
792
  skipUnchanged: options.skipUnchanged ?? true,
650
- dryRun: options.dryRun ?? false
793
+ dryRun: options.dryRun ?? false,
794
+ renderHtmlBlocks: options.renderHtmlBlocks === true
651
795
  };
652
796
  }
653
797
  async function syncConfluenceToDocs(options = {}) {
@@ -692,9 +836,20 @@ async function syncEntry(entry, plan, client, cache, log) {
692
836
  const page2 = await cache.findOrCreate(title, currentParentId);
693
837
  const pageId = page2.id;
694
838
  const markdown = readFileSync2(entry.absolute, "utf8");
695
- const { html } = markdownToStorage(markdown);
839
+ const { html, mermaidBlocks } = markdownToStorage(markdown, {
840
+ renderHtmlBlocks: plan.renderHtmlBlocks
841
+ });
696
842
  const markdownDir = dirname(entry.absolute);
697
843
  let body = html;
844
+ if (mermaidBlocks.length > 0) {
845
+ const mermaidResult = await rewriteMermaidBlocks(body, mermaidBlocks, pageId, client);
846
+ body = mermaidResult.html;
847
+ if (mermaidResult.fallbacks.length > 0) {
848
+ log(
849
+ `mermaid: ${mermaidResult.fallbacks.length} block(s) not rendered (mmdc unavailable or failed); emitted as code macros`
850
+ );
851
+ }
852
+ }
698
853
  LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
699
854
  if (LOCAL_IMAGE_PLACEHOLDER_RE.test(body)) {
700
855
  const result = await rewriteImagesToAttachments(body, pageId, client, { markdownDir });
@@ -771,11 +926,13 @@ export {
771
926
  markdownToStorage,
772
927
  parseFlags,
773
928
  readDocTree,
929
+ renderHtmlBlock,
774
930
  renderInline,
775
931
  resolveCliOptions,
776
932
  resolveConfluenceSyncPlan,
777
933
  resolveConfluenceSyncPlan as resolveSyncPlan,
778
934
  rewriteImagesToAttachments,
935
+ rewriteMermaidBlocks,
779
936
  syncConfluenceToDocs,
780
937
  titleFromSegment
781
938
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@repo-toolkit/confluence",
3
3
  "description": "Sync a folder of markdown docs to Confluence pages and attachments (GitHub Action compatible)",
4
- "version": "0.8.0",
4
+ "version": "0.9.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "keywords": [
@@ -27,7 +27,7 @@
27
27
  "node": ">=20"
28
28
  },
29
29
  "dependencies": {
30
- "@repo-toolkit/publish-package": "0.8.0"
30
+ "@repo-toolkit/publish-package": "0.9.0"
31
31
  },
32
32
  "files": [
33
33
  "**/*",