@repo-toolkit/confluence 0.14.2 → 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 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
@@ -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(html);
1626
+ const hasLocalImages = hasLocalImagePlaceholder(body);
1563
1627
  const hasMermaidBlocks = mermaidBlocks.length > 0;
1564
- const attachments = hasLocalImages ? validateAttachmentSources(html, {
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,11 @@ function resolveConfluenceSyncPlan(options = {}) {
1624
1688
  throw new Error(`parentPageId must be numeric, got: ${options.parentPageId}`);
1625
1689
  }
1626
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
+ }
1627
1696
  return {
1628
1697
  cwd,
1629
1698
  folder,
@@ -1635,7 +1704,8 @@ function resolveConfluenceSyncPlan(options = {}) {
1635
1704
  versionMessage: options.versionMessage ?? "Synced via repo-toolkit-confluence",
1636
1705
  skipUnchanged: options.skipUnchanged ?? true,
1637
1706
  dryRun: options.dryRun ?? false,
1638
- renderHtmlBlocks: options.renderHtmlBlocks === true
1707
+ renderHtmlBlocks: options.renderHtmlBlocks === true,
1708
+ repositoryUrl: options.repositoryUrl ?? ""
1639
1709
  };
1640
1710
  }
1641
1711
  async function syncConfluenceToDocs(options = {}) {
@@ -1831,6 +1901,14 @@ function hasLocalImagePlaceholder(html) {
1831
1901
  LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
1832
1902
  return LOCAL_IMAGE_PLACEHOLDER_RE.test(html);
1833
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
+ }
1834
1912
  async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
1835
1913
  let predicted = html;
1836
1914
  if (ctx.hasMermaidBlocks) {
@@ -1884,6 +1962,7 @@ var SPECS = [
1884
1962
  { name: "space-key" },
1885
1963
  { name: "parent-page-id" },
1886
1964
  { name: "version-message" },
1965
+ { name: "repository-url" },
1887
1966
  { name: "skip-unchanged", boolean: true, negatable: true },
1888
1967
  { name: "dry-run", boolean: true },
1889
1968
  { name: "render-html-blocks", boolean: true },
@@ -1928,6 +2007,7 @@ Environment variables (CLI form; GitHub Action INPUT_* form is also read):
1928
2007
  CONFLUENCE_SPACE_KEY Confluence space key
1929
2008
  CONFLUENCE_PARENT_PAGE_ID Numeric parent page id
1930
2009
  CONFLUENCE_VERSION_MESSAGE Version-message suffix for every PUT
2010
+ CONFLUENCE_REPOSITORY_URL Repository URL appended to synced pages
1931
2011
  CONFLUENCE_SKIP_UNCHANGED true|false (default: true)
1932
2012
  CONFLUENCE_DRY_RUN true|false (default: false)
1933
2013
  CONFLUENCE_RENDER_HTML_BLOCKS true|false (default: false)
@@ -1947,6 +2027,7 @@ Options:
1947
2027
  --space-key <key> Confluence space key (required). Resolved to a spaceId via the API
1948
2028
  --parent-page-id <id> Numeric page id under which docs will be published (required)
1949
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
1950
2031
  --skip-unchanged Skip pages whose body is unchanged (default: true)
1951
2032
  --no-skip-unchanged Re-upload every page even when unchanged
1952
2033
  --dry-run Walk the doc tree and print the plan without API calls
@@ -1965,7 +2046,8 @@ var STRING_OPTION_KEYS = [
1965
2046
  "baseUrl",
1966
2047
  "spaceKey",
1967
2048
  "parentPageId",
1968
- "versionMessage"
2049
+ "versionMessage",
2050
+ "repositoryUrl"
1969
2051
  ];
1970
2052
  function setIfString(options, key, value) {
1971
2053
  if (typeof value === "string" && value.length > 0) {
@@ -1986,6 +2068,7 @@ var ENV_BINDINGS = [
1986
2068
  { envName: "INPUT_SPACE-KEY", key: "spaceKey", kind: "string" },
1987
2069
  { envName: "INPUT_PARENT-PAGE-ID", key: "parentPageId", kind: "string" },
1988
2070
  { envName: "INPUT_VERSION-MESSAGE", key: "versionMessage", kind: "string" },
2071
+ { envName: "INPUT_REPOSITORY-URL", key: "repositoryUrl", kind: "string" },
1989
2072
  { envName: "INPUT_DRY-RUN", key: "dryRun", kind: "boolean" },
1990
2073
  { envName: "INPUT_SKIP-UNCHANGED", key: "skipUnchanged", kind: "boolean" },
1991
2074
  { envName: "INPUT_RENDER-HTML-BLOCKS", key: "renderHtmlBlocks", kind: "boolean" },
@@ -1997,6 +2080,7 @@ var ENV_BINDINGS = [
1997
2080
  { envName: "CONFLUENCE_SPACE_KEY", key: "spaceKey", kind: "string" },
1998
2081
  { envName: "CONFLUENCE_PARENT_PAGE_ID", key: "parentPageId", kind: "string" },
1999
2082
  { envName: "CONFLUENCE_VERSION_MESSAGE", key: "versionMessage", kind: "string" },
2083
+ { envName: "CONFLUENCE_REPOSITORY_URL", key: "repositoryUrl", kind: "string" },
2000
2084
  { envName: "CONFLUENCE_DRY_RUN", key: "dryRun", kind: "boolean" },
2001
2085
  { envName: "CONFLUENCE_SKIP_UNCHANGED", key: "skipUnchanged", kind: "boolean" },
2002
2086
  { envName: "CONFLUENCE_RENDER_HTML_BLOCKS", key: "renderHtmlBlocks", kind: "boolean" }
@@ -2017,8 +2101,22 @@ function optionsFromEnv(env = process.env) {
2017
2101
  options[key] = parseBooleanEnv(raw, envName);
2018
2102
  }
2019
2103
  }
2104
+ if (options.repositoryUrl === void 0) {
2105
+ const githubRepositoryUrl = repositoryUrlFromGitHubEnv(env);
2106
+ if (githubRepositoryUrl) {
2107
+ options.repositoryUrl = githubRepositoryUrl;
2108
+ }
2109
+ }
2020
2110
  return options;
2021
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
+ }
2022
2120
  function buildOptions(result) {
2023
2121
  if (!result) {
2024
2122
  return {};
@@ -2034,6 +2132,7 @@ function buildOptions(result) {
2034
2132
  setIfString(options, "spaceKey", values["space-key"]);
2035
2133
  setIfString(options, "parentPageId", values["parent-page-id"]);
2036
2134
  setIfString(options, "versionMessage", values["version-message"]);
2135
+ setIfString(options, "repositoryUrl", values["repository-url"]);
2037
2136
  if (values["skip-unchanged"] !== void 0) {
2038
2137
  options.skipUnchanged = values["skip-unchanged"] === "true";
2039
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
@@ -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(html);
1613
+ const hasLocalImages = hasLocalImagePlaceholder(body);
1550
1614
  const hasMermaidBlocks = mermaidBlocks.length > 0;
1551
- const attachments = hasLocalImages ? validateAttachmentSources(html, {
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,11 @@ function resolveConfluenceSyncPlan(options = {}) {
1611
1675
  throw new Error(`parentPageId must be numeric, got: ${options.parentPageId}`);
1612
1676
  }
1613
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
+ }
1614
1683
  return {
1615
1684
  cwd,
1616
1685
  folder,
@@ -1622,7 +1691,8 @@ function resolveConfluenceSyncPlan(options = {}) {
1622
1691
  versionMessage: options.versionMessage ?? "Synced via repo-toolkit-confluence",
1623
1692
  skipUnchanged: options.skipUnchanged ?? true,
1624
1693
  dryRun: options.dryRun ?? false,
1625
- renderHtmlBlocks: options.renderHtmlBlocks === true
1694
+ renderHtmlBlocks: options.renderHtmlBlocks === true,
1695
+ repositoryUrl: options.repositoryUrl ?? ""
1626
1696
  };
1627
1697
  }
1628
1698
  async function syncConfluenceToDocs(options = {}) {
@@ -1818,6 +1888,14 @@ function hasLocalImagePlaceholder(html) {
1818
1888
  LOCAL_IMAGE_PLACEHOLDER_RE.lastIndex = 0;
1819
1889
  return LOCAL_IMAGE_PLACEHOLDER_RE.test(html);
1820
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
+ }
1821
1899
  async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
1822
1900
  let predicted = html;
1823
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.14.2",
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.14.2"
30
+ "@repo-toolkit/publish-package": "0.15.0"
31
31
  },
32
32
  "files": [
33
33
  "**/*",