@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.
- package/cli.js +172 -8
- package/index.d.ts +34 -2
- package/index.js +163 -6
- 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
|
-
|
|
323
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = "<";
|
|
|
314
314
|
var GT = ">";
|
|
315
315
|
var QUOT = """;
|
|
316
316
|
var APOS = "'";
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
30
|
+
"@repo-toolkit/publish-package": "0.9.0"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"**/*",
|