@repo-toolkit/confluence 0.19.0 → 0.20.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 +40 -14
- package/cli.js +83 -8
- package/index.d.ts +21 -1
- package/index.js +74 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ Flags:
|
|
|
76
76
|
italic source notice. The `--folder` path is added to the notice link when it
|
|
77
77
|
is relative to `--cwd`. When omitted, GitHub Actions runs infer this from
|
|
78
78
|
`GITHUB_SERVER_URL` and `GITHUB_REPOSITORY`.
|
|
79
|
+
- `--page-title-strategy <value>` — leaf page title strategy (default: `filename-stem`). One of `filename-stem`, `filename`, `sentence-case-parent`, `sentence-case-parents`, `sentence-case-path`. Applied only to Markdown leaf pages; folder-generated parent pages keep their raw directory-segment titles. See [leaf page title strategies](#leaf-page-title-strategies).
|
|
79
80
|
- `--skip-unchanged` / `--no-skip-unchanged` — skip pages whose body is unchanged (default: `skip`)
|
|
80
81
|
- `--dry-run` — walk the doc tree and validate every markdown file and local
|
|
81
82
|
image source (same preflight as a real sync) then log the plan. No API
|
|
@@ -93,26 +94,49 @@ both read for every option. Boolean env values accept `true|1|yes|on` /
|
|
|
93
94
|
`false|0|no|off` (empty string is falsy); any other value exits nonzero with
|
|
94
95
|
`Invalid boolean value for <ENV_NAME>: <raw>`.
|
|
95
96
|
|
|
96
|
-
| Option | `CONFLUENCE_*`
|
|
97
|
-
| ----------------------- |
|
|
98
|
-
| folder | `CONFLUENCE_FOLDER`
|
|
99
|
-
| username | `CONFLUENCE_USERNAME`
|
|
100
|
-
| apiToken | `CONFLUENCE_API_TOKEN`
|
|
101
|
-
| apiTokenFile | `CONFLUENCE_API_TOKEN_FILE`
|
|
102
|
-
| baseUrl | `CONFLUENCE_BASE_URL`
|
|
103
|
-
| spaceKey | `CONFLUENCE_SPACE_KEY`
|
|
104
|
-
| parentPageId | `CONFLUENCE_PARENT_PAGE_ID`
|
|
105
|
-
| versionMessage | `CONFLUENCE_VERSION_MESSAGE`
|
|
106
|
-
| repositoryUrl | `CONFLUENCE_REPOSITORY_URL`
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
97
|
+
| Option | `CONFLUENCE_*` | `INPUT_*` (Actions form) |
|
|
98
|
+
| ----------------------- | -------------------------------- | --------------------------------------------- |
|
|
99
|
+
| folder | `CONFLUENCE_FOLDER` | `INPUT_FOLDER` |
|
|
100
|
+
| username | `CONFLUENCE_USERNAME` | `INPUT_USERNAME` |
|
|
101
|
+
| apiToken | `CONFLUENCE_API_TOKEN` | `INPUT_API-TOKEN`, `INPUT_PASSWORD` |
|
|
102
|
+
| apiTokenFile | `CONFLUENCE_API_TOKEN_FILE` | `INPUT_API-TOKEN-FILE`, `INPUT_PASSWORD-FILE` |
|
|
103
|
+
| baseUrl | `CONFLUENCE_BASE_URL` | `INPUT_CONFLUENCE-BASE-URL` |
|
|
104
|
+
| spaceKey | `CONFLUENCE_SPACE_KEY` | `INPUT_SPACE-KEY` |
|
|
105
|
+
| parentPageId | `CONFLUENCE_PARENT_PAGE_ID` | `INPUT_PARENT-PAGE-ID` |
|
|
106
|
+
| versionMessage | `CONFLUENCE_VERSION_MESSAGE` | `INPUT_VERSION-MESSAGE` |
|
|
107
|
+
| repositoryUrl | `CONFLUENCE_REPOSITORY_URL` | `INPUT_REPOSITORY-URL` |
|
|
108
|
+
| pageTitleStrategy | `CONFLUENCE_PAGE_TITLE_STRATEGY` | `INPUT_PAGE-TITLE-STRATEGY` |
|
|
109
|
+
| skipUnchanged (bool) | `CONFLUENCE_SKIP_UNCHANGED` | `INPUT_SKIP-UNCHANGED` |
|
|
110
|
+
| dryRun (bool) | `CONFLUENCE_DRY_RUN` | `INPUT_DRY-RUN` |
|
|
111
|
+
| renderHtmlBlocks (bool) | `CONFLUENCE_RENDER_HTML_BLOCKS` | `INPUT_RENDER-HTML-BLOCKS` |
|
|
110
112
|
|
|
111
113
|
Errors, `--help`, and all log lines never print the supplied token value. The
|
|
112
114
|
secret-file loader wraps fs errors via `Error.cause` (`Failed to read
|
|
113
115
|
apiTokenFile at <path>`, `apiTokenFile at <path> is empty`) without revealing
|
|
114
116
|
the file contents.
|
|
115
117
|
|
|
118
|
+
## Leaf page title strategies
|
|
119
|
+
|
|
120
|
+
Leaf Confluence page titles are derived from the Markdown file's relative path segments. Directory segments become synthetic parent-page titles and retain their raw segment text for every strategy — only the Markdown leaf file uses `pageTitleStrategy`.
|
|
121
|
+
|
|
122
|
+
| Strategy | Behavior | Example for `community-nodes/cdogs-document-generator/credentials.md` |
|
|
123
|
+
| ------------------------- | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
|
124
|
+
| `filename-stem` (default) | Original filename without the final `.md` extension | `credentials` |
|
|
125
|
+
| `filename` | Original filename including extension | `credentials.md` |
|
|
126
|
+
| `sentence-case-parent` | Sentence-case filename stem plus immediate parent folder | `Credentials (cdogs-document-generator)` |
|
|
127
|
+
| `sentence-case-parents` | Sentence-case filename stem plus all parent folders | `Credentials (community-nodes/cdogs-document-generator)` |
|
|
128
|
+
| `sentence-case-path` | Sentence-case filename stem plus all parent folders and original filename including extension | `Credentials (community-nodes/cdogs-document-generator/credentials.md)` |
|
|
129
|
+
|
|
130
|
+
Sentence case is deterministic and dependency-free: remove only the final case-insensitive `.md`; replace each run of `-` and `_` with one space; trim; lowercase ASCII letters; uppercase the first ASCII letter. Digits and other punctuation are preserved. Examples: `failed-deployment.md` → `Failed deployment`, `n8n_setup.md` → `N8n setup`, `README.md` → `Readme`. Parent folders are the segments before the filename, joined with `/` exactly as stored. Preserve the original filename and extension casing in `filename` and inside `sentence-case-path` (for example, `Guide.MD` remains `Guide.MD` where the original filename is included).
|
|
131
|
+
|
|
132
|
+
Root-file behavior: for `sentence-case-parent` and `sentence-case-parents` the parentheses are omitted when a file is directly under the documentation root (`overview.md` → `Overview`). For `sentence-case-path`, the root parenthesized path is the filename itself (`overview.md` → `Overview (overview.md)`).
|
|
133
|
+
|
|
134
|
+
### Migration and uniqueness notes
|
|
135
|
+
|
|
136
|
+
> **Changing the strategy changes title-based identity.** Sync is additive and non-pruning and looks up Confluence pages by title under parent. Switching the strategy therefore seeks a new title and may **create a new page while leaving the old page untouched** — it does not rename, move, or delete existing pages. Operators must manually rename, delete, or archive old generated pages (or migrate them) before switching strategies in production.
|
|
137
|
+
|
|
138
|
+
Path-based strategies reduce predictable local collisions (for example, repeated basenames such as `credentials.md` or `README.md` in separate subtrees) but cannot guarantee uniqueness against unrelated or manually created pages already present in the target Confluence space. Existing spaces may still cause Confluence API conflicts if a generated title collides with an unrelated page under the same parent. Do not truncate or hash titles locally; if Confluence imposes a remote title-length limit, the existing API error behavior applies.
|
|
139
|
+
|
|
116
140
|
## JavaScript API
|
|
117
141
|
|
|
118
142
|
```ts
|
|
@@ -126,6 +150,7 @@ await syncConfluenceToDocs({
|
|
|
126
150
|
spaceKey: 'ENG',
|
|
127
151
|
parentPageId: '123456789',
|
|
128
152
|
versionMessage: 'chore(docs): sync',
|
|
153
|
+
pageTitleStrategy: 'sentence-case-parents', // optional; default: 'filename-stem'
|
|
129
154
|
});
|
|
130
155
|
```
|
|
131
156
|
|
|
@@ -185,6 +210,7 @@ inputs:
|
|
|
185
210
|
space-key: { required: true }
|
|
186
211
|
parent-page-id: { required: true }
|
|
187
212
|
version-message: { required: false }
|
|
213
|
+
page-title-strategy: { required: false, default: 'filename-stem' }
|
|
188
214
|
dry-run: { required: false, default: 'false' }
|
|
189
215
|
skip-unchanged: { required: false, default: 'true' }
|
|
190
216
|
render-html-blocks: { required: false, default: 'false' }
|
package/cli.js
CHANGED
|
@@ -588,6 +588,64 @@ function titleFromSegment(segment) {
|
|
|
588
588
|
}
|
|
589
589
|
return segment;
|
|
590
590
|
}
|
|
591
|
+
var PAGE_TITLE_STRATEGIES = [
|
|
592
|
+
"filename-stem",
|
|
593
|
+
"filename",
|
|
594
|
+
"sentence-case-parent",
|
|
595
|
+
"sentence-case-parents",
|
|
596
|
+
"sentence-case-path"
|
|
597
|
+
];
|
|
598
|
+
var DEFAULT_PAGE_TITLE_STRATEGY = "filename-stem";
|
|
599
|
+
function resolvePageTitleStrategy(value) {
|
|
600
|
+
if (value === void 0 || value === null) {
|
|
601
|
+
return DEFAULT_PAGE_TITLE_STRATEGY;
|
|
602
|
+
}
|
|
603
|
+
if (typeof value === "string" && PAGE_TITLE_STRATEGIES.includes(value)) {
|
|
604
|
+
return value;
|
|
605
|
+
}
|
|
606
|
+
throw new Error(
|
|
607
|
+
`Invalid pageTitleStrategy: expected one of ${PAGE_TITLE_STRATEGIES.join(", ")}, got ${JSON.stringify(value)}`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
function sentenceCaseStem(filename) {
|
|
611
|
+
let stem = filename;
|
|
612
|
+
if (/\.md$/i.test(stem)) {
|
|
613
|
+
stem = stem.slice(0, stem.length - MARKDOWN_EXT.length);
|
|
614
|
+
}
|
|
615
|
+
let out = stem.replace(/[-_]+/g, " ").trim().replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
616
|
+
const firstLetter = /[a-z]/.exec(out);
|
|
617
|
+
if (firstLetter) {
|
|
618
|
+
const i = firstLetter.index;
|
|
619
|
+
out = out.slice(0, i) + out.charAt(i).toUpperCase() + out.slice(i + 1);
|
|
620
|
+
}
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
function pageTitleFromSegments(segments, strategy) {
|
|
624
|
+
const filename = segments.length > 0 ? segments[segments.length - 1] : "";
|
|
625
|
+
const parents = segments.slice(0, segments.length - 1);
|
|
626
|
+
switch (strategy) {
|
|
627
|
+
case "filename-stem":
|
|
628
|
+
return titleFromSegment(filename);
|
|
629
|
+
case "filename":
|
|
630
|
+
return filename;
|
|
631
|
+
case "sentence-case-parent": {
|
|
632
|
+
const title = sentenceCaseStem(filename);
|
|
633
|
+
if (parents.length === 0) {
|
|
634
|
+
return title;
|
|
635
|
+
}
|
|
636
|
+
return `${title} (${parents[parents.length - 1]})`;
|
|
637
|
+
}
|
|
638
|
+
case "sentence-case-parents": {
|
|
639
|
+
const title = sentenceCaseStem(filename);
|
|
640
|
+
if (parents.length === 0) {
|
|
641
|
+
return title;
|
|
642
|
+
}
|
|
643
|
+
return `${title} (${parents.join("/")})`;
|
|
644
|
+
}
|
|
645
|
+
case "sentence-case-path":
|
|
646
|
+
return `${sentenceCaseStem(filename)} (${[...parents, filename].join("/")})`;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
591
649
|
|
|
592
650
|
// src/markdown.ts
|
|
593
651
|
var STORAGE_LINE_BREAK = "<br />";
|
|
@@ -1631,6 +1689,7 @@ function validateLocalSync(entries, plan) {
|
|
|
1631
1689
|
}) : [];
|
|
1632
1690
|
plans.push({
|
|
1633
1691
|
entry,
|
|
1692
|
+
title: pageTitleFromSegments(entry.segments, plan.pageTitleStrategy),
|
|
1634
1693
|
html: body,
|
|
1635
1694
|
mermaidBlocks,
|
|
1636
1695
|
markdownDir,
|
|
@@ -1660,6 +1719,7 @@ var SyncMutationError = class extends Error {
|
|
|
1660
1719
|
}
|
|
1661
1720
|
};
|
|
1662
1721
|
function resolveConfluenceSyncPlan(options = {}) {
|
|
1722
|
+
const pageTitleStrategy = resolvePageTitleStrategy(options.pageTitleStrategy);
|
|
1663
1723
|
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
1664
1724
|
const folder = resolveInputPath(cwd, options.folder ?? "");
|
|
1665
1725
|
if (!options.folder) {
|
|
@@ -1706,7 +1766,8 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1706
1766
|
skipUnchanged: options.skipUnchanged ?? true,
|
|
1707
1767
|
dryRun: options.dryRun ?? false,
|
|
1708
1768
|
renderHtmlBlocks: options.renderHtmlBlocks === true,
|
|
1709
|
-
repositoryUrl
|
|
1769
|
+
repositoryUrl,
|
|
1770
|
+
pageTitleStrategy
|
|
1710
1771
|
};
|
|
1711
1772
|
}
|
|
1712
1773
|
async function syncConfluenceToDocs(options = {}) {
|
|
@@ -1717,15 +1778,15 @@ async function syncConfluenceToDocs(options = {}) {
|
|
|
1717
1778
|
log(`No markdown files found under ${plan.folder}`);
|
|
1718
1779
|
return;
|
|
1719
1780
|
}
|
|
1720
|
-
validateLocalHierarchy(tree.entries);
|
|
1721
1781
|
const localPlan = validateLocalSync(tree.entries, plan);
|
|
1782
|
+
validateLocalHierarchy(localPlan.entries, plan.pageTitleStrategy);
|
|
1722
1783
|
if (plan.dryRun) {
|
|
1723
1784
|
log("[dry-run] Walking documentation tree only.");
|
|
1724
1785
|
for (const entryPlan of localPlan.entries) {
|
|
1725
1786
|
const attCount = entryPlan.attachments.length;
|
|
1726
1787
|
const mermaidCount = entryPlan.mermaidBlocks.length;
|
|
1727
1788
|
log(
|
|
1728
|
-
`[dry-run] would sync ${entryPlan.entry.segments.join("/")}` + (attCount > 0 ? ` (${attCount} attachment${attCount === 1 ? "" : "s"} validated)` : "") + (mermaidCount > 0 ? ` (${mermaidCount} mermaid block${mermaidCount === 1 ? "" : "s"})` : "")
|
|
1789
|
+
`[dry-run] would sync ${entryPlan.entry.segments.join("/")} as "${entryPlan.title}"` + (attCount > 0 ? ` (${attCount} attachment${attCount === 1 ? "" : "s"} validated)` : "") + (mermaidCount > 0 ? ` (${mermaidCount} mermaid block${mermaidCount === 1 ? "" : "s"})` : "")
|
|
1729
1790
|
);
|
|
1730
1791
|
}
|
|
1731
1792
|
return;
|
|
@@ -1763,7 +1824,7 @@ async function syncEntry(entryPlan, plan, client, cache, log, changes) {
|
|
|
1763
1824
|
const isLast = idx === segments.length - 1;
|
|
1764
1825
|
const segment = segments[idx] ?? "";
|
|
1765
1826
|
if (isLast && isMarkdownName(segment)) {
|
|
1766
|
-
const title =
|
|
1827
|
+
const title = entryPlan.title;
|
|
1767
1828
|
const leafNeedsUploads = hasLocalImages || hasMermaidBlocks;
|
|
1768
1829
|
const existing = await cache.find(title, currentParentId);
|
|
1769
1830
|
if (!existing && !leafNeedsUploads) {
|
|
@@ -1822,7 +1883,7 @@ async function syncEntry(entryPlan, plan, client, cache, log, changes) {
|
|
|
1822
1883
|
return;
|
|
1823
1884
|
}
|
|
1824
1885
|
if (isMarkdownName(segment)) {
|
|
1825
|
-
const title =
|
|
1886
|
+
const title = pageTitleFromSegments(segments.slice(0, idx + 1), plan.pageTitleStrategy);
|
|
1826
1887
|
const page2 = await cache.findOrCreate(title, currentParentId);
|
|
1827
1888
|
currentParentId = page2.id;
|
|
1828
1889
|
continue;
|
|
@@ -1953,14 +2014,15 @@ async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
|
|
|
1953
2014
|
}
|
|
1954
2015
|
return predicted;
|
|
1955
2016
|
}
|
|
1956
|
-
function validateLocalHierarchy(entries) {
|
|
2017
|
+
function validateLocalHierarchy(entries, strategy) {
|
|
1957
2018
|
const seen = /* @__PURE__ */ new Map();
|
|
1958
|
-
for (const
|
|
2019
|
+
for (const entryPlan of entries) {
|
|
2020
|
+
const { entry } = entryPlan;
|
|
1959
2021
|
let parentKey = "";
|
|
1960
2022
|
for (let index = 0; index < entry.segments.length; index += 1) {
|
|
1961
2023
|
const segment = entry.segments[index] ?? "";
|
|
1962
2024
|
const isLast = index === entry.segments.length - 1;
|
|
1963
|
-
const title = isMarkdownName(segment) ?
|
|
2025
|
+
const title = isMarkdownName(segment) ? isLast ? entryPlan.title : pageTitleFromSegments(entry.segments.slice(0, index + 1), strategy) : segment;
|
|
1964
2026
|
const kind = isLast && isMarkdownName(segment) ? "file" : "dir";
|
|
1965
2027
|
const key = `${parentKey}::${title}`;
|
|
1966
2028
|
const existing = seen.get(key);
|
|
@@ -1986,6 +2048,7 @@ var SPECS = [
|
|
|
1986
2048
|
{ name: "parent-page-id" },
|
|
1987
2049
|
{ name: "version-message" },
|
|
1988
2050
|
{ name: "repository-url" },
|
|
2051
|
+
{ name: "page-title-strategy" },
|
|
1989
2052
|
{ name: "skip-unchanged", boolean: true, negatable: true },
|
|
1990
2053
|
{ name: "dry-run", boolean: true },
|
|
1991
2054
|
{ name: "render-html-blocks", boolean: true },
|
|
@@ -2031,6 +2094,7 @@ Environment variables (CLI form; GitHub Action INPUT_* form is also read):
|
|
|
2031
2094
|
CONFLUENCE_PARENT_PAGE_ID Numeric parent page id
|
|
2032
2095
|
CONFLUENCE_VERSION_MESSAGE Version-message suffix for every PUT
|
|
2033
2096
|
CONFLUENCE_REPOSITORY_URL Repository URL appended to synced pages
|
|
2097
|
+
CONFLUENCE_PAGE_TITLE_STRATEGY Leaf page title strategy (default: filename-stem)
|
|
2034
2098
|
CONFLUENCE_SKIP_UNCHANGED true|false (default: true)
|
|
2035
2099
|
CONFLUENCE_DRY_RUN true|false (default: false)
|
|
2036
2100
|
CONFLUENCE_RENDER_HTML_BLOCKS true|false (default: false)
|
|
@@ -2051,6 +2115,8 @@ Options:
|
|
|
2051
2115
|
--parent-page-id <id> Numeric page id under which docs will be published (required)
|
|
2052
2116
|
--version-message <text> Commit message appended to every page/attachment PUT
|
|
2053
2117
|
--repository-url <url> Repository URL appended to synced pages as an italic notice
|
|
2118
|
+
--page-title-strategy <value> Leaf page title strategy: filename-stem (default), filename,
|
|
2119
|
+
sentence-case-parent, sentence-case-parents, sentence-case-path
|
|
2054
2120
|
--skip-unchanged Skip pages whose body is unchanged (default: true)
|
|
2055
2121
|
--no-skip-unchanged Re-upload every page even when unchanged
|
|
2056
2122
|
--dry-run Walk the doc tree and print the plan without API calls
|
|
@@ -2092,6 +2158,7 @@ var ENV_BINDINGS = [
|
|
|
2092
2158
|
{ envName: "INPUT_PARENT-PAGE-ID", key: "parentPageId", kind: "string" },
|
|
2093
2159
|
{ envName: "INPUT_VERSION-MESSAGE", key: "versionMessage", kind: "string" },
|
|
2094
2160
|
{ envName: "INPUT_REPOSITORY-URL", key: "repositoryUrl", kind: "string" },
|
|
2161
|
+
{ envName: "INPUT_PAGE-TITLE-STRATEGY", key: "pageTitleStrategy", kind: "string" },
|
|
2095
2162
|
{ envName: "INPUT_DRY-RUN", key: "dryRun", kind: "boolean" },
|
|
2096
2163
|
{ envName: "INPUT_SKIP-UNCHANGED", key: "skipUnchanged", kind: "boolean" },
|
|
2097
2164
|
{ envName: "INPUT_RENDER-HTML-BLOCKS", key: "renderHtmlBlocks", kind: "boolean" },
|
|
@@ -2104,6 +2171,7 @@ var ENV_BINDINGS = [
|
|
|
2104
2171
|
{ envName: "CONFLUENCE_PARENT_PAGE_ID", key: "parentPageId", kind: "string" },
|
|
2105
2172
|
{ envName: "CONFLUENCE_VERSION_MESSAGE", key: "versionMessage", kind: "string" },
|
|
2106
2173
|
{ envName: "CONFLUENCE_REPOSITORY_URL", key: "repositoryUrl", kind: "string" },
|
|
2174
|
+
{ envName: "CONFLUENCE_PAGE_TITLE_STRATEGY", key: "pageTitleStrategy", kind: "string" },
|
|
2107
2175
|
{ envName: "CONFLUENCE_DRY_RUN", key: "dryRun", kind: "boolean" },
|
|
2108
2176
|
{ envName: "CONFLUENCE_SKIP_UNCHANGED", key: "skipUnchanged", kind: "boolean" },
|
|
2109
2177
|
{ envName: "CONFLUENCE_RENDER_HTML_BLOCKS", key: "renderHtmlBlocks", kind: "boolean" }
|
|
@@ -2116,6 +2184,10 @@ function optionsFromEnv(env = process.env) {
|
|
|
2116
2184
|
continue;
|
|
2117
2185
|
}
|
|
2118
2186
|
if (kind === "string") {
|
|
2187
|
+
if (key === "pageTitleStrategy") {
|
|
2188
|
+
options[key] = raw;
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2119
2191
|
if (!isStringOptionKey(key)) {
|
|
2120
2192
|
continue;
|
|
2121
2193
|
}
|
|
@@ -2156,6 +2228,9 @@ function buildOptions(result) {
|
|
|
2156
2228
|
setIfString(options, "parentPageId", values["parent-page-id"]);
|
|
2157
2229
|
setIfString(options, "versionMessage", values["version-message"]);
|
|
2158
2230
|
setIfString(options, "repositoryUrl", values["repository-url"]);
|
|
2231
|
+
if (values["page-title-strategy"] !== void 0) {
|
|
2232
|
+
options.pageTitleStrategy = values["page-title-strategy"];
|
|
2233
|
+
}
|
|
2159
2234
|
if (values["skip-unchanged"] !== void 0) {
|
|
2160
2235
|
options.skipUnchanged = values["skip-unchanged"] === "true";
|
|
2161
2236
|
}
|
package/index.d.ts
CHANGED
|
@@ -167,6 +167,11 @@ interface DocTree {
|
|
|
167
167
|
declare function readDocTree(root: string, depth?: number): Promise<DocTree>;
|
|
168
168
|
declare function isMarkdownName(name: string): boolean;
|
|
169
169
|
declare function titleFromSegment(segment: string): string;
|
|
170
|
+
declare const PAGE_TITLE_STRATEGIES: readonly ["filename-stem", "filename", "sentence-case-parent", "sentence-case-parents", "sentence-case-path"];
|
|
171
|
+
type PageTitleStrategy = (typeof PAGE_TITLE_STRATEGIES)[number];
|
|
172
|
+
declare const DEFAULT_PAGE_TITLE_STRATEGY: PageTitleStrategy;
|
|
173
|
+
declare function resolvePageTitleStrategy(value: unknown): PageTitleStrategy;
|
|
174
|
+
declare function pageTitleFromSegments(segments: readonly string[], strategy: PageTitleStrategy): string;
|
|
170
175
|
|
|
171
176
|
interface RewriteResult {
|
|
172
177
|
html: string;
|
|
@@ -336,6 +341,12 @@ interface ConfluenceSyncOptions {
|
|
|
336
341
|
* render/upload work is performed.
|
|
337
342
|
*/
|
|
338
343
|
skipUnchanged?: boolean;
|
|
344
|
+
/**
|
|
345
|
+
* Leaf page title strategy (default: `filename-stem`). Applied only to
|
|
346
|
+
* Markdown leaf pages; folder-generated parent pages keep their raw
|
|
347
|
+
* directory-segment titles. See {@link PAGE_TITLE_STRATEGIES}.
|
|
348
|
+
*/
|
|
349
|
+
pageTitleStrategy?: PageTitleStrategy;
|
|
339
350
|
/** Render ```html fenced blocks as inline HTML via the Confluence `html` macro instead of a code box (default: false). */
|
|
340
351
|
renderHtmlBlocks?: boolean;
|
|
341
352
|
/** Repository URL appended to synced pages as an italic source notice. */
|
|
@@ -370,11 +381,20 @@ interface ConfluenceSyncPlan {
|
|
|
370
381
|
dryRun: boolean;
|
|
371
382
|
renderHtmlBlocks: boolean;
|
|
372
383
|
repositoryUrl: string;
|
|
384
|
+
/** Validated leaf page title strategy applied during planning. */
|
|
385
|
+
pageTitleStrategy: PageTitleStrategy;
|
|
373
386
|
}
|
|
374
387
|
/** A single markdown entry's locally-validated sync plan. */
|
|
375
388
|
interface LocalSyncEntryPlan {
|
|
376
389
|
/** The original doc-tree entry this plan covers. */
|
|
377
390
|
entry: DocEntry;
|
|
391
|
+
/**
|
|
392
|
+
* The resolved Confluence page title for this leaf entry, computed once via
|
|
393
|
+
* {@link pageTitleFromSegments} with the plan's {@link ConfluenceSyncPlan.pageTitleStrategy}.
|
|
394
|
+
* Consumed verbatim by local hierarchy validation, dry-run output, remote
|
|
395
|
+
* lookup, create, and update paths.
|
|
396
|
+
*/
|
|
397
|
+
title: string;
|
|
378
398
|
/** Rendered storage HTML from `markdownToStorage` (placeholder macros intact). */
|
|
379
399
|
html: string;
|
|
380
400
|
/** Mermaid blocks parsed from the markdown source. */
|
|
@@ -465,4 +485,4 @@ interface SyncResult {
|
|
|
465
485
|
declare function resolveConfluenceSyncPlan(options?: ConfluenceSyncOptions): ConfluenceSyncPlan;
|
|
466
486
|
declare function syncConfluenceToDocs(options?: ConfluenceSyncOptions): Promise<SyncResult | void>;
|
|
467
487
|
|
|
468
|
-
export { type Attachment, type AttachmentGateway, type PreflightResult as AttachmentPreflightResult, ConfluenceApiError, ConfluenceClient, type ConfluenceClientOptions, type ConfluenceGateway, type ConfluenceSyncOptions, type ConfluenceSyncPlan, type CreatePageInput, type DocEntry, type DocTree, INTERACTIVE_FLAG, type LocalSyncEntryPlan, type LocalSyncPlan, LocalSyncValidationAggregateError, type LocalSyncValidationError, type MarkdownConvertOptions, type MarkdownConvertResult, type MermaidBlock, type MermaidPreflightResult, type MermaidRewriteOptions, type MermaidRewriteResult, type Page, type PageBody, type PageVersion, type RewriteOptions, type RewriteResult, type SyncChange, type SyncFailure, SyncMutationError, type SyncResult, type UpdatePageInput, type ValidatedAttachmentSource, escapeAttachmentFilename, escapeXmlAttribute, isAllowedUrl, isMarkdownName, isRemoteUrl, markdownToStorage, preflightImagesToAttachments, preflightMermaidBlocks, readDocTree, resolveConfluenceSyncPlan, resolveConfluenceSyncPlan as resolveSyncPlan, rewriteImagesToAttachments, rewriteMermaidBlocks, syncConfluenceToDocs, titleFromSegment, validateAttachmentSources, validateLocalSync };
|
|
488
|
+
export { type Attachment, type AttachmentGateway, type PreflightResult as AttachmentPreflightResult, ConfluenceApiError, ConfluenceClient, type ConfluenceClientOptions, type ConfluenceGateway, type ConfluenceSyncOptions, type ConfluenceSyncPlan, type CreatePageInput, DEFAULT_PAGE_TITLE_STRATEGY, type DocEntry, type DocTree, INTERACTIVE_FLAG, type LocalSyncEntryPlan, type LocalSyncPlan, LocalSyncValidationAggregateError, type LocalSyncValidationError, type MarkdownConvertOptions, type MarkdownConvertResult, type MermaidBlock, type MermaidPreflightResult, type MermaidRewriteOptions, type MermaidRewriteResult, PAGE_TITLE_STRATEGIES, type Page, type PageBody, type PageTitleStrategy, type PageVersion, type RewriteOptions, type RewriteResult, type SyncChange, type SyncFailure, SyncMutationError, type SyncResult, type UpdatePageInput, type ValidatedAttachmentSource, escapeAttachmentFilename, escapeXmlAttribute, isAllowedUrl, isMarkdownName, isRemoteUrl, markdownToStorage, pageTitleFromSegments, preflightImagesToAttachments, preflightMermaidBlocks, readDocTree, resolveConfluenceSyncPlan, resolvePageTitleStrategy, resolveConfluenceSyncPlan as resolveSyncPlan, rewriteImagesToAttachments, rewriteMermaidBlocks, syncConfluenceToDocs, titleFromSegment, validateAttachmentSources, validateLocalSync };
|
package/index.js
CHANGED
|
@@ -574,6 +574,64 @@ function titleFromSegment(segment) {
|
|
|
574
574
|
}
|
|
575
575
|
return segment;
|
|
576
576
|
}
|
|
577
|
+
var PAGE_TITLE_STRATEGIES = [
|
|
578
|
+
"filename-stem",
|
|
579
|
+
"filename",
|
|
580
|
+
"sentence-case-parent",
|
|
581
|
+
"sentence-case-parents",
|
|
582
|
+
"sentence-case-path"
|
|
583
|
+
];
|
|
584
|
+
var DEFAULT_PAGE_TITLE_STRATEGY = "filename-stem";
|
|
585
|
+
function resolvePageTitleStrategy(value) {
|
|
586
|
+
if (value === void 0 || value === null) {
|
|
587
|
+
return DEFAULT_PAGE_TITLE_STRATEGY;
|
|
588
|
+
}
|
|
589
|
+
if (typeof value === "string" && PAGE_TITLE_STRATEGIES.includes(value)) {
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
throw new Error(
|
|
593
|
+
`Invalid pageTitleStrategy: expected one of ${PAGE_TITLE_STRATEGIES.join(", ")}, got ${JSON.stringify(value)}`
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
function sentenceCaseStem(filename) {
|
|
597
|
+
let stem = filename;
|
|
598
|
+
if (/\.md$/i.test(stem)) {
|
|
599
|
+
stem = stem.slice(0, stem.length - MARKDOWN_EXT.length);
|
|
600
|
+
}
|
|
601
|
+
let out = stem.replace(/[-_]+/g, " ").trim().replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
602
|
+
const firstLetter = /[a-z]/.exec(out);
|
|
603
|
+
if (firstLetter) {
|
|
604
|
+
const i = firstLetter.index;
|
|
605
|
+
out = out.slice(0, i) + out.charAt(i).toUpperCase() + out.slice(i + 1);
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
function pageTitleFromSegments(segments, strategy) {
|
|
610
|
+
const filename = segments.length > 0 ? segments[segments.length - 1] : "";
|
|
611
|
+
const parents = segments.slice(0, segments.length - 1);
|
|
612
|
+
switch (strategy) {
|
|
613
|
+
case "filename-stem":
|
|
614
|
+
return titleFromSegment(filename);
|
|
615
|
+
case "filename":
|
|
616
|
+
return filename;
|
|
617
|
+
case "sentence-case-parent": {
|
|
618
|
+
const title = sentenceCaseStem(filename);
|
|
619
|
+
if (parents.length === 0) {
|
|
620
|
+
return title;
|
|
621
|
+
}
|
|
622
|
+
return `${title} (${parents[parents.length - 1]})`;
|
|
623
|
+
}
|
|
624
|
+
case "sentence-case-parents": {
|
|
625
|
+
const title = sentenceCaseStem(filename);
|
|
626
|
+
if (parents.length === 0) {
|
|
627
|
+
return title;
|
|
628
|
+
}
|
|
629
|
+
return `${title} (${parents.join("/")})`;
|
|
630
|
+
}
|
|
631
|
+
case "sentence-case-path":
|
|
632
|
+
return `${sentenceCaseStem(filename)} (${[...parents, filename].join("/")})`;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
577
635
|
|
|
578
636
|
// src/markdown.ts
|
|
579
637
|
var STORAGE_LINE_BREAK = "<br />";
|
|
@@ -1618,6 +1676,7 @@ function validateLocalSync(entries, plan) {
|
|
|
1618
1676
|
}) : [];
|
|
1619
1677
|
plans.push({
|
|
1620
1678
|
entry,
|
|
1679
|
+
title: pageTitleFromSegments(entry.segments, plan.pageTitleStrategy),
|
|
1621
1680
|
html: body,
|
|
1622
1681
|
mermaidBlocks,
|
|
1623
1682
|
markdownDir,
|
|
@@ -1647,6 +1706,7 @@ var SyncMutationError = class extends Error {
|
|
|
1647
1706
|
}
|
|
1648
1707
|
};
|
|
1649
1708
|
function resolveConfluenceSyncPlan(options = {}) {
|
|
1709
|
+
const pageTitleStrategy = resolvePageTitleStrategy(options.pageTitleStrategy);
|
|
1650
1710
|
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
1651
1711
|
const folder = resolveInputPath(cwd, options.folder ?? "");
|
|
1652
1712
|
if (!options.folder) {
|
|
@@ -1693,7 +1753,8 @@ function resolveConfluenceSyncPlan(options = {}) {
|
|
|
1693
1753
|
skipUnchanged: options.skipUnchanged ?? true,
|
|
1694
1754
|
dryRun: options.dryRun ?? false,
|
|
1695
1755
|
renderHtmlBlocks: options.renderHtmlBlocks === true,
|
|
1696
|
-
repositoryUrl
|
|
1756
|
+
repositoryUrl,
|
|
1757
|
+
pageTitleStrategy
|
|
1697
1758
|
};
|
|
1698
1759
|
}
|
|
1699
1760
|
async function syncConfluenceToDocs(options = {}) {
|
|
@@ -1704,15 +1765,15 @@ async function syncConfluenceToDocs(options = {}) {
|
|
|
1704
1765
|
log(`No markdown files found under ${plan.folder}`);
|
|
1705
1766
|
return;
|
|
1706
1767
|
}
|
|
1707
|
-
validateLocalHierarchy(tree.entries);
|
|
1708
1768
|
const localPlan = validateLocalSync(tree.entries, plan);
|
|
1769
|
+
validateLocalHierarchy(localPlan.entries, plan.pageTitleStrategy);
|
|
1709
1770
|
if (plan.dryRun) {
|
|
1710
1771
|
log("[dry-run] Walking documentation tree only.");
|
|
1711
1772
|
for (const entryPlan of localPlan.entries) {
|
|
1712
1773
|
const attCount = entryPlan.attachments.length;
|
|
1713
1774
|
const mermaidCount = entryPlan.mermaidBlocks.length;
|
|
1714
1775
|
log(
|
|
1715
|
-
`[dry-run] would sync ${entryPlan.entry.segments.join("/")}` + (attCount > 0 ? ` (${attCount} attachment${attCount === 1 ? "" : "s"} validated)` : "") + (mermaidCount > 0 ? ` (${mermaidCount} mermaid block${mermaidCount === 1 ? "" : "s"})` : "")
|
|
1776
|
+
`[dry-run] would sync ${entryPlan.entry.segments.join("/")} as "${entryPlan.title}"` + (attCount > 0 ? ` (${attCount} attachment${attCount === 1 ? "" : "s"} validated)` : "") + (mermaidCount > 0 ? ` (${mermaidCount} mermaid block${mermaidCount === 1 ? "" : "s"})` : "")
|
|
1716
1777
|
);
|
|
1717
1778
|
}
|
|
1718
1779
|
return;
|
|
@@ -1750,7 +1811,7 @@ async function syncEntry(entryPlan, plan, client, cache, log, changes) {
|
|
|
1750
1811
|
const isLast = idx === segments.length - 1;
|
|
1751
1812
|
const segment = segments[idx] ?? "";
|
|
1752
1813
|
if (isLast && isMarkdownName(segment)) {
|
|
1753
|
-
const title =
|
|
1814
|
+
const title = entryPlan.title;
|
|
1754
1815
|
const leafNeedsUploads = hasLocalImages || hasMermaidBlocks;
|
|
1755
1816
|
const existing = await cache.find(title, currentParentId);
|
|
1756
1817
|
if (!existing && !leafNeedsUploads) {
|
|
@@ -1809,7 +1870,7 @@ async function syncEntry(entryPlan, plan, client, cache, log, changes) {
|
|
|
1809
1870
|
return;
|
|
1810
1871
|
}
|
|
1811
1872
|
if (isMarkdownName(segment)) {
|
|
1812
|
-
const title =
|
|
1873
|
+
const title = pageTitleFromSegments(segments.slice(0, idx + 1), plan.pageTitleStrategy);
|
|
1813
1874
|
const page2 = await cache.findOrCreate(title, currentParentId);
|
|
1814
1875
|
currentParentId = page2.id;
|
|
1815
1876
|
continue;
|
|
@@ -1940,14 +2001,15 @@ async function predictBody(html, mermaidBlocks, pageId, client, ctx) {
|
|
|
1940
2001
|
}
|
|
1941
2002
|
return predicted;
|
|
1942
2003
|
}
|
|
1943
|
-
function validateLocalHierarchy(entries) {
|
|
2004
|
+
function validateLocalHierarchy(entries, strategy) {
|
|
1944
2005
|
const seen = /* @__PURE__ */ new Map();
|
|
1945
|
-
for (const
|
|
2006
|
+
for (const entryPlan of entries) {
|
|
2007
|
+
const { entry } = entryPlan;
|
|
1946
2008
|
let parentKey = "";
|
|
1947
2009
|
for (let index = 0; index < entry.segments.length; index += 1) {
|
|
1948
2010
|
const segment = entry.segments[index] ?? "";
|
|
1949
2011
|
const isLast = index === entry.segments.length - 1;
|
|
1950
|
-
const title = isMarkdownName(segment) ?
|
|
2012
|
+
const title = isMarkdownName(segment) ? isLast ? entryPlan.title : pageTitleFromSegments(entry.segments.slice(0, index + 1), strategy) : segment;
|
|
1951
2013
|
const kind = isLast && isMarkdownName(segment) ? "file" : "dir";
|
|
1952
2014
|
const key = `${parentKey}::${title}`;
|
|
1953
2015
|
const existing = seen.get(key);
|
|
@@ -1962,8 +2024,10 @@ function validateLocalHierarchy(entries) {
|
|
|
1962
2024
|
export {
|
|
1963
2025
|
ConfluenceApiError,
|
|
1964
2026
|
ConfluenceClient,
|
|
2027
|
+
DEFAULT_PAGE_TITLE_STRATEGY,
|
|
1965
2028
|
INTERACTIVE_FLAG,
|
|
1966
2029
|
LocalSyncValidationAggregateError,
|
|
2030
|
+
PAGE_TITLE_STRATEGIES,
|
|
1967
2031
|
SyncMutationError,
|
|
1968
2032
|
escapeAttachmentFilename,
|
|
1969
2033
|
escapeXmlAttribute,
|
|
@@ -1971,10 +2035,12 @@ export {
|
|
|
1971
2035
|
isMarkdownName,
|
|
1972
2036
|
isRemoteUrl,
|
|
1973
2037
|
markdownToStorage,
|
|
2038
|
+
pageTitleFromSegments,
|
|
1974
2039
|
preflightImagesToAttachments,
|
|
1975
2040
|
preflightMermaidBlocks,
|
|
1976
2041
|
readDocTree,
|
|
1977
2042
|
resolveConfluenceSyncPlan,
|
|
2043
|
+
resolvePageTitleStrategy,
|
|
1978
2044
|
resolveConfluenceSyncPlan as resolveSyncPlan,
|
|
1979
2045
|
rewriteImagesToAttachments,
|
|
1980
2046
|
rewriteMermaidBlocks,
|
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.20.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.20.0"
|
|
31
31
|
},
|
|
32
32
|
"files": [
|
|
33
33
|
"**/*",
|