atlass 2.0.0 → 2.1.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 +29 -6
- package/dist/bitbucket.mjs +1 -1
- package/dist/{build-DMLlCHdF.mjs → build-_4VuIcqF.mjs} +865 -494
- package/dist/cli.mjs +1 -1
- package/dist/confluence.mjs +1 -1
- package/dist/jira.mjs +1 -1
- package/package.json +2 -1
|
@@ -10,6 +10,7 @@ import kleur from "kleur";
|
|
|
10
10
|
import { common, createLowlight } from "lowlight";
|
|
11
11
|
import { marked } from "marked";
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { structuredPatch } from "diff";
|
|
13
14
|
//#region src/api/http-error.ts
|
|
14
15
|
var HttpError = class extends Error {
|
|
15
16
|
status;
|
|
@@ -119,6 +120,12 @@ var AtlassianClient = class {
|
|
|
119
120
|
body: JSON.stringify(body)
|
|
120
121
|
});
|
|
121
122
|
}
|
|
123
|
+
async deleteNoContent(path) {
|
|
124
|
+
await this.request(path, {
|
|
125
|
+
method: "DELETE",
|
|
126
|
+
headers: { Accept: "application/json" }
|
|
127
|
+
});
|
|
128
|
+
}
|
|
122
129
|
async postMultipart(path, filename, bytes) {
|
|
123
130
|
const form = new FormData();
|
|
124
131
|
const blob = new Blob([bytes]);
|
|
@@ -1369,7 +1376,7 @@ function link(text, url) {
|
|
|
1369
1376
|
//#endregion
|
|
1370
1377
|
//#region src/commands/view.ts
|
|
1371
1378
|
const INDENT = " ";
|
|
1372
|
-
function fieldLines(pairs) {
|
|
1379
|
+
function fieldLines$1(pairs) {
|
|
1373
1380
|
const kept = pairs.filter(([, value]) => value !== "");
|
|
1374
1381
|
const width = Math.max(...kept.map(([label]) => label.length)) + 1;
|
|
1375
1382
|
return kept.map(([label, value]) => `${kleur.dim(`${label}:`.padEnd(width))} ${value}`);
|
|
@@ -1456,7 +1463,7 @@ function formatPullRequestView(detail, comments, files, nowMs, allComments = fal
|
|
|
1456
1463
|
const label = detail.draft ? "DRAFT" : detail.state || "-";
|
|
1457
1464
|
return [
|
|
1458
1465
|
kleur.bold(detail.title),
|
|
1459
|
-
...fieldLines([
|
|
1466
|
+
...fieldLines$1([
|
|
1460
1467
|
["State", colorForBitbucketState(label)(label)],
|
|
1461
1468
|
["Author", detail.author],
|
|
1462
1469
|
["Branch", branchLine(detail)],
|
|
@@ -1891,87 +1898,186 @@ function outputOptions(command) {
|
|
|
1891
1898
|
return command.option("-l, --limit <n>", "max results (default 25, max 100)").option("--json", "output results as JSON").option("-c, --copy", "pick results to copy to Markdown").option("-o, --out <dir>", "output directory for --copy");
|
|
1892
1899
|
}
|
|
1893
1900
|
//#endregion
|
|
1894
|
-
//#region src/api/
|
|
1895
|
-
function
|
|
1896
|
-
return
|
|
1901
|
+
//#region src/api/confluence-create.ts
|
|
1902
|
+
async function findSpaceId(client, key) {
|
|
1903
|
+
return (await client.getJson(`/wiki/api/v2/spaces?keys=${encodeURIComponent(key)}`)).results[0]?.id ?? null;
|
|
1897
1904
|
}
|
|
1898
|
-
function
|
|
1899
|
-
|
|
1905
|
+
async function createPage(client, page) {
|
|
1906
|
+
const res = await client.postJson("/wiki/api/v2/pages", {
|
|
1907
|
+
spaceId: page.spaceId,
|
|
1908
|
+
status: "current",
|
|
1909
|
+
title: page.title,
|
|
1910
|
+
...page.parentId ? { parentId: page.parentId } : {},
|
|
1911
|
+
body: {
|
|
1912
|
+
representation: "atlas_doc_format",
|
|
1913
|
+
value: JSON.stringify(page.body)
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
return {
|
|
1917
|
+
id: res.id,
|
|
1918
|
+
version: res.version?.number ?? 1
|
|
1919
|
+
};
|
|
1900
1920
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1921
|
+
//#endregion
|
|
1922
|
+
//#region src/api/confluence-attachments.ts
|
|
1923
|
+
const UNKNOWN_SIZE = -1;
|
|
1924
|
+
async function listAttachments(client, id) {
|
|
1925
|
+
return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.map((a) => ({
|
|
1926
|
+
filename: a.title ?? a.id,
|
|
1927
|
+
fileId: mediaNodeId(a),
|
|
1928
|
+
size: typeof a.fileSize === "number" ? a.fileSize : UNKNOWN_SIZE
|
|
1929
|
+
}));
|
|
1903
1930
|
}
|
|
1904
|
-
function
|
|
1905
|
-
return
|
|
1931
|
+
async function uploadAttachment(client, pageId, filename, bytes) {
|
|
1932
|
+
return (await client.postMultipart(`/wiki/rest/api/content/${encodeURIComponent(pageId)}/child/attachment`, filename, bytes)).results?.[0]?.extensions?.fileId ?? await fileIdByListing(client, pageId, filename);
|
|
1906
1933
|
}
|
|
1907
|
-
function
|
|
1908
|
-
|
|
1909
|
-
if (
|
|
1910
|
-
|
|
1934
|
+
async function fileIdByListing(client, pageId, filename) {
|
|
1935
|
+
const match = (await listAttachments(client, pageId)).find((a) => a.filename === filename);
|
|
1936
|
+
if (match) return match.fileId;
|
|
1937
|
+
throw new Error(`Upload of "${filename}" did not return a fileId.`);
|
|
1911
1938
|
}
|
|
1912
|
-
function
|
|
1913
|
-
return
|
|
1939
|
+
async function fetchAttachments(client, id) {
|
|
1940
|
+
return (await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/attachments?limit=250`)).results.filter((a) => a.downloadLink).map((a) => ({
|
|
1941
|
+
mediaId: mediaNodeId(a),
|
|
1942
|
+
filename: a.title ?? a.id,
|
|
1943
|
+
url: withWikiContextPath(a.downloadLink ?? "")
|
|
1944
|
+
}));
|
|
1914
1945
|
}
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
const MAX_CODE_POINT = 1114111;
|
|
1918
|
-
const NAMED = {
|
|
1919
|
-
amp: "&",
|
|
1920
|
-
lt: "<",
|
|
1921
|
-
gt: ">",
|
|
1922
|
-
quot: "\"",
|
|
1923
|
-
apos: "'",
|
|
1924
|
-
nbsp: " "
|
|
1925
|
-
};
|
|
1926
|
-
function decodeEntities(text) {
|
|
1927
|
-
return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, body) => {
|
|
1928
|
-
if (body[0] === "#") {
|
|
1929
|
-
const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
|
|
1930
|
-
return isCodePoint(code) ? String.fromCodePoint(code) : match;
|
|
1931
|
-
}
|
|
1932
|
-
return NAMED[body.toLowerCase()] ?? match;
|
|
1933
|
-
});
|
|
1946
|
+
function mediaNodeId(a) {
|
|
1947
|
+
return a.fileId ?? a.id;
|
|
1934
1948
|
}
|
|
1935
|
-
function
|
|
1936
|
-
|
|
1949
|
+
function withWikiContextPath(link) {
|
|
1950
|
+
if (link.startsWith("http") || link.startsWith("/wiki")) return link;
|
|
1951
|
+
return `/wiki${link}`;
|
|
1937
1952
|
}
|
|
1938
1953
|
//#endregion
|
|
1939
|
-
//#region src/api/confluence-
|
|
1940
|
-
async function
|
|
1941
|
-
|
|
1954
|
+
//#region src/api/confluence-pages.ts
|
|
1955
|
+
async function fetchPage(client, site, id) {
|
|
1956
|
+
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
1957
|
+
const names = new UserNames(client);
|
|
1958
|
+
const [spaceKey, attachments, comments, author] = await Promise.all([
|
|
1959
|
+
fetchSpaceKey(client, page.spaceId),
|
|
1960
|
+
fetchAttachments(client, id),
|
|
1961
|
+
fetchComments$1(client, id, names),
|
|
1962
|
+
names.resolve(page.version?.authorId ?? page.authorId)
|
|
1963
|
+
]);
|
|
1964
|
+
const webui = page._links?.webui ?? "";
|
|
1965
|
+
return {
|
|
1966
|
+
id: page.id,
|
|
1967
|
+
title: page.title,
|
|
1968
|
+
spaceKey,
|
|
1969
|
+
version: page.version?.number ?? 0,
|
|
1970
|
+
author,
|
|
1971
|
+
createdAt: page.createdAt ?? "",
|
|
1972
|
+
updatedAt: page.version?.createdAt ?? "",
|
|
1973
|
+
url: webui ? `${site}/wiki${webui}` : "",
|
|
1974
|
+
body: parseAdfJson(page.body?.atlas_doc_format?.value),
|
|
1975
|
+
attachments,
|
|
1976
|
+
comments
|
|
1977
|
+
};
|
|
1942
1978
|
}
|
|
1943
|
-
async function
|
|
1944
|
-
const
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1979
|
+
async function fetchPageState(client, id) {
|
|
1980
|
+
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
1981
|
+
return {
|
|
1982
|
+
version: page.version?.number ?? 0,
|
|
1983
|
+
title: page.title,
|
|
1984
|
+
body: parseAdfJson(page.body?.atlas_doc_format?.value)
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
async function updatePage(client, id, params) {
|
|
1988
|
+
const res = await client.putJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}`, {
|
|
1989
|
+
id,
|
|
1990
|
+
status: "current",
|
|
1991
|
+
title: params.title,
|
|
1992
|
+
body: {
|
|
1993
|
+
representation: "atlas_doc_format",
|
|
1994
|
+
value: JSON.stringify(params.body)
|
|
1995
|
+
},
|
|
1996
|
+
version: {
|
|
1997
|
+
number: params.nextVersion,
|
|
1998
|
+
message: params.message
|
|
1999
|
+
}
|
|
1948
2000
|
});
|
|
1949
|
-
const results = (await client.getJson(`/wiki/rest/api/search?${query.toString()}`)).results ?? [];
|
|
1950
|
-
const serverPageWasFull = results.length === limit;
|
|
1951
2001
|
return {
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
space: r.content?.space?.key ?? spaceKeyOf(r.resultGlobalContainer?.displayUrl),
|
|
1955
|
-
title: decodeEntities(r.content?.title ?? r.title ?? ""),
|
|
1956
|
-
updated: r.lastModified ?? "",
|
|
1957
|
-
url: r.url ? `${site}/wiki${r.url}` : ""
|
|
1958
|
-
})),
|
|
1959
|
-
hasMore: serverPageWasFull
|
|
2002
|
+
number: res.version?.number ?? params.nextVersion,
|
|
2003
|
+
createdAt: res.version?.createdAt ?? ""
|
|
1960
2004
|
};
|
|
1961
2005
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2006
|
+
async function fetchSpaceKey(client, spaceId) {
|
|
2007
|
+
if (!spaceId) return "";
|
|
2008
|
+
try {
|
|
2009
|
+
return (await client.getJson(`/wiki/api/v2/spaces/${encodeURIComponent(spaceId)}`)).key ?? "";
|
|
2010
|
+
} catch {
|
|
2011
|
+
return "";
|
|
2012
|
+
}
|
|
1965
2013
|
}
|
|
1966
|
-
function
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2014
|
+
async function fetchComments$1(client, id, names) {
|
|
2015
|
+
const res = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}/footer-comments?body-format=atlas_doc_format&limit=250`);
|
|
2016
|
+
return Promise.all(res.results.map(async (c) => ({
|
|
2017
|
+
author: await names.resolve(c.version?.authorId),
|
|
2018
|
+
created: c.version?.createdAt ?? "",
|
|
2019
|
+
body: parseAdfJson(c.body?.atlas_doc_format?.value)
|
|
2020
|
+
})));
|
|
2021
|
+
}
|
|
2022
|
+
function parseAdfJson(value) {
|
|
2023
|
+
if (!value) return null;
|
|
2024
|
+
try {
|
|
2025
|
+
return JSON.parse(value);
|
|
2026
|
+
} catch {
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
var UserNames = class {
|
|
2031
|
+
client;
|
|
2032
|
+
cache = /* @__PURE__ */ new Map();
|
|
2033
|
+
constructor(client) {
|
|
2034
|
+
this.client = client;
|
|
2035
|
+
}
|
|
2036
|
+
async resolve(accountId) {
|
|
2037
|
+
if (!accountId) return "";
|
|
2038
|
+
const cached = this.cache.get(accountId);
|
|
2039
|
+
if (cached !== void 0) return cached;
|
|
2040
|
+
const name = await this.fetchDisplayName(accountId) ?? accountId;
|
|
2041
|
+
this.cache.set(accountId, name);
|
|
2042
|
+
return name;
|
|
2043
|
+
}
|
|
2044
|
+
async fetchDisplayName(accountId) {
|
|
2045
|
+
try {
|
|
2046
|
+
return (await this.client.getJson(`/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}`)).displayName || null;
|
|
2047
|
+
} catch {
|
|
2048
|
+
return null;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2052
|
+
//#endregion
|
|
2053
|
+
//#region src/adf/html.ts
|
|
2054
|
+
const COMMENT = /<!--[\s\S]*?-->/g;
|
|
2055
|
+
function isOnlyComments(html) {
|
|
2056
|
+
return html.replace(COMMENT, "").trim().length === 0;
|
|
2057
|
+
}
|
|
2058
|
+
function withoutComments(md) {
|
|
2059
|
+
const kept = [];
|
|
2060
|
+
let skipSpace = false;
|
|
2061
|
+
for (const token of marked.lexer(md)) {
|
|
2062
|
+
if (skipSpace && token.type === "space") continue;
|
|
2063
|
+
skipSpace = token.type === "html" && isOnlyComments(token.text);
|
|
2064
|
+
if (skipSpace) continue;
|
|
2065
|
+
kept.push(token.type === "code" ? token.raw : token.raw.replace(COMMENT, ""));
|
|
2066
|
+
}
|
|
2067
|
+
return kept.join("").trim();
|
|
2068
|
+
}
|
|
2069
|
+
function htmlWarnings(md) {
|
|
2070
|
+
let blocks = 0;
|
|
2071
|
+
let inline = 0;
|
|
2072
|
+
marked.walkTokens(marked.lexer(md), (token) => {
|
|
2073
|
+
if (token.type !== "html" || isOnlyComments(token.text)) return;
|
|
2074
|
+
if (token.block) blocks++;
|
|
2075
|
+
else inline++;
|
|
2076
|
+
});
|
|
2077
|
+
return [...blocks > 0 ? [`${counted(blocks, "HTML block")} will be dropped`] : [], ...inline > 0 ? [`${counted(inline, "inline HTML tag")} will be sent as text`] : []];
|
|
2078
|
+
}
|
|
2079
|
+
function counted(n, noun) {
|
|
2080
|
+
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
1975
2081
|
}
|
|
1976
2082
|
//#endregion
|
|
1977
2083
|
//#region src/adf/from-markdown.ts
|
|
@@ -2144,7 +2250,10 @@ function inlineNode(token, ctx, marks) {
|
|
|
2144
2250
|
}));
|
|
2145
2251
|
}
|
|
2146
2252
|
case "br": return [{ type: "hardBreak" }];
|
|
2147
|
-
case "html":
|
|
2253
|
+
case "html": {
|
|
2254
|
+
const html = token.text;
|
|
2255
|
+
return isOnlyComments(html) ? [] : textNode(html, marks);
|
|
2256
|
+
}
|
|
2148
2257
|
default: return "text" in token && token.text ? textNode(token.text, marks) : [];
|
|
2149
2258
|
}
|
|
2150
2259
|
}
|
|
@@ -2178,132 +2287,301 @@ function clampLevel(value) {
|
|
|
2178
2287
|
return Math.min(6, Math.max(1, Math.trunc(value) || 1));
|
|
2179
2288
|
}
|
|
2180
2289
|
//#endregion
|
|
2181
|
-
//#region src/
|
|
2182
|
-
const
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2290
|
+
//#region src/adf/lossy.ts
|
|
2291
|
+
const LOSSY_LABELS = {
|
|
2292
|
+
panel: "panel",
|
|
2293
|
+
expand: "expand",
|
|
2294
|
+
nestedExpand: "expand",
|
|
2295
|
+
decisionList: "decision list",
|
|
2296
|
+
layoutSection: "layout",
|
|
2297
|
+
extension: "macro",
|
|
2298
|
+
bodiedExtension: "macro",
|
|
2299
|
+
inlineExtension: "macro"
|
|
2300
|
+
};
|
|
2301
|
+
const JIRA_LOSSY_LABELS = {
|
|
2302
|
+
...LOSSY_LABELS,
|
|
2303
|
+
media: "image",
|
|
2304
|
+
mediaInline: "image"
|
|
2305
|
+
};
|
|
2306
|
+
function findLossyNodes(node, lossyLabels = LOSSY_LABELS) {
|
|
2307
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2308
|
+
const visit = (n) => {
|
|
2309
|
+
const label = lossyLabels[n.type];
|
|
2310
|
+
if (label) counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
2311
|
+
for (const child of n.content ?? []) visit(child);
|
|
2312
|
+
};
|
|
2313
|
+
if (node) visit(node);
|
|
2314
|
+
return counts;
|
|
2197
2315
|
}
|
|
2198
|
-
|
|
2199
|
-
return
|
|
2200
|
-
mediaId: mediaNodeId(a),
|
|
2201
|
-
filename: a.title ?? a.id,
|
|
2202
|
-
url: withWikiContextPath(a.downloadLink ?? "")
|
|
2203
|
-
}));
|
|
2316
|
+
function formatLossy(counts) {
|
|
2317
|
+
return [...counts.entries()].map(([label, n]) => `${n} ${label}${n === 1 ? "" : "s"}`).join(", ");
|
|
2204
2318
|
}
|
|
2205
|
-
|
|
2206
|
-
|
|
2319
|
+
//#endregion
|
|
2320
|
+
//#region src/update/body-diff.ts
|
|
2321
|
+
const CONTEXT_LINES = 3;
|
|
2322
|
+
function unifiedDiff(before, after, labels) {
|
|
2323
|
+
if (before === after) return [];
|
|
2324
|
+
const [from, to] = labels;
|
|
2325
|
+
const patch = structuredPatch(from, to, `${before}\n`, `${after}\n`, void 0, void 0, { context: CONTEXT_LINES });
|
|
2326
|
+
return [
|
|
2327
|
+
`--- ${from}`,
|
|
2328
|
+
`+++ ${to}`,
|
|
2329
|
+
...patch.hunks.flatMap((h) => [`@@ -${h.oldStart},${h.oldLines} +${h.newStart},${h.newLines} @@`, ...h.lines])
|
|
2330
|
+
];
|
|
2207
2331
|
}
|
|
2208
|
-
function
|
|
2209
|
-
if (
|
|
2210
|
-
return
|
|
2332
|
+
function colorDiffLine(line) {
|
|
2333
|
+
if (line.startsWith("+++") || line.startsWith("---")) return kleur.bold(line);
|
|
2334
|
+
if (line.startsWith("@@")) return kleur.cyan(line);
|
|
2335
|
+
if (line.startsWith("+")) return kleur.green(line);
|
|
2336
|
+
if (line.startsWith("-")) return kleur.red(line);
|
|
2337
|
+
return line;
|
|
2211
2338
|
}
|
|
2212
2339
|
//#endregion
|
|
2213
|
-
//#region src/
|
|
2214
|
-
|
|
2215
|
-
const
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2340
|
+
//#region src/update/plan.ts
|
|
2341
|
+
function planIssueUpdate(source, issue, options) {
|
|
2342
|
+
const images = imageHrefs(source.body).map((href) => ({
|
|
2343
|
+
href,
|
|
2344
|
+
kind: isExternalHref(href) ? "external" : "unsupported"
|
|
2345
|
+
}));
|
|
2346
|
+
const unsupported = images.filter((i) => i.kind === "unsupported").map((i) => i.href);
|
|
2347
|
+
const body = markdownToAdf(source.body, { resolveImage: externalImagesOnly });
|
|
2348
|
+
const refusals = [...unsupported.length > 0 ? [`jira update does not support image changes yet. Remove local image reference(s) or edit text only: ${unsupported.join(", ")}`] : [], ...emptyBodyRefusal(body)];
|
|
2349
|
+
return withVerdict({
|
|
2350
|
+
noun: "issue",
|
|
2351
|
+
id: source.key,
|
|
2352
|
+
headline: headline("summary", issue.summary, source.title, options.summary),
|
|
2353
|
+
revision: revision(source.updatedAtCopy, issue.updated),
|
|
2354
|
+
lossy: findLossyNodes(issue.description, JIRA_LOSSY_LABELS),
|
|
2355
|
+
warnings: htmlWarnings(source.body),
|
|
2356
|
+
diff: [],
|
|
2357
|
+
images,
|
|
2358
|
+
uploads: [],
|
|
2359
|
+
body,
|
|
2360
|
+
refusals
|
|
2361
|
+
}, options.force ?? false);
|
|
2362
|
+
}
|
|
2363
|
+
function formatPlan(plan) {
|
|
2364
|
+
const lines = [`Dry run for ${plan.noun} ${plan.id} "${plan.headline.current}"`];
|
|
2365
|
+
if (plan.headline.next !== plan.headline.current) lines.push(row(plan.headline.label, `"${plan.headline.current}" -> "${plan.headline.next}"`));
|
|
2366
|
+
const counts = imageCounts(plan.images);
|
|
2367
|
+
if (counts) lines.push(row("images", counts));
|
|
2368
|
+
for (const refusal of plan.refusals) lines.push(row("blocked", refusal));
|
|
2369
|
+
if (plan.lossy.size > 0) lines.push(row("warning", `${formatLossy(plan.lossy)} will be removed`));
|
|
2370
|
+
for (const warning of plan.warnings) lines.push(row("warning", warning));
|
|
2371
|
+
if (plan.revision.stale) lines.push(row("stale", `${revisionText(plan.revision)} (would refuse without --force)`));
|
|
2372
|
+
lines.push(...plan.diff.map(colorDiffLine));
|
|
2373
|
+
lines.push(" nothing was written (dry run)");
|
|
2374
|
+
return lines;
|
|
2375
|
+
}
|
|
2376
|
+
const IMAGE_COUNT_LABELS = [
|
|
2377
|
+
["upload", "new"],
|
|
2378
|
+
["changed", "changed"],
|
|
2379
|
+
["reuse", "reused"],
|
|
2380
|
+
["external", "external"],
|
|
2381
|
+
["unsupported", "unsupported"],
|
|
2382
|
+
["missing", "missing"]
|
|
2383
|
+
];
|
|
2384
|
+
function imageCounts(images) {
|
|
2385
|
+
return IMAGE_COUNT_LABELS.map(([kind, label]) => ({
|
|
2386
|
+
n: images.filter((i) => i.kind === kind).length,
|
|
2387
|
+
label
|
|
2388
|
+
})).filter(({ n }) => n > 0).map(({ n, label }) => `${n} ${label}`).join(", ");
|
|
2389
|
+
}
|
|
2390
|
+
function row(label, value) {
|
|
2391
|
+
return ` ${`${label}:`.padEnd(9)}${value}`;
|
|
2392
|
+
}
|
|
2393
|
+
function headline(label, current, title, useTitle) {
|
|
2224
2394
|
return {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
version: page.version?.number ?? 0,
|
|
2229
|
-
author,
|
|
2230
|
-
createdAt: page.createdAt ?? "",
|
|
2231
|
-
updatedAt: page.version?.createdAt ?? "",
|
|
2232
|
-
url: webui ? `${site}/wiki${webui}` : "",
|
|
2233
|
-
body: parseAdfJson(page.body?.atlas_doc_format?.value),
|
|
2234
|
-
attachments,
|
|
2235
|
-
comments
|
|
2395
|
+
label,
|
|
2396
|
+
current,
|
|
2397
|
+
next: useTitle && title ? title : current
|
|
2236
2398
|
};
|
|
2237
2399
|
}
|
|
2238
|
-
|
|
2239
|
-
const page = await client.getJson(`/wiki/api/v2/pages/${encodeURIComponent(id)}?body-format=atlas_doc_format`);
|
|
2400
|
+
function revision(local, server) {
|
|
2240
2401
|
return {
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2402
|
+
local: local || "unknown",
|
|
2403
|
+
server: server || "unknown",
|
|
2404
|
+
stale: local !== server
|
|
2244
2405
|
};
|
|
2245
2406
|
}
|
|
2246
|
-
|
|
2247
|
-
return
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2407
|
+
function revisionText(revision) {
|
|
2408
|
+
return `copied at ${revision.local}, server now ${revision.server}`;
|
|
2409
|
+
}
|
|
2410
|
+
function externalImagesOnly(href, alt) {
|
|
2411
|
+
return isExternalHref(href) ? externalMedia(href, alt) : void 0;
|
|
2412
|
+
}
|
|
2413
|
+
function missingImagesRefusal(missing) {
|
|
2414
|
+
return missing.length > 0 ? [`Image file(s) not found: ${missing.join(", ")}`] : [];
|
|
2415
|
+
}
|
|
2416
|
+
function emptyBodyRefusal(body) {
|
|
2417
|
+
if (body.content && body.content.length > 0) return [];
|
|
2418
|
+
return ["Refusing to update: the converted body is empty."];
|
|
2419
|
+
}
|
|
2420
|
+
function withVerdict(plan, force) {
|
|
2421
|
+
return {
|
|
2422
|
+
...plan,
|
|
2423
|
+
verdict: verdict(plan, force)
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
function verdict(plan, force) {
|
|
2427
|
+
if (plan.refusals.length > 0) return {
|
|
2428
|
+
kind: "refuse",
|
|
2429
|
+
message: plan.refusals.join("\n")
|
|
2430
|
+
};
|
|
2431
|
+
const noun = plan.noun;
|
|
2432
|
+
if (plan.revision.stale && !force) return {
|
|
2433
|
+
kind: "refuse",
|
|
2434
|
+
message: `${capitalize(noun)} changed on the server since you copied it (${revisionText(plan.revision)}). Re-copy the ${noun} or pass --force.`
|
|
2435
|
+
};
|
|
2436
|
+
if (plan.lossy.size > 0 && !force) return {
|
|
2437
|
+
kind: "confirm",
|
|
2438
|
+
message: `This ${noun} contains ${formatLossy(plan.lossy)} that Markdown cannot represent and will be removed. Continue?`
|
|
2439
|
+
};
|
|
2440
|
+
return { kind: "proceed" };
|
|
2441
|
+
}
|
|
2442
|
+
function capitalize(text) {
|
|
2443
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
2444
|
+
}
|
|
2445
|
+
//#endregion
|
|
2446
|
+
//#region src/update/plan-page.ts
|
|
2447
|
+
function planPageBody(page, attachments, localImages) {
|
|
2448
|
+
const collection = `contentId-${page.id}`;
|
|
2449
|
+
const images = imageHrefs(page.body).map((href) => pageImage(href, localImages, attachments));
|
|
2450
|
+
const byHref = new Map(images.map((e) => [e.href, e]));
|
|
2451
|
+
return {
|
|
2452
|
+
images,
|
|
2453
|
+
body: markdownToAdf(page.body, { resolveImage: (href, alt) => {
|
|
2454
|
+
const entry = byHref.get(href);
|
|
2455
|
+
if (!entry || entry.kind === "missing") return void 0;
|
|
2456
|
+
if (entry.kind === "external") return externalMedia(href, alt);
|
|
2457
|
+
return mediaNode({
|
|
2458
|
+
type: "file",
|
|
2459
|
+
id: entry.kind === "reuse" ? entry.fileId : href,
|
|
2460
|
+
collection
|
|
2461
|
+
}, alt);
|
|
2462
|
+
} }),
|
|
2463
|
+
uploads: images.flatMap((e) => e.kind === "upload" || e.kind === "changed" ? [{
|
|
2464
|
+
href: e.href,
|
|
2465
|
+
path: e.path,
|
|
2466
|
+
filename: e.filename
|
|
2467
|
+
}] : [])
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
function planPageUpdate(source, state, attachments, localImages, options) {
|
|
2471
|
+
const { images, body, uploads } = planPageBody(source, attachments, localImages);
|
|
2472
|
+
const missing = images.filter((e) => e.kind === "missing").map((e) => e.href);
|
|
2473
|
+
return withVerdict({
|
|
2474
|
+
noun: "page",
|
|
2475
|
+
id: source.id,
|
|
2476
|
+
headline: headline("title", state.title, source.title, options.title),
|
|
2477
|
+
revision: revision(`v${source.version}`, `v${state.version}`),
|
|
2478
|
+
lossy: findLossyNodes(state.body),
|
|
2479
|
+
warnings: htmlWarnings(source.body),
|
|
2480
|
+
diff: unifiedDiff(serverMarkdown(state, attachments, images), withoutComments(source.body), [`server v${state.version}`, options.file]),
|
|
2481
|
+
images: images.map(({ href, kind }) => ({
|
|
2482
|
+
href,
|
|
2483
|
+
kind
|
|
2484
|
+
})),
|
|
2485
|
+
uploads,
|
|
2486
|
+
body,
|
|
2487
|
+
refusals: [...missingImagesRefusal(missing), ...emptyBodyRefusal(body)]
|
|
2488
|
+
}, options.force ?? false);
|
|
2489
|
+
}
|
|
2490
|
+
function serverMarkdown(state, attachments, images) {
|
|
2491
|
+
const reused = new Map(images.flatMap((e) => e.kind === "reuse" ? [[e.fileId, e.href]] : []));
|
|
2492
|
+
return adfToMarkdown(state.body, { resolveMedia: ({ id }) => reused.get(id ?? "") ?? attachments.find((a) => a.fileId === id)?.filename });
|
|
2493
|
+
}
|
|
2494
|
+
function pageImage(href, localImages, attachments) {
|
|
2495
|
+
if (isExternalHref(href)) return {
|
|
2496
|
+
href,
|
|
2497
|
+
kind: "external"
|
|
2498
|
+
};
|
|
2499
|
+
const local = localImages.find((l) => l.href === href);
|
|
2500
|
+
if (!local || local.size === void 0) return {
|
|
2501
|
+
href,
|
|
2502
|
+
kind: "missing"
|
|
2503
|
+
};
|
|
2504
|
+
const existing = attachments.find((a) => a.filename === local.filename);
|
|
2505
|
+
if (existing && existing.size === local.size) return {
|
|
2506
|
+
href,
|
|
2507
|
+
kind: "reuse",
|
|
2508
|
+
fileId: existing.fileId
|
|
2509
|
+
};
|
|
2510
|
+
return {
|
|
2511
|
+
href,
|
|
2512
|
+
kind: existing ? "changed" : "upload",
|
|
2513
|
+
path: local.path,
|
|
2514
|
+
filename: local.filename
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
function withUploadedIds(doc, ids) {
|
|
2518
|
+
if (!doc.content) return doc;
|
|
2519
|
+
return {
|
|
2520
|
+
...doc,
|
|
2521
|
+
content: doc.content.map((n) => withUploadedId(n, ids))
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
function withUploadedId(node, ids) {
|
|
2525
|
+
const id = node.type === "media" ? node.attrs?.["id"] : void 0;
|
|
2526
|
+
const swapped = typeof id === "string" && ids.has(id) ? {
|
|
2527
|
+
...node,
|
|
2528
|
+
attrs: {
|
|
2529
|
+
...node.attrs,
|
|
2530
|
+
id: ids.get(id)
|
|
2258
2531
|
}
|
|
2259
|
-
}
|
|
2532
|
+
} : node;
|
|
2533
|
+
if (!swapped.content) return swapped;
|
|
2534
|
+
return {
|
|
2535
|
+
...swapped,
|
|
2536
|
+
content: swapped.content.map((n) => withUploadedId(n, ids))
|
|
2537
|
+
};
|
|
2260
2538
|
}
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2539
|
+
//#endregion
|
|
2540
|
+
//#region src/commands/confluence-push.ts
|
|
2541
|
+
async function pushPage({ session, term, files }, content, push) {
|
|
2542
|
+
const ids = /* @__PURE__ */ new Map();
|
|
2543
|
+
for (const upload of content.uploads) {
|
|
2544
|
+
term.err(`Uploading ${upload.filename} ...`);
|
|
2545
|
+
const bytes = await files.readBytes(upload.path);
|
|
2546
|
+
ids.set(upload.href, await uploadAttachment(session, push.id, upload.filename, bytes));
|
|
2267
2547
|
}
|
|
2548
|
+
return updatePage(session, push.id, {
|
|
2549
|
+
title: content.title,
|
|
2550
|
+
nextVersion: push.nextVersion,
|
|
2551
|
+
body: withUploadedIds(content.body, ids),
|
|
2552
|
+
message: push.message
|
|
2553
|
+
});
|
|
2268
2554
|
}
|
|
2269
|
-
async function
|
|
2270
|
-
const
|
|
2271
|
-
return Promise.all(
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2555
|
+
async function statLocalImages(files, dir, md) {
|
|
2556
|
+
const hrefs = imageHrefs(md).filter((href) => !isExternalHref(href));
|
|
2557
|
+
return Promise.all(hrefs.map(async (href) => {
|
|
2558
|
+
const path = isAbsolute(href) ? href : resolve(dir, href);
|
|
2559
|
+
return {
|
|
2560
|
+
href,
|
|
2561
|
+
path,
|
|
2562
|
+
filename: basename(path),
|
|
2563
|
+
...await fileSize(files, path)
|
|
2564
|
+
};
|
|
2565
|
+
}));
|
|
2276
2566
|
}
|
|
2277
|
-
function
|
|
2278
|
-
if (!value) return null;
|
|
2567
|
+
async function fileSize(files, path) {
|
|
2279
2568
|
try {
|
|
2280
|
-
return
|
|
2569
|
+
return { size: await files.size(path) };
|
|
2281
2570
|
} catch {
|
|
2282
|
-
return
|
|
2571
|
+
return {};
|
|
2283
2572
|
}
|
|
2284
2573
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
this.cache.set(accountId, name);
|
|
2297
|
-
return name;
|
|
2298
|
-
}
|
|
2299
|
-
async fetchDisplayName(accountId) {
|
|
2300
|
-
try {
|
|
2301
|
-
return (await this.client.getJson(`/wiki/rest/api/user?accountId=${encodeURIComponent(accountId)}`)).displayName || null;
|
|
2302
|
-
} catch {
|
|
2303
|
-
return null;
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
};
|
|
2574
|
+
//#endregion
|
|
2575
|
+
//#region src/api/confluence-favourites.ts
|
|
2576
|
+
function favouritePath(id) {
|
|
2577
|
+
return `/wiki/rest/api/relation/favourite/from/user/current/to/content/${encodeURIComponent(id)}`;
|
|
2578
|
+
}
|
|
2579
|
+
async function starPage(client, id) {
|
|
2580
|
+
await client.putNoContent(favouritePath(id), void 0);
|
|
2581
|
+
}
|
|
2582
|
+
async function unstarPage(client, id) {
|
|
2583
|
+
await client.deleteNoContent(favouritePath(id));
|
|
2584
|
+
}
|
|
2307
2585
|
//#endregion
|
|
2308
2586
|
//#region src/commands/resolve-ref.ts
|
|
2309
2587
|
async function resolveRef(ask, arg, kind) {
|
|
@@ -2316,43 +2594,138 @@ async function resolveRef(ask, arg, kind) {
|
|
|
2316
2594
|
if (!ref) throw new Error(kind.notFound(raw));
|
|
2317
2595
|
return ref;
|
|
2318
2596
|
}
|
|
2597
|
+
function layoutOf(raw) {
|
|
2598
|
+
return {
|
|
2599
|
+
bom: raw.startsWith(""),
|
|
2600
|
+
crlf: /\r?\n/.exec(raw)?.[0] === "\r\n"
|
|
2601
|
+
};
|
|
2602
|
+
}
|
|
2603
|
+
function normalized(raw) {
|
|
2604
|
+
return (raw.startsWith("") ? raw.slice(1) : raw).replace(/\r\n/g, "\n");
|
|
2605
|
+
}
|
|
2606
|
+
function editKeepingLayout(raw, edit) {
|
|
2607
|
+
return withLayout(edit(normalized(raw)), layoutOf(raw));
|
|
2608
|
+
}
|
|
2609
|
+
function withLayout(text, layout) {
|
|
2610
|
+
const lines = layout.crlf ? text.replace(/\n/g, "\r\n") : text;
|
|
2611
|
+
return layout.bom ? `${lines}` : lines;
|
|
2612
|
+
}
|
|
2319
2613
|
//#endregion
|
|
2320
|
-
//#region src/
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2614
|
+
//#region src/markdown/frontmatter.ts
|
|
2615
|
+
const BLOCK = /^---\n([\s\S]*?)\n---\n?/;
|
|
2616
|
+
const KEY = /^([A-Za-z0-9_]+):\s*(.*)$/;
|
|
2617
|
+
const LIST_ITEM = /^ {2}- (.*)$/;
|
|
2618
|
+
const BARE_NUMBER = /^-?\d+(\.\d+)?$/;
|
|
2619
|
+
function splitFrontmatter(text) {
|
|
2620
|
+
const match = text.match(BLOCK);
|
|
2621
|
+
if (!match) return null;
|
|
2622
|
+
return {
|
|
2623
|
+
fields: parseFields(match[1] ?? ""),
|
|
2624
|
+
rest: text.slice(match[0].length)
|
|
2327
2625
|
};
|
|
2328
2626
|
}
|
|
2627
|
+
function formatFrontmatter(fields) {
|
|
2628
|
+
return [
|
|
2629
|
+
"---",
|
|
2630
|
+
...Object.entries(fields).flatMap(([key, value]) => fieldLines(key, value)),
|
|
2631
|
+
"---"
|
|
2632
|
+
].join("\n");
|
|
2633
|
+
}
|
|
2634
|
+
function rewriteFields(raw, fields) {
|
|
2635
|
+
return editKeepingLayout(raw, (text) => withFields(text, fields));
|
|
2636
|
+
}
|
|
2637
|
+
function withFields(text, fields) {
|
|
2638
|
+
const match = text.match(BLOCK);
|
|
2639
|
+
if (!match) return `${formatFrontmatter(fields)}\n\n${text}`;
|
|
2640
|
+
const lines = (match[1] ?? "").split("\n");
|
|
2641
|
+
const pending = new Map(Object.entries(fields));
|
|
2642
|
+
const kept = [];
|
|
2643
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2644
|
+
const line = lines[i] ?? "";
|
|
2645
|
+
const key = line.match(KEY)?.[1];
|
|
2646
|
+
const value = key === void 0 ? void 0 : pending.get(key);
|
|
2647
|
+
if (key === void 0 || value === void 0) {
|
|
2648
|
+
kept.push(line);
|
|
2649
|
+
continue;
|
|
2650
|
+
}
|
|
2651
|
+
kept.push(...fieldLines(key, value));
|
|
2652
|
+
pending.delete(key);
|
|
2653
|
+
while (LIST_ITEM.test(lines[i + 1] ?? "")) i++;
|
|
2654
|
+
}
|
|
2655
|
+
for (const [key, value] of pending) kept.push(...fieldLines(key, value));
|
|
2656
|
+
const closing = match[0].endsWith("\n") ? "\n" : "";
|
|
2657
|
+
return `---\n${kept.join("\n")}\n---${closing}${text.slice(match[0].length)}`;
|
|
2658
|
+
}
|
|
2659
|
+
function fieldLines(key, value) {
|
|
2660
|
+
if (Array.isArray(value)) return value.length === 0 ? [`${key}: []`] : [`${key}:`, ...value.map((v) => ` - ${quote$1(v)}`)];
|
|
2661
|
+
return [`${key}: ${typeof value === "number" ? value : quote$1(value)}`];
|
|
2662
|
+
}
|
|
2663
|
+
function parseFields(block) {
|
|
2664
|
+
const fields = {};
|
|
2665
|
+
const lines = block.split("\n");
|
|
2666
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2667
|
+
const m = lines[i]?.match(KEY);
|
|
2668
|
+
if (!m) continue;
|
|
2669
|
+
const key = m[1] ?? "";
|
|
2670
|
+
const raw = (m[2] ?? "").trim();
|
|
2671
|
+
if (raw === "[]") fields[key] = [];
|
|
2672
|
+
else if (raw === "") {
|
|
2673
|
+
const items = [];
|
|
2674
|
+
for (let item; item = lines[i + 1]?.match(LIST_ITEM); i++) items.push(unquote((item[1] ?? "").trim()));
|
|
2675
|
+
fields[key] = items;
|
|
2676
|
+
} else fields[key] = scalar(raw);
|
|
2677
|
+
}
|
|
2678
|
+
return fields;
|
|
2679
|
+
}
|
|
2680
|
+
function scalar(raw) {
|
|
2681
|
+
if (raw.startsWith("\"")) return unquote(raw);
|
|
2682
|
+
if (BARE_NUMBER.test(raw)) return Number(raw);
|
|
2683
|
+
return raw;
|
|
2684
|
+
}
|
|
2685
|
+
function quote$1(value) {
|
|
2686
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
2687
|
+
}
|
|
2688
|
+
function unquote(value) {
|
|
2689
|
+
if (!value.startsWith("\"") || !value.endsWith("\"")) return value;
|
|
2690
|
+
return value.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
2691
|
+
}
|
|
2329
2692
|
//#endregion
|
|
2330
2693
|
//#region src/markdown/copied-document.ts
|
|
2694
|
+
const GENERATED_MARKER = "<!-- atlass:generated -->";
|
|
2331
2695
|
const COMMENTS_HEADING = "## Comments";
|
|
2332
2696
|
const ATTACHMENTS_HEADING = "## Attachments";
|
|
2333
|
-
const
|
|
2334
|
-
const FRONTMATTER_KEY = /^([A-Za-z0-9_]+):\s*(.*)$/;
|
|
2335
|
-
const FRONTMATTER_LIST_ITEM = /^ {2}- (.*)$/;
|
|
2336
|
-
const BARE_NUMBER = /^-?\d+(\.\d+)?$/;
|
|
2337
|
-
const TRAILING_SECTION_HEADING = /^## (Comments|Attachments)\s*$/;
|
|
2697
|
+
const LEGACY_SECTION_HEADING = /^## (Comments|Attachments)\s*$/;
|
|
2338
2698
|
function render(doc) {
|
|
2339
2699
|
return `${[
|
|
2340
|
-
|
|
2700
|
+
formatFrontmatter(doc.fields),
|
|
2341
2701
|
`# ${doc.title}`,
|
|
2342
2702
|
doc.body,
|
|
2703
|
+
GENERATED_MARKER,
|
|
2343
2704
|
commentsSection(doc.comments),
|
|
2344
2705
|
attachmentsSection(doc.attachments)
|
|
2345
2706
|
].filter((s) => s.trim().length > 0).join("\n\n")}\n`;
|
|
2346
2707
|
}
|
|
2347
|
-
function parse(
|
|
2348
|
-
const
|
|
2349
|
-
if (!
|
|
2350
|
-
|
|
2351
|
-
|
|
2708
|
+
function parse(raw) {
|
|
2709
|
+
const split = splitFrontmatter(normalized(raw));
|
|
2710
|
+
if (!split) throw new Error("Not an atlass file: no YAML frontmatter found.");
|
|
2711
|
+
return sourceOf(split, "marker-or-legacy-heading");
|
|
2712
|
+
}
|
|
2713
|
+
function parseDraft(raw) {
|
|
2714
|
+
const text = normalized(raw);
|
|
2715
|
+
return sourceOf(splitFrontmatter(text) ?? {
|
|
2716
|
+
fields: {},
|
|
2717
|
+
rest: text
|
|
2718
|
+
}, "marker");
|
|
2719
|
+
}
|
|
2720
|
+
function withGeneratedMarker(text) {
|
|
2721
|
+
if (text.split("\n").some(isMarker)) return text;
|
|
2722
|
+
return `${text.trimEnd()}\n\n${GENERATED_MARKER}\n`;
|
|
2723
|
+
}
|
|
2724
|
+
function sourceOf({ fields, rest }, ending) {
|
|
2725
|
+
const lines = rest.split("\n");
|
|
2352
2726
|
const h1 = leadingH1(lines);
|
|
2353
2727
|
const start = h1 ? h1.index + 1 : 0;
|
|
2354
|
-
const
|
|
2355
|
-
const end = trailing === -1 ? lines.length : trailing;
|
|
2728
|
+
const end = bodyEnd(lines, start, ending);
|
|
2356
2729
|
const fallbackTitle = typeof fields["title"] === "string" ? fields["title"] : "";
|
|
2357
2730
|
return {
|
|
2358
2731
|
fields,
|
|
@@ -2388,45 +2761,6 @@ function scalarString(value) {
|
|
|
2388
2761
|
if (typeof value === "number") return String(value);
|
|
2389
2762
|
return "";
|
|
2390
2763
|
}
|
|
2391
|
-
function frontmatter(fields) {
|
|
2392
|
-
const lines = ["---"];
|
|
2393
|
-
for (const [key, value] of Object.entries(fields)) if (Array.isArray(value)) {
|
|
2394
|
-
if (value.length === 0) lines.push(`${key}: []`);
|
|
2395
|
-
else lines.push(`${key}:`, ...value.map((v) => ` - ${quote(v)}`));
|
|
2396
|
-
} else if (typeof value === "number") lines.push(`${key}: ${value}`);
|
|
2397
|
-
else lines.push(`${key}: ${quote(value)}`);
|
|
2398
|
-
lines.push("---");
|
|
2399
|
-
return lines.join("\n");
|
|
2400
|
-
}
|
|
2401
|
-
function parseFrontmatter(block) {
|
|
2402
|
-
const fields = {};
|
|
2403
|
-
const lines = block.split("\n");
|
|
2404
|
-
for (let i = 0; i < lines.length; i++) {
|
|
2405
|
-
const m = lines[i]?.match(FRONTMATTER_KEY);
|
|
2406
|
-
if (!m) continue;
|
|
2407
|
-
const key = m[1] ?? "";
|
|
2408
|
-
const raw = (m[2] ?? "").trim();
|
|
2409
|
-
if (raw === "[]") fields[key] = [];
|
|
2410
|
-
else if (raw === "") {
|
|
2411
|
-
const items = [];
|
|
2412
|
-
for (let item; item = lines[i + 1]?.match(FRONTMATTER_LIST_ITEM); i++) items.push(unquote((item[1] ?? "").trim()));
|
|
2413
|
-
fields[key] = items;
|
|
2414
|
-
} else fields[key] = scalar(raw);
|
|
2415
|
-
}
|
|
2416
|
-
return fields;
|
|
2417
|
-
}
|
|
2418
|
-
function scalar(raw) {
|
|
2419
|
-
if (raw.startsWith("\"")) return unquote(raw);
|
|
2420
|
-
if (BARE_NUMBER.test(raw)) return Number(raw);
|
|
2421
|
-
return raw;
|
|
2422
|
-
}
|
|
2423
|
-
function quote(value) {
|
|
2424
|
-
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
2425
|
-
}
|
|
2426
|
-
function unquote(value) {
|
|
2427
|
-
if (!value.startsWith("\"") || !value.endsWith("\"")) return value;
|
|
2428
|
-
return value.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
2429
|
-
}
|
|
2430
2764
|
function leadingH1(lines) {
|
|
2431
2765
|
for (const [index, line] of lines.entries()) {
|
|
2432
2766
|
if (line.startsWith("# ")) return {
|
|
@@ -2437,6 +2771,16 @@ function leadingH1(lines) {
|
|
|
2437
2771
|
}
|
|
2438
2772
|
return null;
|
|
2439
2773
|
}
|
|
2774
|
+
function bodyEnd(lines, start, ending) {
|
|
2775
|
+
const after = (test) => lines.findIndex((line, i) => i >= start && test(line));
|
|
2776
|
+
const marker = after(isMarker);
|
|
2777
|
+
if (marker !== -1) return marker;
|
|
2778
|
+
const legacy = ending === "marker" ? -1 : after((line) => LEGACY_SECTION_HEADING.test(line));
|
|
2779
|
+
return legacy === -1 ? lines.length : legacy;
|
|
2780
|
+
}
|
|
2781
|
+
function isMarker(line) {
|
|
2782
|
+
return line.trimEnd() === GENERATED_MARKER;
|
|
2783
|
+
}
|
|
2440
2784
|
function commentsSection(comments) {
|
|
2441
2785
|
if (comments.length === 0) return "";
|
|
2442
2786
|
const blocks = comments.map((c) => {
|
|
@@ -2459,6 +2803,44 @@ function attachmentsSection(attachments) {
|
|
|
2459
2803
|
].join("\n");
|
|
2460
2804
|
}
|
|
2461
2805
|
//#endregion
|
|
2806
|
+
//#region src/commands/page-ref.ts
|
|
2807
|
+
const PAGE_REF = {
|
|
2808
|
+
message: "Confluence page id or URL:",
|
|
2809
|
+
flag: "[page]",
|
|
2810
|
+
parse: parsePageId,
|
|
2811
|
+
notFound: (raw) => `Could not find a page id in "${raw}".`
|
|
2812
|
+
};
|
|
2813
|
+
async function pageIdOrFile({ term, files }, arg) {
|
|
2814
|
+
if (!arg?.endsWith(".md")) return resolveRef(term.ask, arg, PAGE_REF);
|
|
2815
|
+
const id = scalarString(parse(await files.readText(arg)).fields["id"]);
|
|
2816
|
+
if (!id) throw new Error(`${arg} has no page \`id\` in its frontmatter.`);
|
|
2817
|
+
return id;
|
|
2818
|
+
}
|
|
2819
|
+
//#endregion
|
|
2820
|
+
//#region src/commands/confluence-star.ts
|
|
2821
|
+
async function confluenceStar(env, arg) {
|
|
2822
|
+
await starById(env, await pageIdOrFile(env, arg));
|
|
2823
|
+
}
|
|
2824
|
+
async function starById({ session, term }, id) {
|
|
2825
|
+
await starPage(session, id);
|
|
2826
|
+
term.out(`Starred page ${id}.`);
|
|
2827
|
+
}
|
|
2828
|
+
async function confluenceUnstar(env, arg) {
|
|
2829
|
+
const id = await pageIdOrFile(env, arg);
|
|
2830
|
+
await unstarPage(env.session, id);
|
|
2831
|
+
env.term.out(`Unstarred page ${id}.`);
|
|
2832
|
+
}
|
|
2833
|
+
//#endregion
|
|
2834
|
+
//#region src/api/attachments.ts
|
|
2835
|
+
function mediaResolver(downloaded) {
|
|
2836
|
+
const byMediaId = new Map(downloaded.map((d) => [d.mediaId, d.relativePath]));
|
|
2837
|
+
const byFilename = new Map(downloaded.map((d) => [d.filename, d.relativePath]));
|
|
2838
|
+
return (media) => {
|
|
2839
|
+
if (media.id && byMediaId.has(media.id)) return byMediaId.get(media.id);
|
|
2840
|
+
if (media.alt && byFilename.has(media.alt)) return byFilename.get(media.alt);
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
//#endregion
|
|
2462
2844
|
//#region src/copy/plan.ts
|
|
2463
2845
|
function planIssueCopy(issue, out) {
|
|
2464
2846
|
return planCopy({
|
|
@@ -2481,19 +2863,22 @@ function planIssueCopy(issue, out) {
|
|
|
2481
2863
|
attachments: issue.attachments
|
|
2482
2864
|
}, out);
|
|
2483
2865
|
}
|
|
2866
|
+
function pageFields(page) {
|
|
2867
|
+
return {
|
|
2868
|
+
title: page.title,
|
|
2869
|
+
id: page.id,
|
|
2870
|
+
space: page.spaceKey,
|
|
2871
|
+
version: page.version,
|
|
2872
|
+
author: page.author,
|
|
2873
|
+
created: page.createdAt,
|
|
2874
|
+
updated: page.updatedAt,
|
|
2875
|
+
url: page.url
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2484
2878
|
function planPageCopy(page, out) {
|
|
2485
2879
|
return planCopy({
|
|
2486
2880
|
baseName: `${page.id}-${slugify(page.title)}`,
|
|
2487
|
-
fields:
|
|
2488
|
-
title: page.title,
|
|
2489
|
-
id: page.id,
|
|
2490
|
-
space: page.spaceKey,
|
|
2491
|
-
version: page.version,
|
|
2492
|
-
author: page.author,
|
|
2493
|
-
created: page.createdAt,
|
|
2494
|
-
updated: page.updatedAt,
|
|
2495
|
-
url: page.url
|
|
2496
|
-
},
|
|
2881
|
+
fields: pageFields(page),
|
|
2497
2882
|
title: page.title,
|
|
2498
2883
|
body: page.body,
|
|
2499
2884
|
comments: page.comments,
|
|
@@ -2564,246 +2949,235 @@ function numberedName(name, suffix) {
|
|
|
2564
2949
|
return `${dot > 0 ? name.slice(0, dot) : name}-${suffix}${dot > 0 ? name.slice(dot) : ""}`;
|
|
2565
2950
|
}
|
|
2566
2951
|
//#endregion
|
|
2567
|
-
//#region src/
|
|
2568
|
-
|
|
2569
|
-
const
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
landed.push(attachment);
|
|
2578
|
-
} catch (err) {
|
|
2579
|
-
term.err(` ! could not download ${attachment.filename}: ${err.message}`);
|
|
2580
|
-
}
|
|
2581
|
-
return landed;
|
|
2582
|
-
}
|
|
2583
|
-
function wroteLine(filePath, attachmentCount) {
|
|
2584
|
-
return `Wrote ${filePath}${attachmentCount > 0 ? ` (+${attachmentCount} attachment${attachmentCount === 1 ? "" : "s"})` : ""}`;
|
|
2585
|
-
}
|
|
2586
|
-
//#endregion
|
|
2587
|
-
//#region src/adf/lossy.ts
|
|
2588
|
-
const LOSSY_LABELS = {
|
|
2589
|
-
panel: "panel",
|
|
2590
|
-
expand: "expand",
|
|
2591
|
-
nestedExpand: "expand",
|
|
2592
|
-
decisionList: "decision list",
|
|
2593
|
-
layoutSection: "layout",
|
|
2594
|
-
extension: "macro",
|
|
2595
|
-
bodiedExtension: "macro",
|
|
2596
|
-
inlineExtension: "macro"
|
|
2597
|
-
};
|
|
2598
|
-
const JIRA_LOSSY_LABELS = {
|
|
2599
|
-
...LOSSY_LABELS,
|
|
2600
|
-
media: "image",
|
|
2601
|
-
mediaInline: "image"
|
|
2602
|
-
};
|
|
2603
|
-
function findLossyNodes(node, lossyLabels = LOSSY_LABELS) {
|
|
2604
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2605
|
-
const visit = (n) => {
|
|
2606
|
-
const label = lossyLabels[n.type];
|
|
2607
|
-
if (label) counts.set(label, (counts.get(label) ?? 0) + 1);
|
|
2608
|
-
for (const child of n.content ?? []) visit(child);
|
|
2609
|
-
};
|
|
2610
|
-
if (node) visit(node);
|
|
2611
|
-
return counts;
|
|
2612
|
-
}
|
|
2613
|
-
function formatLossy(counts) {
|
|
2614
|
-
return [...counts.entries()].map(([label, n]) => `${n} ${label}${n === 1 ? "" : "s"}`).join(", ");
|
|
2615
|
-
}
|
|
2616
|
-
//#endregion
|
|
2617
|
-
//#region src/update/plan.ts
|
|
2618
|
-
function planIssueUpdate(source, issue, options) {
|
|
2619
|
-
const images = imageHrefs(source.body).map((href) => ({
|
|
2620
|
-
href,
|
|
2621
|
-
kind: isExternalHref(href) ? "external" : "unsupported"
|
|
2622
|
-
}));
|
|
2623
|
-
const unsupported = images.filter((i) => i.kind === "unsupported").map((i) => i.href);
|
|
2624
|
-
const body = markdownToAdf(source.body, { resolveImage: (href, alt) => isExternalHref(href) ? externalMedia(href, alt) : void 0 });
|
|
2625
|
-
const refusals = [...unsupported.length > 0 ? [`jira update does not support image changes yet. Remove local image reference(s) or edit text only: ${unsupported.join(", ")}`] : [], ...emptyBodyRefusal(body)];
|
|
2626
|
-
return withVerdict({
|
|
2627
|
-
noun: "issue",
|
|
2628
|
-
id: source.key,
|
|
2629
|
-
headline: headline("summary", issue.summary, source.title, options.summary),
|
|
2630
|
-
revision: revision(source.updatedAtCopy, issue.updated),
|
|
2631
|
-
lossy: findLossyNodes(issue.description, JIRA_LOSSY_LABELS),
|
|
2952
|
+
//#region src/create/page-plan.ts
|
|
2953
|
+
function planPageCreate(draft, target, localImages) {
|
|
2954
|
+
const images = imageHrefs(draft.body).map((href) => ({
|
|
2955
|
+
href,
|
|
2956
|
+
kind: imageKind(href, localImages)
|
|
2957
|
+
}));
|
|
2958
|
+
const missing = images.filter((i) => i.kind === "missing").map((i) => i.href);
|
|
2959
|
+
return {
|
|
2960
|
+
...target,
|
|
2961
|
+
title: draft.title,
|
|
2632
2962
|
images,
|
|
2633
|
-
|
|
2634
|
-
body,
|
|
2635
|
-
refusals
|
|
2636
|
-
|
|
2963
|
+
body: markdownToAdf(draft.body, { resolveImage: externalImagesOnly }),
|
|
2964
|
+
warnings: htmlWarnings(draft.body),
|
|
2965
|
+
refusals: [
|
|
2966
|
+
...draft.title ? [] : ["No title. Add an H1 or a `title` to the frontmatter."],
|
|
2967
|
+
...spaceRefusal(target),
|
|
2968
|
+
...missingImagesRefusal(missing)
|
|
2969
|
+
]
|
|
2970
|
+
};
|
|
2637
2971
|
}
|
|
2638
|
-
function
|
|
2639
|
-
const lines = [`Dry run for
|
|
2640
|
-
if (plan.
|
|
2972
|
+
function formatCreatePlan(plan) {
|
|
2973
|
+
const lines = [`Dry run for new page "${plan.title}"`];
|
|
2974
|
+
if (plan.spaceId) lines.push(row("space", plan.space));
|
|
2975
|
+
lines.push(row("parent", plan.parentId ?? "space homepage"));
|
|
2641
2976
|
const counts = imageCounts(plan.images);
|
|
2642
2977
|
if (counts) lines.push(row("images", counts));
|
|
2643
2978
|
for (const refusal of plan.refusals) lines.push(row("blocked", refusal));
|
|
2644
|
-
|
|
2645
|
-
if (plan.revision.stale) lines.push(row("stale", `${revisionText(plan.revision)} (would refuse without --force)`));
|
|
2979
|
+
for (const warning of plan.warnings) lines.push(row("warning", warning));
|
|
2646
2980
|
lines.push(" nothing was written (dry run)");
|
|
2647
2981
|
return lines;
|
|
2648
2982
|
}
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
["reuse", "reused"],
|
|
2653
|
-
["external", "external"],
|
|
2654
|
-
["unsupported", "unsupported"],
|
|
2655
|
-
["missing", "missing"]
|
|
2656
|
-
];
|
|
2657
|
-
function imageCounts(images) {
|
|
2658
|
-
return IMAGE_COUNT_LABELS.map(([kind, label]) => ({
|
|
2659
|
-
n: images.filter((i) => i.kind === kind).length,
|
|
2660
|
-
label
|
|
2661
|
-
})).filter(({ n }) => n > 0).map(({ n, label }) => `${n} ${label}`).join(", ");
|
|
2983
|
+
function imageKind(href, localImages) {
|
|
2984
|
+
if (isExternalHref(href)) return "external";
|
|
2985
|
+
return localImages.find((l) => l.href === href)?.size === void 0 ? "missing" : "upload";
|
|
2662
2986
|
}
|
|
2663
|
-
function
|
|
2664
|
-
return `
|
|
2987
|
+
function spaceRefusal({ space, spaceId }) {
|
|
2988
|
+
if (!space) return ["No space. Pass --space or add `space` to the frontmatter."];
|
|
2989
|
+
return spaceId ? [] : [`Space "${space}" not found.`];
|
|
2665
2990
|
}
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2991
|
+
//#endregion
|
|
2992
|
+
//#region src/commands/confluence-create.ts
|
|
2993
|
+
async function confluenceCreate(env, arg, options) {
|
|
2994
|
+
const { session, term, files } = env;
|
|
2995
|
+
const file = arg ?? await term.ask.text({
|
|
2996
|
+
message: "Path to the Markdown file:",
|
|
2997
|
+
flag: "[file]",
|
|
2998
|
+
required: true
|
|
2999
|
+
});
|
|
3000
|
+
const raw = await files.readText(file);
|
|
3001
|
+
const draft = parseDraft(raw);
|
|
3002
|
+
const existing = scalarString(draft.fields["id"]);
|
|
3003
|
+
if (existing) throw new Error(`${file} already has page id ${existing}. Use \`atlass confluence update\` instead.`);
|
|
3004
|
+
const parent = parentOf(options.parent);
|
|
3005
|
+
const space = options.space ?? scalarString(draft.fields["space"]);
|
|
3006
|
+
const spaceId = space ? await findSpaceId(session, space) : null;
|
|
3007
|
+
const localImages = await statLocalImages(files, dirname(resolve(file)), draft.body);
|
|
3008
|
+
const plan = planPageCreate(draft, {
|
|
3009
|
+
space,
|
|
3010
|
+
spaceId,
|
|
3011
|
+
...parent
|
|
3012
|
+
}, localImages);
|
|
3013
|
+
if (options.dryRun) term.out(formatCreatePlan(plan));
|
|
3014
|
+
if (plan.refusals.length > 0 || !plan.spaceId) throw new Error(plan.refusals.join("\n"));
|
|
3015
|
+
if (options.dryRun) return;
|
|
3016
|
+
for (const warning of plan.warnings) term.err(`warning: ${warning}`);
|
|
3017
|
+
const created = await createPage(session, {
|
|
3018
|
+
...plan,
|
|
3019
|
+
spaceId: plan.spaceId
|
|
3020
|
+
});
|
|
3021
|
+
const createdFields = {
|
|
3022
|
+
title: plan.title,
|
|
3023
|
+
id: created.id,
|
|
3024
|
+
space,
|
|
3025
|
+
version: created.version
|
|
2671
3026
|
};
|
|
3027
|
+
await files.writeText(file, editKeepingLayout(raw, (text) => withGeneratedMarker(withFields(text, createdFields))));
|
|
3028
|
+
if (plan.images.some((i) => i.kind === "upload")) await addImages(env, draft, created, localImages);
|
|
3029
|
+
const page = await fetchPage(session, session.site, created.id);
|
|
3030
|
+
await files.writeText(file, rewriteFields(await files.readText(file), pageFields(page)));
|
|
3031
|
+
term.out(`Created page ${page.id} in ${page.spaceKey}: ${page.url}`);
|
|
3032
|
+
if (options.star) await starCreated(env, page.id, file);
|
|
3033
|
+
}
|
|
3034
|
+
function parentOf(parent) {
|
|
3035
|
+
if (parent === void 0) return {};
|
|
3036
|
+
const parentId = parsePageId(parent);
|
|
3037
|
+
if (!parentId) throw new Error(`Could not find a page id in --parent "${parent}".`);
|
|
3038
|
+
return { parentId };
|
|
3039
|
+
}
|
|
3040
|
+
async function addImages(env, draft, created, localImages) {
|
|
3041
|
+
const { body, uploads } = planPageBody({
|
|
3042
|
+
id: created.id,
|
|
3043
|
+
body: draft.body
|
|
3044
|
+
}, [], localImages);
|
|
3045
|
+
await pushPage(env, {
|
|
3046
|
+
title: draft.title,
|
|
3047
|
+
body,
|
|
3048
|
+
uploads
|
|
3049
|
+
}, {
|
|
3050
|
+
id: created.id,
|
|
3051
|
+
nextVersion: created.version + 1,
|
|
3052
|
+
message: "Added images via atlass"
|
|
3053
|
+
});
|
|
2672
3054
|
}
|
|
2673
|
-
function
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
3055
|
+
async function starCreated(env, id, file) {
|
|
3056
|
+
try {
|
|
3057
|
+
await starById(env, id);
|
|
3058
|
+
} catch (err) {
|
|
3059
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3060
|
+
throw new Error(`Created page ${id} but could not star it: ${reason}. Run \`atlass confluence star ${file}\`.`);
|
|
3061
|
+
}
|
|
2679
3062
|
}
|
|
2680
|
-
|
|
2681
|
-
|
|
3063
|
+
//#endregion
|
|
3064
|
+
//#region src/api/query.ts
|
|
3065
|
+
function quote(value) {
|
|
3066
|
+
return `"${value.replace(/(["\\])/g, "\\$1")}"`;
|
|
2682
3067
|
}
|
|
2683
|
-
function
|
|
2684
|
-
|
|
2685
|
-
return ["Refusing to update: the converted body is empty."];
|
|
3068
|
+
function values(given) {
|
|
3069
|
+
return (given ?? []).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
2686
3070
|
}
|
|
2687
|
-
function
|
|
2688
|
-
return
|
|
2689
|
-
...plan,
|
|
2690
|
-
verdict: verdict(plan, force)
|
|
2691
|
-
};
|
|
3071
|
+
function inClause(field, given) {
|
|
3072
|
+
return renderClause(field, values(given).map(quote));
|
|
2692
3073
|
}
|
|
2693
|
-
function
|
|
2694
|
-
|
|
2695
|
-
kind: "refuse",
|
|
2696
|
-
message: plan.refusals.join("\n")
|
|
2697
|
-
};
|
|
2698
|
-
const noun = plan.noun;
|
|
2699
|
-
if (plan.revision.stale && !force) return {
|
|
2700
|
-
kind: "refuse",
|
|
2701
|
-
message: `${capitalize(noun)} changed on the server since you copied it (${revisionText(plan.revision)}). Re-copy the ${noun} or pass --force.`
|
|
2702
|
-
};
|
|
2703
|
-
if (plan.lossy.size > 0 && !force) return {
|
|
2704
|
-
kind: "confirm",
|
|
2705
|
-
message: `This ${noun} contains ${formatLossy(plan.lossy)} that Markdown cannot represent and will be removed. Continue?`
|
|
2706
|
-
};
|
|
2707
|
-
return { kind: "proceed" };
|
|
3074
|
+
function userClause(field, given) {
|
|
3075
|
+
return renderClause(field, values(given).map((v) => v === "me" ? "currentUser()" : quote(v)));
|
|
2708
3076
|
}
|
|
2709
|
-
function
|
|
2710
|
-
|
|
3077
|
+
function renderClause(field, rendered) {
|
|
3078
|
+
if (rendered.length === 0) return null;
|
|
3079
|
+
if (rendered.length === 1) return `${field} = ${rendered[0]}`;
|
|
3080
|
+
return `${field} in (${rendered.join(", ")})`;
|
|
3081
|
+
}
|
|
3082
|
+
function joinClauses(clauses, order) {
|
|
3083
|
+
return `${clauses.filter((c) => c !== null).join(" AND ")} ORDER BY ${order}`;
|
|
2711
3084
|
}
|
|
2712
3085
|
//#endregion
|
|
2713
|
-
//#region src/
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
noun: "page",
|
|
2732
|
-
id: source.id,
|
|
2733
|
-
headline: headline("title", state.title, source.title, options.title),
|
|
2734
|
-
revision: revision(`v${source.version}`, `v${state.version}`),
|
|
2735
|
-
lossy: findLossyNodes(state.body),
|
|
2736
|
-
images: entries.map(({ href, kind }) => ({
|
|
2737
|
-
href,
|
|
2738
|
-
kind
|
|
2739
|
-
})),
|
|
2740
|
-
uploads: entries.flatMap((e) => e.kind === "upload" || e.kind === "changed" ? [{
|
|
2741
|
-
href: e.href,
|
|
2742
|
-
path: e.path,
|
|
2743
|
-
filename: e.filename
|
|
2744
|
-
}] : []),
|
|
2745
|
-
body,
|
|
2746
|
-
refusals
|
|
2747
|
-
}, options.force ?? false);
|
|
3086
|
+
//#region src/util/html.ts
|
|
3087
|
+
const MAX_CODE_POINT = 1114111;
|
|
3088
|
+
const NAMED = {
|
|
3089
|
+
amp: "&",
|
|
3090
|
+
lt: "<",
|
|
3091
|
+
gt: ">",
|
|
3092
|
+
quot: "\"",
|
|
3093
|
+
apos: "'",
|
|
3094
|
+
nbsp: " "
|
|
3095
|
+
};
|
|
3096
|
+
function decodeEntities(text) {
|
|
3097
|
+
return text.replace(/&(#x?[0-9a-f]+|[a-z]+);/gi, (match, body) => {
|
|
3098
|
+
if (body[0] === "#") {
|
|
3099
|
+
const code = body[1] === "x" || body[1] === "X" ? Number.parseInt(body.slice(2), 16) : Number.parseInt(body.slice(1), 10);
|
|
3100
|
+
return isCodePoint(code) ? String.fromCodePoint(code) : match;
|
|
3101
|
+
}
|
|
3102
|
+
return NAMED[body.toLowerCase()] ?? match;
|
|
3103
|
+
});
|
|
2748
3104
|
}
|
|
2749
|
-
function
|
|
2750
|
-
|
|
2751
|
-
href,
|
|
2752
|
-
kind: "external"
|
|
2753
|
-
};
|
|
2754
|
-
const local = localImages.find((l) => l.href === href);
|
|
2755
|
-
if (!local || local.size === void 0) return {
|
|
2756
|
-
href,
|
|
2757
|
-
kind: "missing"
|
|
2758
|
-
};
|
|
2759
|
-
const existing = attachments.find((a) => a.filename === local.filename);
|
|
2760
|
-
if (existing && existing.size === local.size) return {
|
|
2761
|
-
href,
|
|
2762
|
-
kind: "reuse",
|
|
2763
|
-
fileId: existing.fileId
|
|
2764
|
-
};
|
|
2765
|
-
return {
|
|
2766
|
-
href,
|
|
2767
|
-
kind: existing ? "changed" : "upload",
|
|
2768
|
-
path: local.path,
|
|
2769
|
-
filename: local.filename
|
|
2770
|
-
};
|
|
3105
|
+
function isCodePoint(code) {
|
|
3106
|
+
return Number.isFinite(code) && code >= 0 && code <= MAX_CODE_POINT;
|
|
2771
3107
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
content: doc.content.map((n) => withUploadedId(n, ids))
|
|
2777
|
-
};
|
|
3108
|
+
//#endregion
|
|
3109
|
+
//#region src/api/confluence-search.ts
|
|
3110
|
+
async function searchPages(client, site, params) {
|
|
3111
|
+
return searchPagesByCql(client, site, buildCql(params), params.limit);
|
|
2778
3112
|
}
|
|
2779
|
-
function
|
|
2780
|
-
const
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
} : node;
|
|
2788
|
-
if (!swapped.content) return swapped;
|
|
3113
|
+
async function searchPagesByCql(client, site, cql, limit) {
|
|
3114
|
+
const query = new URLSearchParams({
|
|
3115
|
+
cql,
|
|
3116
|
+
limit: String(limit),
|
|
3117
|
+
expand: "content.space"
|
|
3118
|
+
});
|
|
3119
|
+
const results = (await client.getJson(`/wiki/rest/api/search?${query.toString()}`)).results ?? [];
|
|
3120
|
+
const serverPageWasFull = results.length === limit;
|
|
2789
3121
|
return {
|
|
2790
|
-
|
|
2791
|
-
|
|
3122
|
+
pages: results.filter((r) => r.content?.id).map((r) => ({
|
|
3123
|
+
id: r.content?.id ?? "",
|
|
3124
|
+
space: r.content?.space?.key ?? spaceKeyOf(r.resultGlobalContainer?.displayUrl),
|
|
3125
|
+
title: decodeEntities(r.content?.title ?? r.title ?? ""),
|
|
3126
|
+
updated: r.lastModified ?? "",
|
|
3127
|
+
url: r.url ? `${site}/wiki${r.url}` : ""
|
|
3128
|
+
})),
|
|
3129
|
+
hasMore: serverPageWasFull
|
|
2792
3130
|
};
|
|
2793
3131
|
}
|
|
3132
|
+
const SPACE_URL = /\/spaces\/([^/?#]+)/;
|
|
3133
|
+
function spaceKeyOf(displayUrl) {
|
|
3134
|
+
return SPACE_URL.exec(displayUrl ?? "")?.[1] ?? "";
|
|
3135
|
+
}
|
|
3136
|
+
function buildCql(params) {
|
|
3137
|
+
return joinClauses([
|
|
3138
|
+
"type = page",
|
|
3139
|
+
params.starred ? "favourite = currentUser()" : null,
|
|
3140
|
+
inClause("space", params.space),
|
|
3141
|
+
inClause("label", params.label),
|
|
3142
|
+
params.updatedSince ? `lastmodified >= ${quote(params.updatedSince)}` : null,
|
|
3143
|
+
params.text ? `text ~ ${quote(params.text)}` : null
|
|
3144
|
+
], "lastmodified DESC");
|
|
3145
|
+
}
|
|
3146
|
+
//#endregion
|
|
3147
|
+
//#region src/copy/run.ts
|
|
3148
|
+
async function runCopy(env, plan) {
|
|
3149
|
+
const landed = await download(env, plan);
|
|
3150
|
+
await env.files.writeText(plan.filePath, renderCopy(plan, landed));
|
|
3151
|
+
env.term.out(wroteLine(plan.filePath, landed.length));
|
|
3152
|
+
}
|
|
3153
|
+
async function download({ term, files, session }, plan) {
|
|
3154
|
+
const landed = [];
|
|
3155
|
+
for (const { path, ...attachment } of plan.downloads) try {
|
|
3156
|
+
await files.writeBytes(path, await session.getBinary(attachment.url));
|
|
3157
|
+
landed.push(attachment);
|
|
3158
|
+
} catch (err) {
|
|
3159
|
+
term.err(` ! could not download ${attachment.filename}: ${err.message}`);
|
|
3160
|
+
}
|
|
3161
|
+
return landed;
|
|
3162
|
+
}
|
|
3163
|
+
function wroteLine(filePath, attachmentCount) {
|
|
3164
|
+
return `Wrote ${filePath}${attachmentCount > 0 ? ` (+${attachmentCount} attachment${attachmentCount === 1 ? "" : "s"})` : ""}`;
|
|
3165
|
+
}
|
|
2794
3166
|
//#endregion
|
|
2795
3167
|
//#region src/update/run.ts
|
|
2796
3168
|
async function runPlan(term, plan, options, push) {
|
|
3169
|
+
const verdict = plan.verdict;
|
|
2797
3170
|
if (options.dryRun) {
|
|
2798
3171
|
term.out(formatPlan(plan));
|
|
3172
|
+
if (verdict.kind === "refuse") throw new Error(verdict.message);
|
|
3173
|
+
if (verdict.kind === "confirm" && !term.interactive) throw new NotInteractiveError(forcePrompt(verdict.message));
|
|
2799
3174
|
return;
|
|
2800
3175
|
}
|
|
2801
|
-
const
|
|
3176
|
+
for (const warning of plan.warnings) term.err(`warning: ${warning}`);
|
|
2802
3177
|
if (verdict.kind === "refuse") throw new Error(verdict.message);
|
|
2803
3178
|
if (verdict.kind === "confirm") {
|
|
2804
3179
|
if (!await term.ask.confirm({
|
|
2805
|
-
|
|
2806
|
-
flag: "--force",
|
|
3180
|
+
...forcePrompt(verdict.message),
|
|
2807
3181
|
default: false
|
|
2808
3182
|
})) {
|
|
2809
3183
|
term.out("Aborted.");
|
|
@@ -2812,6 +3186,12 @@ async function runPlan(term, plan, options, push) {
|
|
|
2812
3186
|
}
|
|
2813
3187
|
await push();
|
|
2814
3188
|
}
|
|
3189
|
+
function forcePrompt(message) {
|
|
3190
|
+
return {
|
|
3191
|
+
message,
|
|
3192
|
+
flag: "--force"
|
|
3193
|
+
};
|
|
3194
|
+
}
|
|
2815
3195
|
//#endregion
|
|
2816
3196
|
//#region src/commands/confluence.ts
|
|
2817
3197
|
async function confluenceView({ session, term }, arg, options) {
|
|
@@ -2822,7 +3202,7 @@ async function confluenceView({ session, term }, arg, options) {
|
|
|
2822
3202
|
function formatPageView(page, nowMs, allComments) {
|
|
2823
3203
|
return [
|
|
2824
3204
|
kleur.bold(page.title),
|
|
2825
|
-
...fieldLines([
|
|
3205
|
+
...fieldLines$1([
|
|
2826
3206
|
["Space", page.spaceKey],
|
|
2827
3207
|
["ID", page.id],
|
|
2828
3208
|
["Version", page.version ? String(page.version) : ""],
|
|
@@ -2839,60 +3219,44 @@ function formatPageView(page, nowMs, allComments) {
|
|
|
2839
3219
|
async function confluenceCopy(env, arg, options) {
|
|
2840
3220
|
await copyPage(env, await resolveRef(env.term.ask, arg, PAGE_REF), options.out);
|
|
2841
3221
|
}
|
|
2842
|
-
async function confluenceUpdate(
|
|
3222
|
+
async function confluenceUpdate(env, arg, options) {
|
|
3223
|
+
const { session, term, files } = env;
|
|
2843
3224
|
const file = arg ?? await term.ask.text({
|
|
2844
3225
|
message: "Path to the page Markdown file:",
|
|
2845
3226
|
flag: "[file]",
|
|
2846
3227
|
required: true
|
|
2847
3228
|
});
|
|
2848
|
-
const
|
|
3229
|
+
const raw = await files.readText(file);
|
|
3230
|
+
const src = parsePageSource(raw);
|
|
2849
3231
|
const state = await fetchPageState(session, src.id);
|
|
2850
|
-
const plan = planPageUpdate(src, state, await listAttachments(session, src.id), await statLocalImages(files, dirname(resolve(file)), src.body),
|
|
3232
|
+
const plan = planPageUpdate(src, state, await listAttachments(session, src.id), await statLocalImages(files, dirname(resolve(file)), src.body), {
|
|
3233
|
+
...options,
|
|
3234
|
+
file
|
|
3235
|
+
});
|
|
2851
3236
|
await runPlan(term, plan, options, async () => {
|
|
2852
|
-
const
|
|
2853
|
-
for (const upload of plan.uploads) {
|
|
2854
|
-
term.err(`Uploading ${upload.filename} ...`);
|
|
2855
|
-
const bytes = await files.readBytes(upload.path);
|
|
2856
|
-
ids.set(upload.href, await uploadAttachment(session, src.id, upload.filename, bytes));
|
|
2857
|
-
}
|
|
2858
|
-
const version = await updatePage(session, src.id, {
|
|
3237
|
+
const version = await pushPage(env, {
|
|
2859
3238
|
title: plan.headline.next,
|
|
3239
|
+
body: plan.body,
|
|
3240
|
+
uploads: plan.uploads
|
|
3241
|
+
}, {
|
|
3242
|
+
id: src.id,
|
|
2860
3243
|
nextVersion: state.version + 1,
|
|
2861
|
-
body: withUploadedIds(plan.body, ids),
|
|
2862
3244
|
message: options.message ?? "Updated via atlass"
|
|
2863
3245
|
});
|
|
2864
|
-
term.out(`Updated page ${src.id} to version ${version}.`);
|
|
3246
|
+
term.out(`Updated page ${src.id} to version ${version.number}.`);
|
|
3247
|
+
await files.writeText(file, rewriteFields(raw, pushedFields(plan, version)));
|
|
2865
3248
|
});
|
|
2866
3249
|
}
|
|
2867
|
-
|
|
2868
|
-
const
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
href,
|
|
2873
|
-
path,
|
|
2874
|
-
filename: basename(path),
|
|
2875
|
-
...await fileSize(files, path)
|
|
2876
|
-
};
|
|
2877
|
-
}));
|
|
2878
|
-
}
|
|
2879
|
-
async function fileSize(files, path) {
|
|
2880
|
-
try {
|
|
2881
|
-
return { size: await files.size(path) };
|
|
2882
|
-
} catch {
|
|
2883
|
-
return {};
|
|
2884
|
-
}
|
|
3250
|
+
function pushedFields(plan, version) {
|
|
3251
|
+
const fields = { version: version.number };
|
|
3252
|
+
if (version.createdAt) fields["updated"] = version.createdAt;
|
|
3253
|
+
if (plan.headline.next !== plan.headline.current) fields["title"] = plan.headline.next;
|
|
3254
|
+
return fields;
|
|
2885
3255
|
}
|
|
2886
3256
|
async function copyPage(env, id, out) {
|
|
2887
3257
|
env.term.err(`Fetching page ${id} ...`);
|
|
2888
3258
|
await runCopy(env, planPageCopy(await fetchPage(env.session, env.session.site, id), out));
|
|
2889
3259
|
}
|
|
2890
|
-
const PAGE_REF = {
|
|
2891
|
-
message: "Confluence page id or URL:",
|
|
2892
|
-
flag: "[page]",
|
|
2893
|
-
parse: parsePageId,
|
|
2894
|
-
notFound: (raw) => `Could not find a page id in "${raw}".`
|
|
2895
|
-
};
|
|
2896
3260
|
//#endregion
|
|
2897
3261
|
//#region src/commands/confluence-search.ts
|
|
2898
3262
|
async function confluenceSearch(env, query, options) {
|
|
@@ -2963,6 +3327,9 @@ function registerConfluence(confluence) {
|
|
|
2963
3327
|
confluence.command("view [page]").description("Show a Confluence page (id or URL) in the terminal").option("--all-comments", "show all comments instead of the last 5").option("--no-pager", "print directly instead of paging long output").action(jiraAction(confluenceView));
|
|
2964
3328
|
confluence.command("copy [page]").description("Copy a Confluence page (id or URL) to a Markdown file").option("-o, --out <path>", "output file or directory").action(jiraAction(confluenceCopy));
|
|
2965
3329
|
confluence.command("update [file]").description("Update a Confluence page from an edited Markdown file").option("--title", "also push the H1 as the page title").option("-m, --message <text>", "version message (default 'Updated via atlass')").option("-f, --force", "skip the stale-version and data-loss checks").option("--dry-run", "show what would change without writing").action(jiraAction(confluenceUpdate));
|
|
3330
|
+
confluence.command("create [file]").description("Create a Confluence page from a Markdown file").option("-s, --space <key>", "space to create it in (default: the frontmatter space)").option("--parent <page>", "parent page id or URL (default: the space homepage)").option("--star", "star the new page").option("--dry-run", "show what would be created without writing").action(jiraAction(confluenceCreate));
|
|
3331
|
+
confluence.command("star [page]").description("Star a Confluence page (id, URL, or copied file)").action(jiraAction(confluenceStar));
|
|
3332
|
+
confluence.command("unstar [page]").description("Unstar a Confluence page (id, URL, or copied file)").action(jiraAction(confluenceUnstar));
|
|
2966
3333
|
outputOptions(confluence.command("list").description("List pages you starred").option("-s, --space <key>", "limit to a space")).action(jiraAction(confluenceList));
|
|
2967
3334
|
outputOptions(confluence.command("search [query]").description("Search Confluence pages by text and filters").option("-s, --space <key>", "limit to a space (repeatable)", collect).option("--label <label>", "limit to a label (repeatable)", collect).option("--starred", "limit to pages you starred").option("-u, --updated <when>", "changed since 7d, 2w, 3m, or YYYY-MM-DD").addOption(moved("--cql <cql>", "confluence cql"))).action(jiraAction(confluenceSearch));
|
|
2968
3335
|
outputOptions(confluence.command("cql <query>").description("Search Confluence pages with a raw CQL query")).action(jiraAction(confluenceCql));
|
|
@@ -3784,8 +4151,8 @@ function buildJql(params) {
|
|
|
3784
4151
|
userClause("assignee", params.assignee),
|
|
3785
4152
|
userClause("reporter", params.reporter),
|
|
3786
4153
|
inClause("labels", params.label),
|
|
3787
|
-
params.updatedSince ? `updated >= ${quote
|
|
3788
|
-
params.text ? `text ~ ${quote
|
|
4154
|
+
params.updatedSince ? `updated >= ${quote(params.updatedSince)}` : null,
|
|
4155
|
+
params.text ? `text ~ ${quote(params.text)}` : null
|
|
3789
4156
|
].filter((c) => c !== null);
|
|
3790
4157
|
if (clauses.length === 0) clauses.push(RECENT_ISSUES_CLAUSE);
|
|
3791
4158
|
return joinClauses(clauses, ISSUE_ORDER);
|
|
@@ -3793,7 +4160,7 @@ function buildJql(params) {
|
|
|
3793
4160
|
function buildListJql(params) {
|
|
3794
4161
|
return joinClauses([
|
|
3795
4162
|
"assignee = currentUser()",
|
|
3796
|
-
params.project ? `project = ${quote
|
|
4163
|
+
params.project ? `project = ${quote(params.project)}` : null,
|
|
3797
4164
|
params.all ? `(${OPEN_CLAUSE} OR ${RECENT_ISSUES_CLAUSE})` : OPEN_CLAUSE
|
|
3798
4165
|
], ISSUE_ORDER);
|
|
3799
4166
|
}
|
|
@@ -3928,7 +4295,9 @@ async function fetchIssue(client, site, key) {
|
|
|
3928
4295
|
async function updateIssue(client, key, update) {
|
|
3929
4296
|
const fields = { description: update.description };
|
|
3930
4297
|
if (update.summary !== void 0) fields["summary"] = update.summary;
|
|
3931
|
-
|
|
4298
|
+
const path = `/rest/api/3/issue/${encodeURIComponent(key)}`;
|
|
4299
|
+
await client.putNoContent(path, { fields });
|
|
4300
|
+
return (await client.getJson(`${path}?fields=updated`)).fields.updated ?? "";
|
|
3932
4301
|
}
|
|
3933
4302
|
async function fetchComments(client, key) {
|
|
3934
4303
|
return (await client.getJson(`/rest/api/3/issue/${encodeURIComponent(key)}/comment?maxResults=100&orderBy=created`)).comments.map((c) => ({
|
|
@@ -4020,7 +4389,7 @@ function formatIssueView(issue, nowMs, allComments) {
|
|
|
4020
4389
|
const colorStatus = colorForCategory(issue.statusCategory);
|
|
4021
4390
|
return [
|
|
4022
4391
|
`${kleur.bold(issue.key)} ${issue.summary}`,
|
|
4023
|
-
...fieldLines([
|
|
4392
|
+
...fieldLines$1([
|
|
4024
4393
|
["Type", issue.type],
|
|
4025
4394
|
["Status", issue.status && colorStatus(issue.status)],
|
|
4026
4395
|
["Assignee", issue.assignee],
|
|
@@ -4045,15 +4414,17 @@ async function jiraUpdate({ session, term, files }, arg, options) {
|
|
|
4045
4414
|
flag: "[file]",
|
|
4046
4415
|
required: true
|
|
4047
4416
|
});
|
|
4048
|
-
const
|
|
4417
|
+
const raw = await files.readText(file);
|
|
4418
|
+
const src = parseIssueSource(raw);
|
|
4049
4419
|
const plan = planIssueUpdate(src, await fetchIssue(session, session.site, src.key), options);
|
|
4050
4420
|
await runPlan(term, plan, options, async () => {
|
|
4051
4421
|
const { current, next } = plan.headline;
|
|
4052
|
-
await updateIssue(session, src.key, {
|
|
4422
|
+
const updated = await updateIssue(session, src.key, {
|
|
4053
4423
|
description: plan.body,
|
|
4054
4424
|
summary: next !== current ? next : void 0
|
|
4055
4425
|
});
|
|
4056
4426
|
term.out(`Updated ${src.key}.`);
|
|
4427
|
+
if (updated) await files.writeText(file, rewriteFields(raw, { updated }));
|
|
4057
4428
|
});
|
|
4058
4429
|
}
|
|
4059
4430
|
async function copyIssue(env, key, out) {
|
|
@@ -4136,7 +4507,7 @@ function registerJira(jira) {
|
|
|
4136
4507
|
}
|
|
4137
4508
|
//#endregion
|
|
4138
4509
|
//#region package.json
|
|
4139
|
-
var version = "2.
|
|
4510
|
+
var version = "2.1.0";
|
|
4140
4511
|
//#endregion
|
|
4141
4512
|
//#region src/cli/build.ts
|
|
4142
4513
|
const PRODUCTS = {
|