@repo-toolkit/confluence 0.14.1 → 0.15.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/README.md +4 -0
- package/cli.js +120 -10
- package/index.d.ts +3 -0
- package/index.js +98 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -72,6 +72,9 @@ Flags:
|
|
|
72
72
|
- `--space-key <key>` — Confluence space key (required; resolved to a `spaceId`)
|
|
73
73
|
- `--parent-page-id <id>` — numeric Confluence page id (required)
|
|
74
74
|
- `--version-message <text>` — version-message suffix appended to every PUT
|
|
75
|
+
- `--repository-url <url>` — repository URL appended to synced pages as an
|
|
76
|
+
italic source notice. When omitted, GitHub Actions runs infer this from
|
|
77
|
+
`GITHUB_SERVER_URL` and `GITHUB_REPOSITORY`.
|
|
75
78
|
- `--skip-unchanged` / `--no-skip-unchanged` — skip pages whose body is unchanged (default: `skip`)
|
|
76
79
|
- `--dry-run` — walk the doc tree and validate every markdown file and local
|
|
77
80
|
image source (same preflight as a real sync) then log the plan. No API
|
|
@@ -99,6 +102,7 @@ both read for every option. Boolean env values accept `true|1|yes|on` /
|
|
|
99
102
|
| spaceKey | `CONFLUENCE_SPACE_KEY` | `INPUT_SPACE-KEY` |
|
|
100
103
|
| parentPageId | `CONFLUENCE_PARENT_PAGE_ID` | `INPUT_PARENT-PAGE-ID` |
|
|
101
104
|
| versionMessage | `CONFLUENCE_VERSION_MESSAGE` | `INPUT_VERSION-MESSAGE` |
|
|
105
|
+
| repositoryUrl | `CONFLUENCE_REPOSITORY_URL` | `INPUT_REPOSITORY-URL` |
|
|
102
106
|
| skipUnchanged (bool) | `CONFLUENCE_SKIP_UNCHANGED` | `INPUT_SKIP-UNCHANGED` |
|
|
103
107
|
| dryRun (bool) | `CONFLUENCE_DRY_RUN` | `INPUT_DRY-RUN` |
|
|
104
108
|
| renderHtmlBlocks (bool) | `CONFLUENCE_RENDER_HTML_BLOCKS` | `INPUT_RENDER-HTML-BLOCKS` |
|
package/cli.js
CHANGED
|
@@ -282,23 +282,24 @@ Content-Type: application/octet-stream\r
|
|
|
282
282
|
if (response.status >= 300 && response.status < 400) {
|
|
283
283
|
throw new ConfluenceApiError("Redirect responses are not allowed", response.status, endpoint, "");
|
|
284
284
|
}
|
|
285
|
-
const text = await readBodyBounded(response, MAX_ERROR_BODY_LENGTH);
|
|
286
285
|
if (!response.ok) {
|
|
286
|
+
const text2 = await readBodyBounded(response, MAX_ERROR_BODY_LENGTH);
|
|
287
287
|
if (isSafe && shouldRetryStatus(response.status) && attempt < maxRetries) {
|
|
288
288
|
attempt += 1;
|
|
289
|
-
lastError = new ConfluenceApiError(describeStatus(response.status), response.status, endpoint,
|
|
289
|
+
lastError = new ConfluenceApiError(describeStatus(response.status), response.status, endpoint, text2);
|
|
290
290
|
await sleepBackoff(response, endpoint);
|
|
291
291
|
continue;
|
|
292
292
|
}
|
|
293
|
-
throw new ConfluenceApiError(describeStatus(response.status), response.status, endpoint,
|
|
293
|
+
throw new ConfluenceApiError(describeStatus(response.status), response.status, endpoint, text2);
|
|
294
294
|
}
|
|
295
|
+
const text = await response.text();
|
|
295
296
|
if (text.length === 0) {
|
|
296
297
|
return {};
|
|
297
298
|
}
|
|
298
299
|
try {
|
|
299
300
|
return JSON.parse(text);
|
|
300
301
|
} catch {
|
|
301
|
-
throw new ConfluenceApiError("Response was not valid JSON", response.status, endpoint, text);
|
|
302
|
+
throw new ConfluenceApiError("Response was not valid JSON", response.status, endpoint, truncateText(text));
|
|
302
303
|
}
|
|
303
304
|
} catch (cause) {
|
|
304
305
|
if (cause instanceof ConfluenceApiError || cause instanceof ConfluenceUploadError) {
|
|
@@ -437,6 +438,16 @@ function sanitizeFilename(name) {
|
|
|
437
438
|
}
|
|
438
439
|
return cleaned;
|
|
439
440
|
}
|
|
441
|
+
function truncateText(text, maxBytes = MAX_ERROR_BODY_LENGTH) {
|
|
442
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) {
|
|
443
|
+
return text;
|
|
444
|
+
}
|
|
445
|
+
let end = text.length;
|
|
446
|
+
while (end > 0 && Buffer.byteLength(text.slice(0, end), "utf8") > maxBytes) {
|
|
447
|
+
end -= 1;
|
|
448
|
+
}
|
|
449
|
+
return text.slice(0, end) + "...";
|
|
450
|
+
}
|
|
440
451
|
function multipartField(boundary, name, filename, value) {
|
|
441
452
|
const headerLines = [`--${boundary}\r
|
|
442
453
|
`];
|
|
@@ -709,13 +720,22 @@ function markdownToStorage(markdown, options = {}) {
|
|
|
709
720
|
out.push(`<blockquote>${renderInline(quoteLines.join("\n"))}</blockquote>`);
|
|
710
721
|
continue;
|
|
711
722
|
}
|
|
723
|
+
if (isTableStart(lines, i)) {
|
|
724
|
+
const tableLines = [];
|
|
725
|
+
while (i < lines.length && typeof lines[i] === "string" && isTableRow(lines[i])) {
|
|
726
|
+
tableLines.push(lines[i]);
|
|
727
|
+
i += 1;
|
|
728
|
+
}
|
|
729
|
+
out.push(renderTable(tableLines));
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
712
732
|
if (/^\s{0,3}---+\s*$/.test(line) || /^\s{0,3}\*\*\*+\s*$/.test(line)) {
|
|
713
733
|
out.push("<hr />");
|
|
714
734
|
i += 1;
|
|
715
735
|
continue;
|
|
716
736
|
}
|
|
717
737
|
const para = [];
|
|
718
|
-
while (i < lines.length && typeof lines[i] === "string" && lines[i].trim() !== "" && !/^\s{0,3}#{1,6}\s/.test(lines[i]) && !/^\s{0,3}```/.test(lines[i]) && !/^\s{0,3}(?:-|\*|\+)\s+/.test(lines[i]) && !/^\s{0,3}\d+\.\s+/.test(lines[i]) && !/^\s{0,3}>/.test(lines[i]) && !/^\s{0,3}---+\s*$/.test(lines[i]) && !/^\s{0,3}\*\*\*+\s*$/.test(lines[i])) {
|
|
738
|
+
while (i < lines.length && typeof lines[i] === "string" && lines[i].trim() !== "" && !/^\s{0,3}#{1,6}\s/.test(lines[i]) && !/^\s{0,3}```/.test(lines[i]) && !/^\s{0,3}(?:-|\*|\+)\s+/.test(lines[i]) && !/^\s{0,3}\d+\.\s+/.test(lines[i]) && !/^\s{0,3}>/.test(lines[i]) && !isTableStart(lines, i) && !/^\s{0,3}---+\s*$/.test(lines[i]) && !/^\s{0,3}\*\*\*+\s*$/.test(lines[i])) {
|
|
719
739
|
para.push(lines[i]);
|
|
720
740
|
i += 1;
|
|
721
741
|
}
|
|
@@ -775,6 +795,60 @@ function renderList(listLines) {
|
|
|
775
795
|
const body = items.map((item) => `<li>${renderInline(item)}</li>`).join("");
|
|
776
796
|
return `<${tag}>${body}</${tag}>`;
|
|
777
797
|
}
|
|
798
|
+
function isTableStart(lines, index) {
|
|
799
|
+
const header = lines[index];
|
|
800
|
+
const separator = lines[index + 1];
|
|
801
|
+
if (typeof header !== "string" || typeof separator !== "string") {
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
if (!isTableRow(header) || !isTableSeparator(separator)) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
return splitTableRow(header).length === splitTableRow(separator).length;
|
|
808
|
+
}
|
|
809
|
+
function isTableRow(line) {
|
|
810
|
+
return /^\s{0,3}\|/.test(line) && splitTableRow(line).length > 1;
|
|
811
|
+
}
|
|
812
|
+
function isTableSeparator(line) {
|
|
813
|
+
const cells = splitTableRow(line);
|
|
814
|
+
if (cells.length < 2) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
return cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
|
|
818
|
+
}
|
|
819
|
+
function splitTableRow(line) {
|
|
820
|
+
const trimmed = line.trim();
|
|
821
|
+
const start = trimmed.startsWith("|") ? 1 : 0;
|
|
822
|
+
const end = trimmed.endsWith("|") && !isEscapedPipe(trimmed, trimmed.length - 1) ? trimmed.length - 1 : trimmed.length;
|
|
823
|
+
const cells = [];
|
|
824
|
+
let cellStart = start;
|
|
825
|
+
for (let i = start; i < end; i += 1) {
|
|
826
|
+
if (trimmed[i] === "|" && !isEscapedPipe(trimmed, i)) {
|
|
827
|
+
cells.push(unescapeTableCell(trimmed.slice(cellStart, i).trim()));
|
|
828
|
+
cellStart = i + 1;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
cells.push(unescapeTableCell(trimmed.slice(cellStart, end).trim()));
|
|
832
|
+
return cells;
|
|
833
|
+
}
|
|
834
|
+
function isEscapedPipe(text, index) {
|
|
835
|
+
let backslashes = 0;
|
|
836
|
+
for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
|
|
837
|
+
backslashes += 1;
|
|
838
|
+
}
|
|
839
|
+
return backslashes % 2 === 1;
|
|
840
|
+
}
|
|
841
|
+
function unescapeTableCell(cell) {
|
|
842
|
+
return cell.replace(/\\\|/g, "|");
|
|
843
|
+
}
|
|
844
|
+
function renderTable(tableLines) {
|
|
845
|
+
const rows = tableLines.filter((_, index) => index !== 1).map(splitTableRow);
|
|
846
|
+
const body = rows.map((cells, rowIndex) => {
|
|
847
|
+
const tag = rowIndex === 0 ? "th" : "td";
|
|
848
|
+
return "<tr>" + cells.map((cell) => "<" + tag + ">" + renderInline(cell) + "</" + tag + ">").join("") + "</tr>";
|
|
849
|
+
}).join("");
|
|
850
|
+
return "<table><tbody>" + body + "</tbody></table>";
|
|
851
|
+
}
|
|
778
852
|
function renderInline(text) {
|
|
779
853
|
const out = [];
|
|
780
854
|
const tokens = tokenizeInline(text, 0, text.length);
|
|
@@ -1547,16 +1621,17 @@ function validateLocalSync(entries, plan) {
|
|
|
1547
1621
|
const { html, mermaidBlocks } = markdownToStorage(markdown, {
|
|
1548
1622
|
renderHtmlBlocks: plan.renderHtmlBlocks
|
|
1549
1623
|
});
|
|
1624
|
+
const body = appendRepositoryNotice(html, plan.repositoryUrl);
|
|
1550
1625
|
const markdownDir = dirname(entry.absolute);
|
|
1551
|
-
const hasLocalImages = hasLocalImagePlaceholder(
|
|
1626
|
+
const hasLocalImages = hasLocalImagePlaceholder(body);
|
|
1552
1627
|
const hasMermaidBlocks = mermaidBlocks.length > 0;
|
|
1553
|
-
const attachments = hasLocalImages ? validateAttachmentSources(
|
|
1628
|
+
const attachments = hasLocalImages ? validateAttachmentSources(body, {
|
|
1554
1629
|
markdownDir,
|
|
1555
1630
|
allowedRoot: plan.folder
|
|
1556
1631
|
}) : [];
|
|
1557
1632
|
plans.push({
|
|
1558
1633
|
entry,
|
|
1559
|
-
html,
|
|
1634
|
+
html: body,
|
|
1560
1635
|
mermaidBlocks,
|
|
1561
1636
|
markdownDir,
|
|
1562
1637
|
hasLocalImages,
|
|
@@ -1613,6 +1688,11 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1613
1688
|
throw new Error(`parentPageId must be numeric, got: ${options.parentPageId}`);
|
|
1614
1689
|
}
|
|
1615
1690
|
}
|
|
1691
|
+
if (options.repositoryUrl && !isAllowedUrl(options.repositoryUrl)) {
|
|
1692
|
+
throw new Error(
|
|
1693
|
+
"repositoryUrl must be an http(s), protocol-relative, server-relative, mailto, tel, or relative URL"
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1616
1696
|
return {
|
|
1617
1697
|
cwd,
|
|
1618
1698
|
folder,
|
|
@@ -1624,7 +1704,8 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1624
1704
|
versionMessage: options.versionMessage ?? "Synced via repo-toolkit-confluence",
|
|
1625
1705
|
skipUnchanged: options.skipUnchanged ?? true,
|
|
1626
1706
|
dryRun: options.dryRun ?? false,
|
|
1627
|
-
renderHtmlBlocks: options.renderHtmlBlocks === true
|
|
1707
|
+
renderHtmlBlocks: options.renderHtmlBlocks === true,
|
|
1708
|
+
repositoryUrl: options.repositoryUrl ?? ""
|
|
1628
1709
|
};
|
|
1629
1710
|
}
|
|
1630
1711
|
async function syncConfluenceToDocs(options = {}) {
|
|
@@ -1820,6 +1901,14 @@ function hasLocalImagePlaceholder(html) {
|
|
|
1820
1901
|
LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
|
|
1821
1902
|
return LOCAL_IMAGE_PLACEHOLDER_RE.test(html);
|
|
1822
1903
|
}
|
|
1904
|
+
function appendRepositoryNotice(html, repositoryUrl) {
|
|
1905
|
+
if (repositoryUrl.length === 0) {
|
|
1906
|
+
return html;
|
|
1907
|
+
}
|
|
1908
|
+
const url = escapeXmlAttribute(repositoryUrl);
|
|
1909
|
+
const text = escapeHtml(repositoryUrl);
|
|
1910
|
+
return html + '\n<p><em>This document is synced from repository <a href="' + url + '">' + text + "</a>.</em></p>";
|
|
1911
|
+
}
|
|
1823
1912
|
async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
|
|
1824
1913
|
let predicted = html;
|
|
1825
1914
|
if (ctx.hasMermaidBlocks) {
|
|
@@ -1873,6 +1962,7 @@ var SPECS = [
|
|
|
1873
1962
|
{ name: "space-key" },
|
|
1874
1963
|
{ name: "parent-page-id" },
|
|
1875
1964
|
{ name: "version-message" },
|
|
1965
|
+
{ name: "repository-url" },
|
|
1876
1966
|
{ name: "skip-unchanged", boolean: true, negatable: true },
|
|
1877
1967
|
{ name: "dry-run", boolean: true },
|
|
1878
1968
|
{ name: "render-html-blocks", boolean: true },
|
|
@@ -1917,6 +2007,7 @@ Environment variables (CLI form; GitHub Action INPUT_* form is also read):
|
|
|
1917
2007
|
CONFLUENCE_SPACE_KEY Confluence space key
|
|
1918
2008
|
CONFLUENCE_PARENT_PAGE_ID Numeric parent page id
|
|
1919
2009
|
CONFLUENCE_VERSION_MESSAGE Version-message suffix for every PUT
|
|
2010
|
+
CONFLUENCE_REPOSITORY_URL Repository URL appended to synced pages
|
|
1920
2011
|
CONFLUENCE_SKIP_UNCHANGED true|false (default: true)
|
|
1921
2012
|
CONFLUENCE_DRY_RUN true|false (default: false)
|
|
1922
2013
|
CONFLUENCE_RENDER_HTML_BLOCKS true|false (default: false)
|
|
@@ -1936,6 +2027,7 @@ Options:
|
|
|
1936
2027
|
--space-key <key> Confluence space key (required). Resolved to a spaceId via the API
|
|
1937
2028
|
--parent-page-id <id> Numeric page id under which docs will be published (required)
|
|
1938
2029
|
--version-message <text> Commit message appended to every page/attachment PUT
|
|
2030
|
+
--repository-url <url> Repository URL appended to synced pages as an italic notice
|
|
1939
2031
|
--skip-unchanged Skip pages whose body is unchanged (default: true)
|
|
1940
2032
|
--no-skip-unchanged Re-upload every page even when unchanged
|
|
1941
2033
|
--dry-run Walk the doc tree and print the plan without API calls
|
|
@@ -1954,7 +2046,8 @@ var STRING_OPTION_KEYS = [
|
|
|
1954
2046
|
"baseUrl",
|
|
1955
2047
|
"spaceKey",
|
|
1956
2048
|
"parentPageId",
|
|
1957
|
-
"versionMessage"
|
|
2049
|
+
"versionMessage",
|
|
2050
|
+
"repositoryUrl"
|
|
1958
2051
|
];
|
|
1959
2052
|
function setIfString(options, key, value) {
|
|
1960
2053
|
if (typeof value === "string" && value.length > 0) {
|
|
@@ -1975,6 +2068,7 @@ var ENV_BINDINGS = [
|
|
|
1975
2068
|
{ envName: "INPUT_SPACE-KEY", key: "spaceKey", kind: "string" },
|
|
1976
2069
|
{ envName: "INPUT_PARENT-PAGE-ID", key: "parentPageId", kind: "string" },
|
|
1977
2070
|
{ envName: "INPUT_VERSION-MESSAGE", key: "versionMessage", kind: "string" },
|
|
2071
|
+
{ envName: "INPUT_REPOSITORY-URL", key: "repositoryUrl", kind: "string" },
|
|
1978
2072
|
{ envName: "INPUT_DRY-RUN", key: "dryRun", kind: "boolean" },
|
|
1979
2073
|
{ envName: "INPUT_SKIP-UNCHANGED", key: "skipUnchanged", kind: "boolean" },
|
|
1980
2074
|
{ envName: "INPUT_RENDER-HTML-BLOCKS", key: "renderHtmlBlocks", kind: "boolean" },
|
|
@@ -1986,6 +2080,7 @@ var ENV_BINDINGS = [
|
|
|
1986
2080
|
{ envName: "CONFLUENCE_SPACE_KEY", key: "spaceKey", kind: "string" },
|
|
1987
2081
|
{ envName: "CONFLUENCE_PARENT_PAGE_ID", key: "parentPageId", kind: "string" },
|
|
1988
2082
|
{ envName: "CONFLUENCE_VERSION_MESSAGE", key: "versionMessage", kind: "string" },
|
|
2083
|
+
{ envName: "CONFLUENCE_REPOSITORY_URL", key: "repositoryUrl", kind: "string" },
|
|
1989
2084
|
{ envName: "CONFLUENCE_DRY_RUN", key: "dryRun", kind: "boolean" },
|
|
1990
2085
|
{ envName: "CONFLUENCE_SKIP_UNCHANGED", key: "skipUnchanged", kind: "boolean" },
|
|
1991
2086
|
{ envName: "CONFLUENCE_RENDER_HTML_BLOCKS", key: "renderHtmlBlocks", kind: "boolean" }
|
|
@@ -2006,8 +2101,22 @@ function optionsFromEnv(env = process.env) {
|
|
|
2006
2101
|
options[key] = parseBooleanEnv(raw, envName);
|
|
2007
2102
|
}
|
|
2008
2103
|
}
|
|
2104
|
+
if (options.repositoryUrl === void 0) {
|
|
2105
|
+
const githubRepositoryUrl = repositoryUrlFromGitHubEnv(env);
|
|
2106
|
+
if (githubRepositoryUrl) {
|
|
2107
|
+
options.repositoryUrl = githubRepositoryUrl;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2009
2110
|
return options;
|
|
2010
2111
|
}
|
|
2112
|
+
function repositoryUrlFromGitHubEnv(env) {
|
|
2113
|
+
const repository = env.GITHUB_REPOSITORY;
|
|
2114
|
+
if (!repository) {
|
|
2115
|
+
return void 0;
|
|
2116
|
+
}
|
|
2117
|
+
const serverUrl = env.GITHUB_SERVER_URL || "https://github.com";
|
|
2118
|
+
return serverUrl.replace(/\/+$/, "") + "/" + repository.replace(/^\/+/, "");
|
|
2119
|
+
}
|
|
2011
2120
|
function buildOptions(result) {
|
|
2012
2121
|
if (!result) {
|
|
2013
2122
|
return {};
|
|
@@ -2023,6 +2132,7 @@ function buildOptions(result) {
|
|
|
2023
2132
|
setIfString(options, "spaceKey", values["space-key"]);
|
|
2024
2133
|
setIfString(options, "parentPageId", values["parent-page-id"]);
|
|
2025
2134
|
setIfString(options, "versionMessage", values["version-message"]);
|
|
2135
|
+
setIfString(options, "repositoryUrl", values["repository-url"]);
|
|
2026
2136
|
if (values["skip-unchanged"] !== void 0) {
|
|
2027
2137
|
options.skipUnchanged = values["skip-unchanged"] === "true";
|
|
2028
2138
|
}
|
package/index.d.ts
CHANGED
|
@@ -338,6 +338,8 @@ interface ConfluenceSyncOptions {
|
|
|
338
338
|
skipUnchanged?: boolean;
|
|
339
339
|
/** Render ```html fenced blocks as inline HTML via the Confluence `html` macro instead of a code box (default: false). */
|
|
340
340
|
renderHtmlBlocks?: boolean;
|
|
341
|
+
/** Repository URL appended to synced pages as an italic source notice. */
|
|
342
|
+
repositoryUrl?: string;
|
|
341
343
|
/**
|
|
342
344
|
* Dry-run: walk the tree and validate every markdown file and local image
|
|
343
345
|
* source (same preflight as a real sync) then print the plan, but make no
|
|
@@ -367,6 +369,7 @@ interface ConfluenceSyncPlan {
|
|
|
367
369
|
skipUnchanged: boolean;
|
|
368
370
|
dryRun: boolean;
|
|
369
371
|
renderHtmlBlocks: boolean;
|
|
372
|
+
repositoryUrl: string;
|
|
370
373
|
}
|
|
371
374
|
/** A single markdown entry's locally-validated sync plan. */
|
|
372
375
|
interface LocalSyncEntryPlan {
|
package/index.js
CHANGED
|
@@ -268,23 +268,24 @@ Content-Type: application/octet-stream\r
|
|
|
268
268
|
if (response.status >= 300 && response.status < 400) {
|
|
269
269
|
throw new ConfluenceApiError("Redirect responses are not allowed", response.status, endpoint, "");
|
|
270
270
|
}
|
|
271
|
-
const text = await readBodyBounded(response, MAX_ERROR_BODY_LENGTH);
|
|
272
271
|
if (!response.ok) {
|
|
272
|
+
const text2 = await readBodyBounded(response, MAX_ERROR_BODY_LENGTH);
|
|
273
273
|
if (isSafe && shouldRetryStatus(response.status) && attempt < maxRetries) {
|
|
274
274
|
attempt += 1;
|
|
275
|
-
lastError = new ConfluenceApiError(describeStatus(response.status), response.status, endpoint,
|
|
275
|
+
lastError = new ConfluenceApiError(describeStatus(response.status), response.status, endpoint, text2);
|
|
276
276
|
await sleepBackoff(response, endpoint);
|
|
277
277
|
continue;
|
|
278
278
|
}
|
|
279
|
-
throw new ConfluenceApiError(describeStatus(response.status), response.status, endpoint,
|
|
279
|
+
throw new ConfluenceApiError(describeStatus(response.status), response.status, endpoint, text2);
|
|
280
280
|
}
|
|
281
|
+
const text = await response.text();
|
|
281
282
|
if (text.length === 0) {
|
|
282
283
|
return {};
|
|
283
284
|
}
|
|
284
285
|
try {
|
|
285
286
|
return JSON.parse(text);
|
|
286
287
|
} catch {
|
|
287
|
-
throw new ConfluenceApiError("Response was not valid JSON", response.status, endpoint, text);
|
|
288
|
+
throw new ConfluenceApiError("Response was not valid JSON", response.status, endpoint, truncateText(text));
|
|
288
289
|
}
|
|
289
290
|
} catch (cause) {
|
|
290
291
|
if (cause instanceof ConfluenceApiError || cause instanceof ConfluenceUploadError) {
|
|
@@ -423,6 +424,16 @@ function sanitizeFilename(name) {
|
|
|
423
424
|
}
|
|
424
425
|
return cleaned;
|
|
425
426
|
}
|
|
427
|
+
function truncateText(text, maxBytes = MAX_ERROR_BODY_LENGTH) {
|
|
428
|
+
if (Buffer.byteLength(text, "utf8") <= maxBytes) {
|
|
429
|
+
return text;
|
|
430
|
+
}
|
|
431
|
+
let end = text.length;
|
|
432
|
+
while (end > 0 && Buffer.byteLength(text.slice(0, end), "utf8") > maxBytes) {
|
|
433
|
+
end -= 1;
|
|
434
|
+
}
|
|
435
|
+
return text.slice(0, end) + "...";
|
|
436
|
+
}
|
|
426
437
|
function multipartField(boundary, name, filename, value) {
|
|
427
438
|
const headerLines = [`--${boundary}\r
|
|
428
439
|
`];
|
|
@@ -695,13 +706,22 @@ function markdownToStorage(markdown, options = {}) {
|
|
|
695
706
|
out.push(`<blockquote>${renderInline(quoteLines.join("\n"))}</blockquote>`);
|
|
696
707
|
continue;
|
|
697
708
|
}
|
|
709
|
+
if (isTableStart(lines, i)) {
|
|
710
|
+
const tableLines = [];
|
|
711
|
+
while (i < lines.length && typeof lines[i] === "string" && isTableRow(lines[i])) {
|
|
712
|
+
tableLines.push(lines[i]);
|
|
713
|
+
i += 1;
|
|
714
|
+
}
|
|
715
|
+
out.push(renderTable(tableLines));
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
698
718
|
if (/^\s{0,3}---+\s*$/.test(line) || /^\s{0,3}\*\*\*+\s*$/.test(line)) {
|
|
699
719
|
out.push("<hr />");
|
|
700
720
|
i += 1;
|
|
701
721
|
continue;
|
|
702
722
|
}
|
|
703
723
|
const para = [];
|
|
704
|
-
while (i < lines.length && typeof lines[i] === "string" && lines[i].trim() !== "" && !/^\s{0,3}#{1,6}\s/.test(lines[i]) && !/^\s{0,3}```/.test(lines[i]) && !/^\s{0,3}(?:-|\*|\+)\s+/.test(lines[i]) && !/^\s{0,3}\d+\.\s+/.test(lines[i]) && !/^\s{0,3}>/.test(lines[i]) && !/^\s{0,3}---+\s*$/.test(lines[i]) && !/^\s{0,3}\*\*\*+\s*$/.test(lines[i])) {
|
|
724
|
+
while (i < lines.length && typeof lines[i] === "string" && lines[i].trim() !== "" && !/^\s{0,3}#{1,6}\s/.test(lines[i]) && !/^\s{0,3}```/.test(lines[i]) && !/^\s{0,3}(?:-|\*|\+)\s+/.test(lines[i]) && !/^\s{0,3}\d+\.\s+/.test(lines[i]) && !/^\s{0,3}>/.test(lines[i]) && !isTableStart(lines, i) && !/^\s{0,3}---+\s*$/.test(lines[i]) && !/^\s{0,3}\*\*\*+\s*$/.test(lines[i])) {
|
|
705
725
|
para.push(lines[i]);
|
|
706
726
|
i += 1;
|
|
707
727
|
}
|
|
@@ -761,6 +781,60 @@ function renderList(listLines) {
|
|
|
761
781
|
const body = items.map((item) => `<li>${renderInline(item)}</li>`).join("");
|
|
762
782
|
return `<${tag}>${body}</${tag}>`;
|
|
763
783
|
}
|
|
784
|
+
function isTableStart(lines, index) {
|
|
785
|
+
const header = lines[index];
|
|
786
|
+
const separator = lines[index + 1];
|
|
787
|
+
if (typeof header !== "string" || typeof separator !== "string") {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
if (!isTableRow(header) || !isTableSeparator(separator)) {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
return splitTableRow(header).length === splitTableRow(separator).length;
|
|
794
|
+
}
|
|
795
|
+
function isTableRow(line) {
|
|
796
|
+
return /^\s{0,3}\|/.test(line) && splitTableRow(line).length > 1;
|
|
797
|
+
}
|
|
798
|
+
function isTableSeparator(line) {
|
|
799
|
+
const cells = splitTableRow(line);
|
|
800
|
+
if (cells.length < 2) {
|
|
801
|
+
return false;
|
|
802
|
+
}
|
|
803
|
+
return cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
|
|
804
|
+
}
|
|
805
|
+
function splitTableRow(line) {
|
|
806
|
+
const trimmed = line.trim();
|
|
807
|
+
const start = trimmed.startsWith("|") ? 1 : 0;
|
|
808
|
+
const end = trimmed.endsWith("|") && !isEscapedPipe(trimmed, trimmed.length - 1) ? trimmed.length - 1 : trimmed.length;
|
|
809
|
+
const cells = [];
|
|
810
|
+
let cellStart = start;
|
|
811
|
+
for (let i = start; i < end; i += 1) {
|
|
812
|
+
if (trimmed[i] === "|" && !isEscapedPipe(trimmed, i)) {
|
|
813
|
+
cells.push(unescapeTableCell(trimmed.slice(cellStart, i).trim()));
|
|
814
|
+
cellStart = i + 1;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
cells.push(unescapeTableCell(trimmed.slice(cellStart, end).trim()));
|
|
818
|
+
return cells;
|
|
819
|
+
}
|
|
820
|
+
function isEscapedPipe(text, index) {
|
|
821
|
+
let backslashes = 0;
|
|
822
|
+
for (let i = index - 1; i >= 0 && text[i] === "\\"; i -= 1) {
|
|
823
|
+
backslashes += 1;
|
|
824
|
+
}
|
|
825
|
+
return backslashes % 2 === 1;
|
|
826
|
+
}
|
|
827
|
+
function unescapeTableCell(cell) {
|
|
828
|
+
return cell.replace(/\\\|/g, "|");
|
|
829
|
+
}
|
|
830
|
+
function renderTable(tableLines) {
|
|
831
|
+
const rows = tableLines.filter((_, index) => index !== 1).map(splitTableRow);
|
|
832
|
+
const body = rows.map((cells, rowIndex) => {
|
|
833
|
+
const tag = rowIndex === 0 ? "th" : "td";
|
|
834
|
+
return "<tr>" + cells.map((cell) => "<" + tag + ">" + renderInline(cell) + "</" + tag + ">").join("") + "</tr>";
|
|
835
|
+
}).join("");
|
|
836
|
+
return "<table><tbody>" + body + "</tbody></table>";
|
|
837
|
+
}
|
|
764
838
|
function renderInline(text) {
|
|
765
839
|
const out = [];
|
|
766
840
|
const tokens = tokenizeInline(text, 0, text.length);
|
|
@@ -1534,16 +1608,17 @@ function validateLocalSync(entries, plan) {
|
|
|
1534
1608
|
const { html, mermaidBlocks } = markdownToStorage(markdown, {
|
|
1535
1609
|
renderHtmlBlocks: plan.renderHtmlBlocks
|
|
1536
1610
|
});
|
|
1611
|
+
const body = appendRepositoryNotice(html, plan.repositoryUrl);
|
|
1537
1612
|
const markdownDir = dirname(entry.absolute);
|
|
1538
|
-
const hasLocalImages = hasLocalImagePlaceholder(
|
|
1613
|
+
const hasLocalImages = hasLocalImagePlaceholder(body);
|
|
1539
1614
|
const hasMermaidBlocks = mermaidBlocks.length > 0;
|
|
1540
|
-
const attachments = hasLocalImages ? validateAttachmentSources(
|
|
1615
|
+
const attachments = hasLocalImages ? validateAttachmentSources(body, {
|
|
1541
1616
|
markdownDir,
|
|
1542
1617
|
allowedRoot: plan.folder
|
|
1543
1618
|
}) : [];
|
|
1544
1619
|
plans.push({
|
|
1545
1620
|
entry,
|
|
1546
|
-
html,
|
|
1621
|
+
html: body,
|
|
1547
1622
|
mermaidBlocks,
|
|
1548
1623
|
markdownDir,
|
|
1549
1624
|
hasLocalImages,
|
|
@@ -1600,6 +1675,11 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1600
1675
|
throw new Error(`parentPageId must be numeric, got: ${options.parentPageId}`);
|
|
1601
1676
|
}
|
|
1602
1677
|
}
|
|
1678
|
+
if (options.repositoryUrl && !isAllowedUrl(options.repositoryUrl)) {
|
|
1679
|
+
throw new Error(
|
|
1680
|
+
"repositoryUrl must be an http(s), protocol-relative, server-relative, mailto, tel, or relative URL"
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1603
1683
|
return {
|
|
1604
1684
|
cwd,
|
|
1605
1685
|
folder,
|
|
@@ -1611,7 +1691,8 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1611
1691
|
versionMessage: options.versionMessage ?? "Synced via repo-toolkit-confluence",
|
|
1612
1692
|
skipUnchanged: options.skipUnchanged ?? true,
|
|
1613
1693
|
dryRun: options.dryRun ?? false,
|
|
1614
|
-
renderHtmlBlocks: options.renderHtmlBlocks === true
|
|
1694
|
+
renderHtmlBlocks: options.renderHtmlBlocks === true,
|
|
1695
|
+
repositoryUrl: options.repositoryUrl ?? ""
|
|
1615
1696
|
};
|
|
1616
1697
|
}
|
|
1617
1698
|
async function syncConfluenceToDocs(options = {}) {
|
|
@@ -1807,6 +1888,14 @@ function hasLocalImagePlaceholder(html) {
|
|
|
1807
1888
|
LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
|
|
1808
1889
|
return LOCAL_IMAGE_PLACEHOLDER_RE.test(html);
|
|
1809
1890
|
}
|
|
1891
|
+
function appendRepositoryNotice(html, repositoryUrl) {
|
|
1892
|
+
if (repositoryUrl.length === 0) {
|
|
1893
|
+
return html;
|
|
1894
|
+
}
|
|
1895
|
+
const url = escapeXmlAttribute(repositoryUrl);
|
|
1896
|
+
const text = escapeHtml(repositoryUrl);
|
|
1897
|
+
return html + '\n<p><em>This document is synced from repository <a href="' + url + '">' + text + "</a>.</em></p>";
|
|
1898
|
+
}
|
|
1810
1899
|
async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
|
|
1811
1900
|
let predicted = html;
|
|
1812
1901
|
if (ctx.hasMermaidBlocks) {
|
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.15.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.15.0"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"**/*",
|