@repo-toolkit/confluence 0.14.2 → 0.16.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 +5 -0
- package/cli.js +129 -7
- package/index.d.ts +3 -0
- package/index.js +107 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -72,6 +72,10 @@ 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. The `--folder` path is added to the notice link when it
|
|
77
|
+
is relative to `--cwd`. When omitted, GitHub Actions runs infer this from
|
|
78
|
+
`GITHUB_SERVER_URL` and `GITHUB_REPOSITORY`.
|
|
75
79
|
- `--skip-unchanged` / `--no-skip-unchanged` — skip pages whose body is unchanged (default: `skip`)
|
|
76
80
|
- `--dry-run` — walk the doc tree and validate every markdown file and local
|
|
77
81
|
image source (same preflight as a real sync) then log the plan. No API
|
|
@@ -99,6 +103,7 @@ both read for every option. Boolean env values accept `true|1|yes|on` /
|
|
|
99
103
|
| spaceKey | `CONFLUENCE_SPACE_KEY` | `INPUT_SPACE-KEY` |
|
|
100
104
|
| parentPageId | `CONFLUENCE_PARENT_PAGE_ID` | `INPUT_PARENT-PAGE-ID` |
|
|
101
105
|
| versionMessage | `CONFLUENCE_VERSION_MESSAGE` | `INPUT_VERSION-MESSAGE` |
|
|
106
|
+
| repositoryUrl | `CONFLUENCE_REPOSITORY_URL` | `INPUT_REPOSITORY-URL` |
|
|
102
107
|
| skipUnchanged (bool) | `CONFLUENCE_SKIP_UNCHANGED` | `INPUT_SKIP-UNCHANGED` |
|
|
103
108
|
| dryRun (bool) | `CONFLUENCE_DRY_RUN` | `INPUT_DRY-RUN` |
|
|
104
109
|
| renderHtmlBlocks (bool) | `CONFLUENCE_RENDER_HTML_BLOCKS` | `INPUT_RENDER-HTML-BLOCKS` |
|
package/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
|
|
15
15
|
// src/index.ts
|
|
16
16
|
import { readFileSync as readFileSync2 } from "fs";
|
|
17
|
-
import { dirname, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
17
|
+
import { dirname, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve2 } from "path";
|
|
18
18
|
|
|
19
19
|
// src/confluence-client.ts
|
|
20
20
|
import { Buffer } from "buffer";
|
|
@@ -720,13 +720,22 @@ function markdownToStorage(markdown, options = {}) {
|
|
|
720
720
|
out.push(`<blockquote>${renderInline(quoteLines.join("\n"))}</blockquote>`);
|
|
721
721
|
continue;
|
|
722
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
|
+
}
|
|
723
732
|
if (/^\s{0,3}---+\s*$/.test(line) || /^\s{0,3}\*\*\*+\s*$/.test(line)) {
|
|
724
733
|
out.push("<hr />");
|
|
725
734
|
i += 1;
|
|
726
735
|
continue;
|
|
727
736
|
}
|
|
728
737
|
const para = [];
|
|
729
|
-
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])) {
|
|
730
739
|
para.push(lines[i]);
|
|
731
740
|
i += 1;
|
|
732
741
|
}
|
|
@@ -786,6 +795,60 @@ function renderList(listLines) {
|
|
|
786
795
|
const body = items.map((item) => `<li>${renderInline(item)}</li>`).join("");
|
|
787
796
|
return `<${tag}>${body}</${tag}>`;
|
|
788
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
|
+
}
|
|
789
852
|
function renderInline(text) {
|
|
790
853
|
const out = [];
|
|
791
854
|
const tokens = tokenizeInline(text, 0, text.length);
|
|
@@ -1558,16 +1621,17 @@ function validateLocalSync(entries, plan) {
|
|
|
1558
1621
|
const { html, mermaidBlocks } = markdownToStorage(markdown, {
|
|
1559
1622
|
renderHtmlBlocks: plan.renderHtmlBlocks
|
|
1560
1623
|
});
|
|
1624
|
+
const body = appendRepositoryNotice(html, plan.repositoryUrl);
|
|
1561
1625
|
const markdownDir = dirname(entry.absolute);
|
|
1562
|
-
const hasLocalImages = hasLocalImagePlaceholder(
|
|
1626
|
+
const hasLocalImages = hasLocalImagePlaceholder(body);
|
|
1563
1627
|
const hasMermaidBlocks = mermaidBlocks.length > 0;
|
|
1564
|
-
const attachments = hasLocalImages ? validateAttachmentSources(
|
|
1628
|
+
const attachments = hasLocalImages ? validateAttachmentSources(body, {
|
|
1565
1629
|
markdownDir,
|
|
1566
1630
|
allowedRoot: plan.folder
|
|
1567
1631
|
}) : [];
|
|
1568
1632
|
plans.push({
|
|
1569
1633
|
entry,
|
|
1570
|
-
html,
|
|
1634
|
+
html: body,
|
|
1571
1635
|
mermaidBlocks,
|
|
1572
1636
|
markdownDir,
|
|
1573
1637
|
hasLocalImages,
|
|
@@ -1624,6 +1688,12 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1624
1688
|
throw new Error(`parentPageId must be numeric, got: ${options.parentPageId}`);
|
|
1625
1689
|
}
|
|
1626
1690
|
}
|
|
1691
|
+
const repositoryUrl = options.repositoryUrl ? repositoryNoticeUrl(options.repositoryUrl, cwd, options.folder ?? "") : "";
|
|
1692
|
+
if (repositoryUrl && !isAllowedUrl(repositoryUrl)) {
|
|
1693
|
+
throw new Error(
|
|
1694
|
+
"repositoryUrl must be an http(s), protocol-relative, server-relative, mailto, tel, or relative URL"
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1627
1697
|
return {
|
|
1628
1698
|
cwd,
|
|
1629
1699
|
folder,
|
|
@@ -1635,7 +1705,8 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1635
1705
|
versionMessage: options.versionMessage ?? "Synced via repo-toolkit-confluence",
|
|
1636
1706
|
skipUnchanged: options.skipUnchanged ?? true,
|
|
1637
1707
|
dryRun: options.dryRun ?? false,
|
|
1638
|
-
renderHtmlBlocks: options.renderHtmlBlocks === true
|
|
1708
|
+
renderHtmlBlocks: options.renderHtmlBlocks === true,
|
|
1709
|
+
repositoryUrl
|
|
1639
1710
|
};
|
|
1640
1711
|
}
|
|
1641
1712
|
async function syncConfluenceToDocs(options = {}) {
|
|
@@ -1831,6 +1902,36 @@ function hasLocalImagePlaceholder(html) {
|
|
|
1831
1902
|
LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
|
|
1832
1903
|
return LOCAL_IMAGE_PLACEHOLDER_RE.test(html);
|
|
1833
1904
|
}
|
|
1905
|
+
function appendRepositoryNotice(html, repositoryUrl) {
|
|
1906
|
+
if (repositoryUrl.length === 0) {
|
|
1907
|
+
return html;
|
|
1908
|
+
}
|
|
1909
|
+
const url = escapeXmlAttribute(repositoryUrl);
|
|
1910
|
+
const text = escapeHtml(repositoryUrl);
|
|
1911
|
+
return html + '\n<p><em>This document is synced from repository <a href="' + url + '">' + text + "</a>.</em></p>";
|
|
1912
|
+
}
|
|
1913
|
+
function repositoryNoticeUrl(repositoryUrl, cwd, folder) {
|
|
1914
|
+
const folderPath = repositoryFolderPath(cwd, folder);
|
|
1915
|
+
if (folderPath.length === 0) {
|
|
1916
|
+
return repositoryUrl;
|
|
1917
|
+
}
|
|
1918
|
+
const trimmedUrl = repositoryUrl.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
1919
|
+
const encodedFolder = folderPath.split("/").map(encodeURIComponent).join("/");
|
|
1920
|
+
if (/^https?:\/\/github\.com\//i.test(trimmedUrl) && !/\/tree\/|\/blob\//.test(trimmedUrl)) {
|
|
1921
|
+
return trimmedUrl + "/tree/HEAD/" + encodedFolder;
|
|
1922
|
+
}
|
|
1923
|
+
return trimmedUrl + "/" + encodedFolder;
|
|
1924
|
+
}
|
|
1925
|
+
function repositoryFolderPath(cwd, folder) {
|
|
1926
|
+
if (folder.length === 0) {
|
|
1927
|
+
return "";
|
|
1928
|
+
}
|
|
1929
|
+
const relativePath = relative3(cwd, resolveInputPath(cwd, folder));
|
|
1930
|
+
if (relativePath === "" || relativePath.startsWith("..") || isAbsolute2(relativePath)) {
|
|
1931
|
+
return "";
|
|
1932
|
+
}
|
|
1933
|
+
return relativePath.split(/[\\/]+/).filter((part) => part !== "" && part !== ".").join("/");
|
|
1934
|
+
}
|
|
1834
1935
|
async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
|
|
1835
1936
|
let predicted = html;
|
|
1836
1937
|
if (ctx.hasMermaidBlocks) {
|
|
@@ -1884,6 +1985,7 @@ var SPECS = [
|
|
|
1884
1985
|
{ name: "space-key" },
|
|
1885
1986
|
{ name: "parent-page-id" },
|
|
1886
1987
|
{ name: "version-message" },
|
|
1988
|
+
{ name: "repository-url" },
|
|
1887
1989
|
{ name: "skip-unchanged", boolean: true, negatable: true },
|
|
1888
1990
|
{ name: "dry-run", boolean: true },
|
|
1889
1991
|
{ name: "render-html-blocks", boolean: true },
|
|
@@ -1928,6 +2030,7 @@ Environment variables (CLI form; GitHub Action INPUT_* form is also read):
|
|
|
1928
2030
|
CONFLUENCE_SPACE_KEY Confluence space key
|
|
1929
2031
|
CONFLUENCE_PARENT_PAGE_ID Numeric parent page id
|
|
1930
2032
|
CONFLUENCE_VERSION_MESSAGE Version-message suffix for every PUT
|
|
2033
|
+
CONFLUENCE_REPOSITORY_URL Repository URL appended to synced pages
|
|
1931
2034
|
CONFLUENCE_SKIP_UNCHANGED true|false (default: true)
|
|
1932
2035
|
CONFLUENCE_DRY_RUN true|false (default: false)
|
|
1933
2036
|
CONFLUENCE_RENDER_HTML_BLOCKS true|false (default: false)
|
|
@@ -1947,6 +2050,7 @@ Options:
|
|
|
1947
2050
|
--space-key <key> Confluence space key (required). Resolved to a spaceId via the API
|
|
1948
2051
|
--parent-page-id <id> Numeric page id under which docs will be published (required)
|
|
1949
2052
|
--version-message <text> Commit message appended to every page/attachment PUT
|
|
2053
|
+
--repository-url <url> Repository URL appended to synced pages as an italic notice
|
|
1950
2054
|
--skip-unchanged Skip pages whose body is unchanged (default: true)
|
|
1951
2055
|
--no-skip-unchanged Re-upload every page even when unchanged
|
|
1952
2056
|
--dry-run Walk the doc tree and print the plan without API calls
|
|
@@ -1965,7 +2069,8 @@ var STRING_OPTION_KEYS = [
|
|
|
1965
2069
|
"baseUrl",
|
|
1966
2070
|
"spaceKey",
|
|
1967
2071
|
"parentPageId",
|
|
1968
|
-
"versionMessage"
|
|
2072
|
+
"versionMessage",
|
|
2073
|
+
"repositoryUrl"
|
|
1969
2074
|
];
|
|
1970
2075
|
function setIfString(options, key, value) {
|
|
1971
2076
|
if (typeof value === "string" && value.length > 0) {
|
|
@@ -1986,6 +2091,7 @@ var ENV_BINDINGS = [
|
|
|
1986
2091
|
{ envName: "INPUT_SPACE-KEY", key: "spaceKey", kind: "string" },
|
|
1987
2092
|
{ envName: "INPUT_PARENT-PAGE-ID", key: "parentPageId", kind: "string" },
|
|
1988
2093
|
{ envName: "INPUT_VERSION-MESSAGE", key: "versionMessage", kind: "string" },
|
|
2094
|
+
{ envName: "INPUT_REPOSITORY-URL", key: "repositoryUrl", kind: "string" },
|
|
1989
2095
|
{ envName: "INPUT_DRY-RUN", key: "dryRun", kind: "boolean" },
|
|
1990
2096
|
{ envName: "INPUT_SKIP-UNCHANGED", key: "skipUnchanged", kind: "boolean" },
|
|
1991
2097
|
{ envName: "INPUT_RENDER-HTML-BLOCKS", key: "renderHtmlBlocks", kind: "boolean" },
|
|
@@ -1997,6 +2103,7 @@ var ENV_BINDINGS = [
|
|
|
1997
2103
|
{ envName: "CONFLUENCE_SPACE_KEY", key: "spaceKey", kind: "string" },
|
|
1998
2104
|
{ envName: "CONFLUENCE_PARENT_PAGE_ID", key: "parentPageId", kind: "string" },
|
|
1999
2105
|
{ envName: "CONFLUENCE_VERSION_MESSAGE", key: "versionMessage", kind: "string" },
|
|
2106
|
+
{ envName: "CONFLUENCE_REPOSITORY_URL", key: "repositoryUrl", kind: "string" },
|
|
2000
2107
|
{ envName: "CONFLUENCE_DRY_RUN", key: "dryRun", kind: "boolean" },
|
|
2001
2108
|
{ envName: "CONFLUENCE_SKIP_UNCHANGED", key: "skipUnchanged", kind: "boolean" },
|
|
2002
2109
|
{ envName: "CONFLUENCE_RENDER_HTML_BLOCKS", key: "renderHtmlBlocks", kind: "boolean" }
|
|
@@ -2017,8 +2124,22 @@ function optionsFromEnv(env = process.env) {
|
|
|
2017
2124
|
options[key] = parseBooleanEnv(raw, envName);
|
|
2018
2125
|
}
|
|
2019
2126
|
}
|
|
2127
|
+
if (options.repositoryUrl === void 0) {
|
|
2128
|
+
const githubRepositoryUrl = repositoryUrlFromGitHubEnv(env);
|
|
2129
|
+
if (githubRepositoryUrl) {
|
|
2130
|
+
options.repositoryUrl = githubRepositoryUrl;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2020
2133
|
return options;
|
|
2021
2134
|
}
|
|
2135
|
+
function repositoryUrlFromGitHubEnv(env) {
|
|
2136
|
+
const repository = env.GITHUB_REPOSITORY;
|
|
2137
|
+
if (!repository) {
|
|
2138
|
+
return void 0;
|
|
2139
|
+
}
|
|
2140
|
+
const serverUrl = env.GITHUB_SERVER_URL || "https://github.com";
|
|
2141
|
+
return serverUrl.replace(/\/+$/, "") + "/" + repository.replace(/^\/+/, "");
|
|
2142
|
+
}
|
|
2022
2143
|
function buildOptions(result) {
|
|
2023
2144
|
if (!result) {
|
|
2024
2145
|
return {};
|
|
@@ -2034,6 +2155,7 @@ function buildOptions(result) {
|
|
|
2034
2155
|
setIfString(options, "spaceKey", values["space-key"]);
|
|
2035
2156
|
setIfString(options, "parentPageId", values["parent-page-id"]);
|
|
2036
2157
|
setIfString(options, "versionMessage", values["version-message"]);
|
|
2158
|
+
setIfString(options, "repositoryUrl", values["repository-url"]);
|
|
2037
2159
|
if (values["skip-unchanged"] !== void 0) {
|
|
2038
2160
|
options.skipUnchanged = values["skip-unchanged"] === "true";
|
|
2039
2161
|
}
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3
|
-
import { dirname, isAbsolute as isAbsolute2, resolve as resolve2 } from "path";
|
|
3
|
+
import { dirname, isAbsolute as isAbsolute2, relative as relative3, resolve as resolve2 } from "path";
|
|
4
4
|
|
|
5
5
|
// src/confluence-client.ts
|
|
6
6
|
import { Buffer } from "buffer";
|
|
@@ -706,13 +706,22 @@ function markdownToStorage(markdown, options = {}) {
|
|
|
706
706
|
out.push(`<blockquote>${renderInline(quoteLines.join("\n"))}</blockquote>`);
|
|
707
707
|
continue;
|
|
708
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
|
+
}
|
|
709
718
|
if (/^\s{0,3}---+\s*$/.test(line) || /^\s{0,3}\*\*\*+\s*$/.test(line)) {
|
|
710
719
|
out.push("<hr />");
|
|
711
720
|
i += 1;
|
|
712
721
|
continue;
|
|
713
722
|
}
|
|
714
723
|
const para = [];
|
|
715
|
-
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])) {
|
|
716
725
|
para.push(lines[i]);
|
|
717
726
|
i += 1;
|
|
718
727
|
}
|
|
@@ -772,6 +781,60 @@ function renderList(listLines) {
|
|
|
772
781
|
const body = items.map((item) => `<li>${renderInline(item)}</li>`).join("");
|
|
773
782
|
return `<${tag}>${body}</${tag}>`;
|
|
774
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
|
+
}
|
|
775
838
|
function renderInline(text) {
|
|
776
839
|
const out = [];
|
|
777
840
|
const tokens = tokenizeInline(text, 0, text.length);
|
|
@@ -1545,16 +1608,17 @@ function validateLocalSync(entries, plan) {
|
|
|
1545
1608
|
const { html, mermaidBlocks } = markdownToStorage(markdown, {
|
|
1546
1609
|
renderHtmlBlocks: plan.renderHtmlBlocks
|
|
1547
1610
|
});
|
|
1611
|
+
const body = appendRepositoryNotice(html, plan.repositoryUrl);
|
|
1548
1612
|
const markdownDir = dirname(entry.absolute);
|
|
1549
|
-
const hasLocalImages = hasLocalImagePlaceholder(
|
|
1613
|
+
const hasLocalImages = hasLocalImagePlaceholder(body);
|
|
1550
1614
|
const hasMermaidBlocks = mermaidBlocks.length > 0;
|
|
1551
|
-
const attachments = hasLocalImages ? validateAttachmentSources(
|
|
1615
|
+
const attachments = hasLocalImages ? validateAttachmentSources(body, {
|
|
1552
1616
|
markdownDir,
|
|
1553
1617
|
allowedRoot: plan.folder
|
|
1554
1618
|
}) : [];
|
|
1555
1619
|
plans.push({
|
|
1556
1620
|
entry,
|
|
1557
|
-
html,
|
|
1621
|
+
html: body,
|
|
1558
1622
|
mermaidBlocks,
|
|
1559
1623
|
markdownDir,
|
|
1560
1624
|
hasLocalImages,
|
|
@@ -1611,6 +1675,12 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1611
1675
|
throw new Error(`parentPageId must be numeric, got: ${options.parentPageId}`);
|
|
1612
1676
|
}
|
|
1613
1677
|
}
|
|
1678
|
+
const repositoryUrl = options.repositoryUrl ? repositoryNoticeUrl(options.repositoryUrl, cwd, options.folder ?? "") : "";
|
|
1679
|
+
if (repositoryUrl && !isAllowedUrl(repositoryUrl)) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
"repositoryUrl must be an http(s), protocol-relative, server-relative, mailto, tel, or relative URL"
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1614
1684
|
return {
|
|
1615
1685
|
cwd,
|
|
1616
1686
|
folder,
|
|
@@ -1622,7 +1692,8 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1622
1692
|
versionMessage: options.versionMessage ?? "Synced via repo-toolkit-confluence",
|
|
1623
1693
|
skipUnchanged: options.skipUnchanged ?? true,
|
|
1624
1694
|
dryRun: options.dryRun ?? false,
|
|
1625
|
-
renderHtmlBlocks: options.renderHtmlBlocks === true
|
|
1695
|
+
renderHtmlBlocks: options.renderHtmlBlocks === true,
|
|
1696
|
+
repositoryUrl
|
|
1626
1697
|
};
|
|
1627
1698
|
}
|
|
1628
1699
|
async function syncConfluenceToDocs(options = {}) {
|
|
@@ -1818,6 +1889,36 @@ function hasLocalImagePlaceholder(html) {
|
|
|
1818
1889
|
LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
|
|
1819
1890
|
return LOCAL_IMAGE_PLACEHOLDER_RE.test(html);
|
|
1820
1891
|
}
|
|
1892
|
+
function appendRepositoryNotice(html, repositoryUrl) {
|
|
1893
|
+
if (repositoryUrl.length === 0) {
|
|
1894
|
+
return html;
|
|
1895
|
+
}
|
|
1896
|
+
const url = escapeXmlAttribute(repositoryUrl);
|
|
1897
|
+
const text = escapeHtml(repositoryUrl);
|
|
1898
|
+
return html + '\n<p><em>This document is synced from repository <a href="' + url + '">' + text + "</a>.</em></p>";
|
|
1899
|
+
}
|
|
1900
|
+
function repositoryNoticeUrl(repositoryUrl, cwd, folder) {
|
|
1901
|
+
const folderPath = repositoryFolderPath(cwd, folder);
|
|
1902
|
+
if (folderPath.length === 0) {
|
|
1903
|
+
return repositoryUrl;
|
|
1904
|
+
}
|
|
1905
|
+
const trimmedUrl = repositoryUrl.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
1906
|
+
const encodedFolder = folderPath.split("/").map(encodeURIComponent).join("/");
|
|
1907
|
+
if (/^https?:\/\/github\.com\//i.test(trimmedUrl) && !/\/tree\/|\/blob\//.test(trimmedUrl)) {
|
|
1908
|
+
return trimmedUrl + "/tree/HEAD/" + encodedFolder;
|
|
1909
|
+
}
|
|
1910
|
+
return trimmedUrl + "/" + encodedFolder;
|
|
1911
|
+
}
|
|
1912
|
+
function repositoryFolderPath(cwd, folder) {
|
|
1913
|
+
if (folder.length === 0) {
|
|
1914
|
+
return "";
|
|
1915
|
+
}
|
|
1916
|
+
const relativePath = relative3(cwd, resolveInputPath(cwd, folder));
|
|
1917
|
+
if (relativePath === "" || relativePath.startsWith("..") || isAbsolute2(relativePath)) {
|
|
1918
|
+
return "";
|
|
1919
|
+
}
|
|
1920
|
+
return relativePath.split(/[\\/]+/).filter((part) => part !== "" && part !== ".").join("/");
|
|
1921
|
+
}
|
|
1821
1922
|
async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
|
|
1822
1923
|
let predicted = html;
|
|
1823
1924
|
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.16.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.16.0"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"**/*",
|