codoc-cli 0.1.2
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/LICENSE +21 -0
- package/README.md +104 -0
- package/assets/drawio-viewer/README.md +13 -0
- package/assets/drawio-viewer/viewer-static.min.js +8640 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +5 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +7 -0
- package/dist/clients/confluence/clients/attachments-client.js +74 -0
- package/dist/clients/confluence/clients/folders-client.js +111 -0
- package/dist/clients/confluence/clients/pages-client.js +128 -0
- package/dist/clients/confluence/confluence-client.js +44 -0
- package/dist/clients/confluence/utils/confluence-url.js +25 -0
- package/dist/clients/confluence/utils/confluence-util.js +20 -0
- package/dist/commands/init.js +29 -0
- package/dist/commands/publish.js +34 -0
- package/dist/commands/pull.js +54 -0
- package/dist/commands/sync.js +27 -0
- package/dist/commands/tree.js +17 -0
- package/dist/config/codoc-config-atlassian.js +89 -0
- package/dist/config/codoc-config-raw.js +21 -0
- package/dist/config/codoc-config.js +50 -0
- package/dist/config/codoc-paths.js +9 -0
- package/dist/hooks/command_not_found.js +7 -0
- package/dist/hooks/init/check-for-update.js +68 -0
- package/dist/hooks/init/load-env.js +15 -0
- package/dist/index.js +2 -0
- package/dist/services/codoc-id.js +7 -0
- package/dist/services/confluence/attachments.js +56 -0
- package/dist/services/confluence/folders.js +42 -0
- package/dist/services/confluence/labels.js +17 -0
- package/dist/services/confluence/pages.js +28 -0
- package/dist/services/conversion/confluenceToMarkdown/diagrams/drawio-diagrams.js +77 -0
- package/dist/services/conversion/confluenceToMarkdown/diagrams/drawio-to-image.js +131 -0
- package/dist/services/conversion/confluenceToMarkdown/index.js +17 -0
- package/dist/services/conversion/confluenceToMarkdown/preprocess/macro-converters.js +68 -0
- package/dist/services/conversion/confluenceToMarkdown/preprocess/preprocess.js +113 -0
- package/dist/services/conversion/confluenceToMarkdown/turndown.js +71 -0
- package/dist/services/conversion/markdownToConfluence/conversion-state.js +18 -0
- package/dist/services/conversion/markdownToConfluence/diagrams/mermaid-diagrams.js +56 -0
- package/dist/services/conversion/markdownToConfluence/diagrams/mermaid-to-drawio.js +331 -0
- package/dist/services/conversion/markdownToConfluence/index.js +48 -0
- package/dist/services/conversion/markdownToConfluence/languages.js +66 -0
- package/dist/services/conversion/markdownToConfluence/links.js +40 -0
- package/dist/services/conversion/markdownToConfluence/raw-html/raw-html.js +86 -0
- package/dist/services/conversion/markdownToConfluence/raw-html/task-lists.js +59 -0
- package/dist/services/conversion/markdownToConfluence/render/blocks.js +171 -0
- package/dist/services/conversion/markdownToConfluence/render/inline.js +44 -0
- package/dist/services/conversion/markdownToConfluence/render/page.js +31 -0
- package/dist/services/conversion/shared/confluence-macro-builder.js +11 -0
- package/dist/services/conversion/shared/gitlab-url.js +16 -0
- package/dist/services/conversion/shared/image-attachments.js +57 -0
- package/dist/services/conversion/shared/jira.js +7 -0
- package/dist/services/conversion/shared/macro-types.js +12 -0
- package/dist/services/conversion/shared/preserved-macros.js +8 -0
- package/dist/services/conversion/shared/read-storage-format.js +29 -0
- package/dist/services/conversion/shared/regex-cache.js +14 -0
- package/dist/services/conversion/shared/xml-escaping.js +19 -0
- package/dist/services/ensure-env.js +59 -0
- package/dist/services/files-service.js +74 -0
- package/dist/services/git/default-branch.js +33 -0
- package/dist/services/init-templates/agent-md-init.js +55 -0
- package/dist/services/init-templates/doc-guide-init.js +131 -0
- package/dist/services/init-templates/env-init.js +12 -0
- package/dist/services/init-templates/yaml-init.js +56 -0
- package/dist/services/lock/lock-file.js +61 -0
- package/dist/services/log/init-printer.js +50 -0
- package/dist/services/log/logger.js +131 -0
- package/dist/services/prompt.js +32 -0
- package/dist/services/slugify.js +6 -0
- package/dist/types/codoc-types.js +1 -0
- package/dist/use-cases/init/init.js +150 -0
- package/dist/use-cases/publish/publish.js +166 -0
- package/dist/use-cases/pull/parse-page-input.js +22 -0
- package/dist/use-cases/pull/pull.js +337 -0
- package/dist/use-cases/shared/codoc-yaml.js +45 -0
- package/dist/use-cases/shared/confluence-client-registry.js +27 -0
- package/dist/use-cases/shared/env-select.js +23 -0
- package/dist/use-cases/shared/fetch-page.js +11 -0
- package/dist/use-cases/shared/list-documents.js +172 -0
- package/dist/use-cases/shared/pull-folder.js +90 -0
- package/dist/use-cases/shared/render-remote-page.js +71 -0
- package/dist/use-cases/sync/attachment-upload.js +31 -0
- package/dist/use-cases/sync/sync-actions.js +170 -0
- package/dist/use-cases/sync/sync-associate.js +41 -0
- package/dist/use-cases/sync/sync-entries.js +36 -0
- package/dist/use-cases/sync/sync-handlers.js +140 -0
- package/dist/use-cases/sync/sync-helpers.js +8 -0
- package/dist/use-cases/sync/sync.js +94 -0
- package/dist/use-cases/tree/docs-tree.js +141 -0
- package/oclif.manifest.json +255 -0
- package/package.json +98 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//#region src/config/codoc-config-atlassian.ts
|
|
2
|
+
/** Noms des variables d'env portant les identifiants Confluence d'un environnement donné. */
|
|
3
|
+
function confluenceEnvVarNames(key) {
|
|
4
|
+
const suffix = key.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
5
|
+
return {
|
|
6
|
+
userVar: `CONFLUENCE_${suffix}_USERNAME`,
|
|
7
|
+
tokenVar: `CONFLUENCE_${suffix}_API_TOKEN`
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function confluenceEnvRequirements(envs) {
|
|
11
|
+
return Object.keys(envs ?? {}).flatMap((key) => {
|
|
12
|
+
const { userVar, tokenVar } = confluenceEnvVarNames(key);
|
|
13
|
+
return [{
|
|
14
|
+
name: userVar,
|
|
15
|
+
secret: false,
|
|
16
|
+
hint: `Identifiant Atlassian (email) pour l'environnement Confluence "${key}".`
|
|
17
|
+
}, {
|
|
18
|
+
name: tokenVar,
|
|
19
|
+
hint: `Token API Atlassian pour l'environnement Confluence "${key}".`,
|
|
20
|
+
generateUrl: "https://kleegroup.atlassian.net/servicedesk/customer/portal/27/group/230 -> Demande d'utilisation clé API -> https://id.atlassian.com/manage-profile/security/api-tokens -> Créer un jeton d'API"
|
|
21
|
+
}];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Retourne l'environnement s'il n'y en a qu'un seul, sinon `undefined`. */
|
|
25
|
+
function onlySingleEnv(envs) {
|
|
26
|
+
return envs.length === 1 ? envs[0] : void 0;
|
|
27
|
+
}
|
|
28
|
+
/** Sous-ensemble de `jira:` utile à codoc (macro Jira Confluence) - un seul jeu de valeurs pour tous les environnements. */
|
|
29
|
+
function parseJiraLink(raw) {
|
|
30
|
+
return {
|
|
31
|
+
serverId: raw?.serverId?.toString(),
|
|
32
|
+
server: raw?.server?.toString()
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function parseDrawio(raw) {
|
|
36
|
+
return {
|
|
37
|
+
macroName: raw?.macroName ?? "drawio",
|
|
38
|
+
width: raw?.width,
|
|
39
|
+
edgeStyle: raw?.edgeStyle,
|
|
40
|
+
edgeAnchor: raw?.edgeAnchor,
|
|
41
|
+
colWidth: raw?.colWidth,
|
|
42
|
+
rowStep: raw?.rowStep
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function resolveAtlassianEnvironment(key, envYaml, missing, jira, drawio) {
|
|
46
|
+
const baseUrl = envYaml?.baseUrl;
|
|
47
|
+
const spaceKey = envYaml?.spaceKey;
|
|
48
|
+
const userVar = `CONFLUENCE_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_USERNAME`;
|
|
49
|
+
const tokenVar = `CONFLUENCE_${key.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_TOKEN`;
|
|
50
|
+
const username = process.env[userVar];
|
|
51
|
+
const apiToken = process.env[tokenVar];
|
|
52
|
+
if (!baseUrl) missing.push(`atlassian.environments.${key}.baseUrl → codoc.yaml`);
|
|
53
|
+
if (!spaceKey) missing.push(`atlassian.environments.${key}.spaceKey → codoc.yaml`);
|
|
54
|
+
if (!username) missing.push(`${userVar} → .env-codoc`);
|
|
55
|
+
if (!apiToken) missing.push(`${tokenVar} → .env-codoc`);
|
|
56
|
+
return {
|
|
57
|
+
key,
|
|
58
|
+
baseUrl,
|
|
59
|
+
username,
|
|
60
|
+
apiToken,
|
|
61
|
+
spaceKey,
|
|
62
|
+
defaultParentPageId: envYaml?.defaultParentPageId?.toString(),
|
|
63
|
+
jira,
|
|
64
|
+
drawio
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function parseAtlassianEnvironments(raw) {
|
|
68
|
+
const missing = [];
|
|
69
|
+
const jira = parseJiraLink(raw.jira);
|
|
70
|
+
const drawio = parseDrawio(raw.drawio);
|
|
71
|
+
const envs = raw.atlassian?.environments;
|
|
72
|
+
if (!envs || !Object.keys(envs).length) throw new Error("Aucun environnement Atlassian défini : ajoute une section `atlassian.environments:` dans codoc.yaml.");
|
|
73
|
+
const resolved = Object.entries(envs).map(([key, envYaml]) => resolveAtlassianEnvironment(key, envYaml, missing, jira, drawio));
|
|
74
|
+
if (missing.length) throw new Error("Configuration Atlassian incomplète - valeurs manquantes :\n\n" + missing.map((m) => ` ${m}`).join("\n") + "\n");
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
function resolveDocEnvironmentByKey(config, doc) {
|
|
78
|
+
const envs = config.atlassian.environments;
|
|
79
|
+
if (doc.env) {
|
|
80
|
+
const env = envs.find((e) => e.key === doc.env);
|
|
81
|
+
if (!env) throw new Error(`Document "${doc.path}" : environnement "${doc.env}" inconnu. Environnements définis : ${envs.map((e) => e.key).join(", ")}.`);
|
|
82
|
+
return env;
|
|
83
|
+
}
|
|
84
|
+
const onlyEnv = onlySingleEnv(envs);
|
|
85
|
+
if (onlyEnv) return onlyEnv;
|
|
86
|
+
throw new Error(`Document "${doc.path}" : champ "env" requis (plusieurs environnements définis : ${envs.map((e) => e.key).join(", ")}).`);
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { confluenceEnvRequirements, onlySingleEnv, parseAtlassianEnvironments, resolveDocEnvironmentByKey };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileExists, readFile } from "../services/files-service.js";
|
|
2
|
+
import { CONFIG_PATH } from "./codoc-paths.js";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
//#region src/config/codoc-config-raw.ts
|
|
5
|
+
/** Normalise une valeur yaml optionnelle (string ou tableau) en tableau de strings non vides et trimées. */
|
|
6
|
+
function asStringList(v) {
|
|
7
|
+
if (!v) return [];
|
|
8
|
+
return (Array.isArray(v) ? v : [v]).map((s) => String(s).trim()).filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
let cache;
|
|
11
|
+
function loadRawConfig(options = {}) {
|
|
12
|
+
if (cache) return cache;
|
|
13
|
+
if (!fileExists(CONFIG_PATH)) {
|
|
14
|
+
if (options.required) throw new Error(`Configuration introuvable : ${CONFIG_PATH}`);
|
|
15
|
+
return {};
|
|
16
|
+
}
|
|
17
|
+
cache = yaml.load(readFile(CONFIG_PATH)) ?? {};
|
|
18
|
+
return cache;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { asStringList, loadRawConfig };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { onlySingleEnv, parseAtlassianEnvironments, resolveDocEnvironmentByKey } from "./codoc-config-atlassian.js";
|
|
2
|
+
import { asStringList, loadRawConfig } from "./codoc-config-raw.js";
|
|
3
|
+
//#region src/config/codoc-config.ts
|
|
4
|
+
let cache;
|
|
5
|
+
function parseGitlab(raw) {
|
|
6
|
+
return {
|
|
7
|
+
baseUrl: raw?.baseUrl,
|
|
8
|
+
defaultBranch: raw?.defaultBranch
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/** Ignore les overrides sans `path`, ou dont les `keywords` sont vides une fois normalisés. */
|
|
12
|
+
function parseKeywordOverrides(raw) {
|
|
13
|
+
return (raw ?? []).map((o) => ({
|
|
14
|
+
path: o.path,
|
|
15
|
+
keywords: asStringList(o.keywords)
|
|
16
|
+
})).filter((o) => Boolean(o.path) && o.keywords.length > 0);
|
|
17
|
+
}
|
|
18
|
+
function parseDocs(raw) {
|
|
19
|
+
return (raw ?? []).map((doc) => ({
|
|
20
|
+
codocId: doc.codocId?.toString(),
|
|
21
|
+
path: doc.path,
|
|
22
|
+
maintainedIn: doc.maintainedIn === "confluence" ? "confluence" : "code",
|
|
23
|
+
env: doc.env,
|
|
24
|
+
imagesDir: doc.imagesDir,
|
|
25
|
+
keywords: asStringList(doc.keywords),
|
|
26
|
+
keywordOverrides: parseKeywordOverrides(doc.keywordOverrides),
|
|
27
|
+
generateSummary: doc.generateSummary ?? true,
|
|
28
|
+
confluence: {
|
|
29
|
+
title: doc.confluence?.title,
|
|
30
|
+
parentPageId: doc.confluence?.parentPageId?.toString(),
|
|
31
|
+
titlePrefix: doc.confluence?.titlePrefix,
|
|
32
|
+
titleSuffix: doc.confluence?.titleSuffix
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
function getGitlabConfig() {
|
|
37
|
+
return parseGitlab(loadRawConfig().gitlab);
|
|
38
|
+
}
|
|
39
|
+
function getCodocConfig() {
|
|
40
|
+
if (cache) return cache;
|
|
41
|
+
const raw = loadRawConfig({ required: true });
|
|
42
|
+
cache = {
|
|
43
|
+
atlassian: { environments: parseAtlassianEnvironments(raw) },
|
|
44
|
+
gitlab: parseGitlab(raw.gitlab),
|
|
45
|
+
docs: parseDocs(raw.docs)
|
|
46
|
+
};
|
|
47
|
+
return cache;
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { getCodocConfig, getGitlabConfig, onlySingleEnv, resolveDocEnvironmentByKey as resolveDocEnvironment };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
//#region src/config/codoc-paths.ts
|
|
3
|
+
const PROJECT_ROOT = process.cwd();
|
|
4
|
+
const CONFIG_PATH = path.join(PROJECT_ROOT, "codoc.yaml");
|
|
5
|
+
const STATE_FILE = path.join(PROJECT_ROOT, "codoc.lock");
|
|
6
|
+
const ENV_FILE = path.join(PROJECT_ROOT, ".env");
|
|
7
|
+
const CODOC_ENV_FILE = path.join(PROJECT_ROOT, ".env-codoc");
|
|
8
|
+
//#endregion
|
|
9
|
+
export { CODOC_ENV_FILE, CONFIG_PATH, ENV_FILE, PROJECT_ROOT, STATE_FILE };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
//#region src/hooks/command_not_found.ts
|
|
2
|
+
const hook = async function(options) {
|
|
3
|
+
const id = options.id.replaceAll(":", " ");
|
|
4
|
+
this.error(`La commande « codoc ${id} » n'existe pas. Lance « codoc help » pour la liste des commandes.`, { exit: 2 });
|
|
5
|
+
};
|
|
6
|
+
//#endregion
|
|
7
|
+
export { hook as default };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { log } from "../../services/log/logger.js";
|
|
2
|
+
import { execa } from "execa";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
//#region src/hooks/init/check-for-update.ts
|
|
5
|
+
const PACKAGE_NAME = "codoc-cli";
|
|
6
|
+
async function currentGlobalVersion() {
|
|
7
|
+
return (await execa("npm", [
|
|
8
|
+
"list",
|
|
9
|
+
"-g",
|
|
10
|
+
PACKAGE_NAME
|
|
11
|
+
], { reject: false })).stdout.split(`${PACKAGE_NAME}@`)[1]?.split(/\s/)[0];
|
|
12
|
+
}
|
|
13
|
+
/** Compare deux versions "x.y.z" segment par segment - true si `candidate` est strictement plus récente que `current`. */
|
|
14
|
+
function isNewerVersion(candidate, current) {
|
|
15
|
+
const a = candidate.split(".").map(Number);
|
|
16
|
+
const b = current.split(".").map(Number);
|
|
17
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
18
|
+
const diff = (a[i] ?? 0) - (b[i] ?? 0);
|
|
19
|
+
if (diff !== 0) return diff > 0;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
async function latestVersion() {
|
|
24
|
+
const result = await execa("npm", [
|
|
25
|
+
"view",
|
|
26
|
+
PACKAGE_NAME,
|
|
27
|
+
"version"
|
|
28
|
+
], { reject: false });
|
|
29
|
+
return result.failed ? void 0 : result.stdout.trim();
|
|
30
|
+
}
|
|
31
|
+
async function update(version) {
|
|
32
|
+
const spinner = ora(`Mise à jour de codoc (${version}) en cours…`).start();
|
|
33
|
+
const result = await execa("npm", [
|
|
34
|
+
"install",
|
|
35
|
+
"-g",
|
|
36
|
+
`${PACKAGE_NAME}@${version}`
|
|
37
|
+
], { reject: false });
|
|
38
|
+
spinner.stop();
|
|
39
|
+
if (result.failed) {
|
|
40
|
+
log.error1(`Mise à jour échouée : ${result.stderr || result.shortMessage}`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
log.success1(`[UPDATED] codoc mis à jour en ${version}.`);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
/** Relance la commande d'origine (argv tel que tapé par l'utilisateur) via le binaire global fraîchement mis à jour. */
|
|
47
|
+
async function rerunWithUpdatedBinary() {
|
|
48
|
+
const result = await execa("codoc", process.argv.slice(2), {
|
|
49
|
+
reject: false,
|
|
50
|
+
stdio: "inherit"
|
|
51
|
+
});
|
|
52
|
+
if (result.failed && !result.exitCode) return false;
|
|
53
|
+
process.exit(result.exitCode ?? 0);
|
|
54
|
+
}
|
|
55
|
+
const hook = async function() {
|
|
56
|
+
if (process.env.CI || !process.stdout.isTTY) return;
|
|
57
|
+
try {
|
|
58
|
+
const [current, latest] = await Promise.all([currentGlobalVersion(), latestVersion()]);
|
|
59
|
+
if (!current || !latest || !isNewerVersion(latest, current)) return;
|
|
60
|
+
this.warn(`Une nouvelle version de codoc est disponible : ${current} → ${latest}.`);
|
|
61
|
+
if (!await update(latest)) return;
|
|
62
|
+
if (!await rerunWithUpdatedBinary()) this.warn("[SKIPPED] Relance automatique impossible - relance ta commande pour utiliser la nouvelle version.");
|
|
63
|
+
} catch (err) {
|
|
64
|
+
this.warn(`[SKIPPED] Vérification de mise à jour ignorée : ${err.message}`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { hook as default, isNewerVersion };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { CODOC_ENV_FILE, ENV_FILE } from "../../config/codoc-paths.js";
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
//#region src/hooks/init/load-env.ts
|
|
4
|
+
const hook = async function() {
|
|
5
|
+
dotenv.config({
|
|
6
|
+
path: CODOC_ENV_FILE,
|
|
7
|
+
quiet: true
|
|
8
|
+
});
|
|
9
|
+
dotenv.config({
|
|
10
|
+
path: ENV_FILE,
|
|
11
|
+
quiet: true
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { hook as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describeError } from "../../clients/confluence/utils/confluence-util.js";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
//#region src/services/confluence/attachments.ts
|
|
5
|
+
async function fetchAttachmentBinary(attachments, pageId, filename) {
|
|
6
|
+
const att = await attachments.find(pageId, filename);
|
|
7
|
+
if (!att) return { notFound: true };
|
|
8
|
+
const errors = {};
|
|
9
|
+
try {
|
|
10
|
+
const buffer = await attachments.downloadV1(att.downloadPath);
|
|
11
|
+
return {
|
|
12
|
+
data: Buffer.from(buffer),
|
|
13
|
+
via: "v1",
|
|
14
|
+
bytes: buffer.byteLength
|
|
15
|
+
};
|
|
16
|
+
} catch (err) {
|
|
17
|
+
errors.v1Error = await describeError(err);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const signedUrl = await attachments.getDownloadLink(att.id);
|
|
21
|
+
if (!signedUrl) {
|
|
22
|
+
errors.v2Error = "Réponse v2 sans downloadLink.";
|
|
23
|
+
return errors;
|
|
24
|
+
}
|
|
25
|
+
const buffer = await attachments.downloadV2(signedUrl);
|
|
26
|
+
return {
|
|
27
|
+
data: Buffer.from(buffer),
|
|
28
|
+
via: "v2",
|
|
29
|
+
bytes: buffer.byteLength
|
|
30
|
+
};
|
|
31
|
+
} catch (err) {
|
|
32
|
+
errors.v2Error = await describeError(err);
|
|
33
|
+
return errors;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function fetchAttachmentText(attachments, pageId, filename) {
|
|
37
|
+
const att = await attachments.find(pageId, filename);
|
|
38
|
+
if (!att) return void 0;
|
|
39
|
+
try {
|
|
40
|
+
return await attachments.downloadTextV1(att.downloadPath);
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function uploadAttachmentBlob(attachments, pageId, filename, blob) {
|
|
46
|
+
await attachments.upload(pageId, filename, blob);
|
|
47
|
+
}
|
|
48
|
+
async function uploadAttachmentBuffer(attachments, pageId, filename, buffer, mimeType = "application/octet-stream") {
|
|
49
|
+
await uploadAttachmentBlob(attachments, pageId, filename, new Blob([new Uint8Array(buffer)], { type: mimeType }));
|
|
50
|
+
}
|
|
51
|
+
async function uploadAttachmentFile(attachments, pageId, filePath, mimeType = "application/octet-stream") {
|
|
52
|
+
const buffer = await readFile(filePath);
|
|
53
|
+
await uploadAttachmentBuffer(attachments, pageId, basename(filePath), buffer, mimeType);
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { fetchAttachmentBinary, fetchAttachmentText, uploadAttachmentBlob, uploadAttachmentBuffer, uploadAttachmentFile };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { log } from "../log/logger.js";
|
|
2
|
+
import { createPageOrThrowConflict } from "./pages.js";
|
|
3
|
+
//#region src/services/confluence/folders.ts
|
|
4
|
+
async function createOrEnsureFolder(client, title, parentId, existingId) {
|
|
5
|
+
if (existingId && await resourceExists(client, existingId)) return { id: existingId };
|
|
6
|
+
if (parentId) {
|
|
7
|
+
const already = await client.folders.findByTitleUnderParent(String(parentId), title);
|
|
8
|
+
if (already) return { id: already.id };
|
|
9
|
+
}
|
|
10
|
+
if (!client.foldersV2Unavailable) try {
|
|
11
|
+
const spaceId = await client.folders.getSpaceId(client.configuration.spaceKey);
|
|
12
|
+
return { id: (await client.folders.create(title, spaceId, parentId)).id };
|
|
13
|
+
} catch (err) {
|
|
14
|
+
client.foldersV2Unavailable = true;
|
|
15
|
+
const status = err.response?.status;
|
|
16
|
+
log.warning2(`API v2 dossiers indisponible (HTTP ${status ?? "réseau"}) - repli sur des pages de type dossier.`);
|
|
17
|
+
}
|
|
18
|
+
return { id: (await createPageOrThrowConflict(client, title, "<p></p>", parentId)).id };
|
|
19
|
+
}
|
|
20
|
+
async function resourceExists(client, id) {
|
|
21
|
+
return await client.folders.exists(id) || await client.pages.exists(id);
|
|
22
|
+
}
|
|
23
|
+
async function getFolderRef(client, id) {
|
|
24
|
+
const folder = await client.folders.get(id);
|
|
25
|
+
if (folder) return {
|
|
26
|
+
...folder,
|
|
27
|
+
type: "folder"
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
const page = await client.pages.fetchPage(id);
|
|
31
|
+
return {
|
|
32
|
+
id: page.id,
|
|
33
|
+
title: page.title,
|
|
34
|
+
parentId: page.parentId,
|
|
35
|
+
type: "page"
|
|
36
|
+
};
|
|
37
|
+
} catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { createOrEnsureFolder, getFolderRef };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { log } from "../log/logger.js";
|
|
2
|
+
//#region src/services/confluence/labels.ts
|
|
3
|
+
const COMBINING_MARKS_RE = new RegExp(`[${String.fromCharCode(768)}-${String.fromCharCode(879)}]`, "g");
|
|
4
|
+
function toConfluenceLabel(keyword) {
|
|
5
|
+
return keyword.normalize("NFD").replace(COMBINING_MARKS_RE, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
6
|
+
}
|
|
7
|
+
async function pushKeywordLabels(client, pageId, keywords) {
|
|
8
|
+
const labels = [...new Set((keywords ?? []).map(toConfluenceLabel).filter(Boolean))];
|
|
9
|
+
if (!labels.length) return;
|
|
10
|
+
try {
|
|
11
|
+
await client.pages.addLabels(pageId, labels);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
log.warning4(`Labels Confluence non appliqués (${labels.join(", ")}) : ${err.message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { pushKeywordLabels, toConfluenceLabel };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { pageUrl } from "../../clients/confluence/utils/confluence-url.js";
|
|
2
|
+
//#region src/services/confluence/pages.ts
|
|
3
|
+
async function createPageOrThrowConflict(client, title, storageContent, parentId, onTitleConflict) {
|
|
4
|
+
try {
|
|
5
|
+
return await client.pages.create(title, storageContent, parentId);
|
|
6
|
+
} catch (err) {
|
|
7
|
+
const message = await err.response?.text?.();
|
|
8
|
+
if (err.response?.status !== 400 || !message?.includes("already exists")) throw err;
|
|
9
|
+
const found = await client.pages.findByTitle(title);
|
|
10
|
+
if (found && onTitleConflict) {
|
|
11
|
+
const url = pageUrl(client.configuration.baseUrl, client.configuration.spaceKey, found.id);
|
|
12
|
+
if (await onTitleConflict(found.id, url)) return client.pages.update(found.id, title, storageContent, parentId);
|
|
13
|
+
}
|
|
14
|
+
const where = found ? `elle a été retrouvée ici : ${pageUrl(client.configuration.baseUrl, client.configuration.spaceKey, found.id)}` : `elle n'a pas pu être retrouvée par titre (peut-être à la corbeille) - vérifie manuellement dans l'espace "${client.configuration.spaceKey}".`;
|
|
15
|
+
throw new Error(`Impossible de créer la page "${title}" : une page portant ce titre existe déjà quelque part dans Confluence, ${where}\nVérifie s'il ne s'agit pas de la même doc mal référencée (codocId/parentPageId à corriger dans codoc.yaml), ou renomme l'une des deux pages pour lever le conflit.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function createOrUpdatePage(client, title, storageContent, parentId, existingPageId, onTitleConflict) {
|
|
19
|
+
if (!existingPageId) return createPageOrThrowConflict(client, title, storageContent, parentId, onTitleConflict);
|
|
20
|
+
try {
|
|
21
|
+
return await client.pages.update(existingPageId, title, storageContent, parentId);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (err.response?.status === 404) return createPageOrThrowConflict(client, title, storageContent, parentId, onTitleConflict);
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
export { createOrUpdatePage, createPageOrThrowConflict };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ensureDir, writeBinaryFile } from "../../../files-service.js";
|
|
2
|
+
import { log } from "../../../log/logger.js";
|
|
3
|
+
import { escapeXml } from "../../shared/xml-escaping.js";
|
|
4
|
+
import { sanitizeAttachmentFilename } from "../../shared/image-attachments.js";
|
|
5
|
+
import { macroParam, macroRegex } from "../../shared/read-storage-format.js";
|
|
6
|
+
import { extractGraphModel, renderDrawioToPng } from "./drawio-to-image.js";
|
|
7
|
+
import path from "path";
|
|
8
|
+
//#region src/services/conversion/confluenceToMarkdown/diagrams/drawio-diagrams.ts
|
|
9
|
+
/** Repère les blocs draw.io (macro classique + extension ADF), avec nom de pièce jointe. */
|
|
10
|
+
function findDrawioBlocks(xml) {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const m of xml.matchAll(macroRegex("drawio"))) {
|
|
13
|
+
const block = m[0];
|
|
14
|
+
const name = macroParam(block, "attachment") ?? macroParam(block, "diagramName");
|
|
15
|
+
if (name) out.push({
|
|
16
|
+
full: block,
|
|
17
|
+
fileName: name,
|
|
18
|
+
start: m.index ?? 0
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
for (const m of xml.matchAll(/<ac:adf-extension>[\s\S]*?<\/ac:adf-extension>/g)) {
|
|
22
|
+
const block = m[0];
|
|
23
|
+
if (!/drawio/i.test(block)) continue;
|
|
24
|
+
const name = block.match(/key="diagram-name">([^<]+)<\/ac:adf-parameter>/)?.[1];
|
|
25
|
+
if (name) out.push({
|
|
26
|
+
full: block,
|
|
27
|
+
fileName: name.trim(),
|
|
28
|
+
start: m.index ?? 0
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return out.sort((a, b) => a.start - b.start);
|
|
32
|
+
}
|
|
33
|
+
/** Nom de pièce jointe draw.io → nom de fichier .png local stable (même diagramme = même nom au fil des pulls). */
|
|
34
|
+
function pngFilenameFor(fileName) {
|
|
35
|
+
const withoutExt = fileName.replace(/\.drawio$/i, "");
|
|
36
|
+
return `${sanitizeAttachmentFilename(withoutExt)}.png`;
|
|
37
|
+
}
|
|
38
|
+
function imgTag(fileName, localFilename, imgRelPath) {
|
|
39
|
+
return `<img src="${imgRelPath ? `${imgRelPath}/${localFilename}` : localFilename}" alt="${escapeXml(fileName)}">`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Blocs draw.io → image PNG rendue par le vrai moteur draw.io (fidèle, icônes/pochoirs inclus),
|
|
43
|
+
* enregistrée dans `imagesDir` et référencée par un `<img>` classique dans le xml (repris ensuite
|
|
44
|
+
* par le pipeline HTML → Markdown, comme n'importe quelle autre image Confluence).
|
|
45
|
+
*
|
|
46
|
+
* Sans `imagesDir` configuré : aucune conversion (mêmes conventions que pour les autres images -
|
|
47
|
+
* pas de dossier, pas de téléchargement local). Les blocs non convertis (échec, pas d'imagesDir)
|
|
48
|
+
* retombent sur `preserveDrawioMacros` (préservés tels quels dans le markdown final).
|
|
49
|
+
*/
|
|
50
|
+
async function convertDrawioBlocksToImages(xml, fetchAttachment, imagesDir, imgRelPath) {
|
|
51
|
+
const blocks = findDrawioBlocks(xml);
|
|
52
|
+
if (!blocks.length || !imagesDir) return xml;
|
|
53
|
+
log.step1(`${blocks.length} diagramme(s) draw.io détecté(s) - conversion en image`);
|
|
54
|
+
ensureDir(imagesDir);
|
|
55
|
+
const parts = [];
|
|
56
|
+
let last = 0;
|
|
57
|
+
for (const { full, fileName, start } of blocks) try {
|
|
58
|
+
const drawioXml = await fetchAttachment(fileName);
|
|
59
|
+
if (!drawioXml) {
|
|
60
|
+
log.warning3(`"${fileName}" : pièce jointe introuvable - bloc draw.io conservé tel quel`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const png = await renderDrawioToPng(extractGraphModel(drawioXml));
|
|
64
|
+
const localFilename = pngFilenameFor(fileName);
|
|
65
|
+
writeBinaryFile(path.resolve(imagesDir, localFilename), png);
|
|
66
|
+
parts.push(xml.slice(last, start), imgTag(fileName, localFilename, imgRelPath));
|
|
67
|
+
last = start + full.length;
|
|
68
|
+
log.success3(`"${fileName}" → ${localFilename}`);
|
|
69
|
+
} catch (err) {
|
|
70
|
+
const why = err?.response?.status ?? err?.message ?? "erreur";
|
|
71
|
+
log.warning3(`"${fileName}" : téléchargement/rendu impossible (${why}) - bloc conservé`);
|
|
72
|
+
}
|
|
73
|
+
parts.push(xml.slice(last));
|
|
74
|
+
return parts.join("");
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { convertDrawioBlocksToImages };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { log } from "../../../log/logger.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { exec, execFile } from "child_process";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import zlib from "zlib";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
//#region src/services/conversion/confluenceToMarkdown/diagrams/drawio-to-image.ts
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const VIEWER_SCRIPT_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../../assets/drawio-viewer/viewer-static.min.js");
|
|
13
|
+
let viewerScriptCache;
|
|
14
|
+
function viewerScript() {
|
|
15
|
+
viewerScriptCache ??= fs.readFileSync(VIEWER_SCRIPT_PATH, "utf-8").replace(/<\/script/gi, "<\\/script");
|
|
16
|
+
return viewerScriptCache;
|
|
17
|
+
}
|
|
18
|
+
let browserPromise;
|
|
19
|
+
const CHROME_MISSING = /Could not find (Chrome|Browser)/i;
|
|
20
|
+
const CHROME_VERSION = /ver\. ([\d.]+)/;
|
|
21
|
+
function chromeInstallHelpMessage(cause) {
|
|
22
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
23
|
+
const version = detail.match(CHROME_VERSION)?.[1];
|
|
24
|
+
const cacheDir = "%USERPROFILE%\\.cache\\puppeteer\\chrome";
|
|
25
|
+
return `Échec de l'installation automatique de Chrome, nécessaire pour convertir les diagrammes draw.io en image.
|
|
26
|
+
Cause connue sur Windows : un antivirus interfère avec l'extraction automatique des fichiers, alors que le téléchargement du .zip, lui, réussit toujours entièrement.
|
|
27
|
+
Contournement (fiable, déjà vérifié sur ce projet) - depuis PowerShell :
|
|
28
|
+
Expand-Archive -Path "${cacheDir}\\${version ? `${version}-chrome-win64.zip` : "<version>-chrome-win64.zip"}" -DestinationPath "${cacheDir}\\${version ? `win64-${version}` : "win64-<version>"}" -Force\n` + (version ? "" : " (remplacez <version> par le numéro présent dans le nom du fichier .zip du dossier ci-dessus)\n") + `Détail technique : ${detail}`;
|
|
29
|
+
}
|
|
30
|
+
function puppeteerCliPath() {
|
|
31
|
+
const pkgPath = fileURLToPath(import.meta.resolve("puppeteer/package.json"));
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
33
|
+
return path.resolve(path.dirname(pkgPath), pkg.bin);
|
|
34
|
+
}
|
|
35
|
+
async function installChromeOnWindows(version) {
|
|
36
|
+
const { getDownloadUrl, Cache, Browser: PuppeteerBrowser, BrowserPlatform } = await import("@puppeteer/browsers");
|
|
37
|
+
const installDir = new Cache(path.join(os.homedir(), ".cache", "puppeteer")).installationDir(PuppeteerBrowser.CHROME, BrowserPlatform.WIN64, version);
|
|
38
|
+
const url = getDownloadUrl(PuppeteerBrowser.CHROME, BrowserPlatform.WIN64, version).toString();
|
|
39
|
+
const tmpZip = path.join(os.tmpdir(), `codoc-chrome-${version}-${Date.now()}.zip`);
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(url);
|
|
42
|
+
if (!res.ok) return false;
|
|
43
|
+
fs.writeFileSync(tmpZip, Buffer.from(await res.arrayBuffer()));
|
|
44
|
+
await execFileAsync("powershell", [
|
|
45
|
+
"-NoProfile",
|
|
46
|
+
"-NonInteractive",
|
|
47
|
+
"-Command",
|
|
48
|
+
`Remove-Item -LiteralPath '${installDir}' -Recurse -Force -ErrorAction SilentlyContinue; Expand-Archive -LiteralPath '${tmpZip}' -DestinationPath '${installDir}' -Force`
|
|
49
|
+
], { timeout: 18e4 });
|
|
50
|
+
return fs.existsSync(path.join(installDir, "chrome-win64", "chrome.exe"));
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
} finally {
|
|
54
|
+
fs.rmSync(tmpZip, { force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function ensureChromeInstalled(version) {
|
|
58
|
+
log.info1("Chrome (nécessaire pour convertir les diagrammes draw.io) introuvable - installation en cours…");
|
|
59
|
+
if (process.platform === "win32" && version && await installChromeOnWindows(version)) return;
|
|
60
|
+
await execAsync(`node "${puppeteerCliPath()}" browsers install chrome`, { timeout: 3e5 });
|
|
61
|
+
}
|
|
62
|
+
async function getBrowser() {
|
|
63
|
+
browserPromise ??= (async () => {
|
|
64
|
+
const { launch } = await import("puppeteer");
|
|
65
|
+
try {
|
|
66
|
+
return await launch({ headless: true });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
if (!(err instanceof Error) || !CHROME_MISSING.test(err.message)) throw err;
|
|
69
|
+
try {
|
|
70
|
+
await ensureChromeInstalled(err.message.match(CHROME_VERSION)?.[1]);
|
|
71
|
+
return await launch({ headless: true });
|
|
72
|
+
} catch (retryErr) {
|
|
73
|
+
throw new Error(chromeInstallHelpMessage(retryErr));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
return browserPromise;
|
|
78
|
+
}
|
|
79
|
+
/** À appeler en fin de commande (pull/sync) pour fermer proprement le navigateur headless. */
|
|
80
|
+
async function closeDrawioRenderer() {
|
|
81
|
+
if (!browserPromise) return;
|
|
82
|
+
const promise = browserPromise;
|
|
83
|
+
browserPromise = void 0;
|
|
84
|
+
try {
|
|
85
|
+
await (await promise).close();
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
function extractGraphModel(drawio) {
|
|
89
|
+
if (drawio.includes("<mxGraphModel")) {
|
|
90
|
+
const start = drawio.indexOf("<mxGraphModel");
|
|
91
|
+
const end = drawio.indexOf("</mxGraphModel>");
|
|
92
|
+
if (end !== -1) return drawio.slice(start, end + 15);
|
|
93
|
+
}
|
|
94
|
+
const payload = drawio.match(/<diagram[^>]*>([\s\S]*?)<\/diagram>/)?.[1]?.trim();
|
|
95
|
+
if (payload) try {
|
|
96
|
+
const inflated = zlib.inflateRawSync(Buffer.from(payload, "base64")).toString("utf-8");
|
|
97
|
+
return decodeURIComponent(inflated);
|
|
98
|
+
} catch {}
|
|
99
|
+
throw new Error("Impossible d'extraire le mxGraphModel (ni XML en clair, ni payload compressé reconnu).");
|
|
100
|
+
}
|
|
101
|
+
function viewerHtml(graphModelXml) {
|
|
102
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;background:#fff}</style></head><body><div class="mxgraph" data-mxgraph="${JSON.stringify({
|
|
103
|
+
toolbar: "",
|
|
104
|
+
resize: true,
|
|
105
|
+
xml: graphModelXml
|
|
106
|
+
}).replace(/&/g, "&").replace(/"/g, """)}"></div><script>${viewerScript()}<\/script></body></html>`;
|
|
107
|
+
}
|
|
108
|
+
/** mxGraphModel XML → PNG rendu par le vrai moteur draw.io (icônes/pochoirs inclus). */
|
|
109
|
+
async function renderDrawioToPng(graphModelXml) {
|
|
110
|
+
const page = await (await getBrowser()).newPage();
|
|
111
|
+
try {
|
|
112
|
+
await page.setViewport({
|
|
113
|
+
width: 4e3,
|
|
114
|
+
height: 4e3,
|
|
115
|
+
deviceScaleFactor: 2
|
|
116
|
+
});
|
|
117
|
+
await page.setContent(viewerHtml(graphModelXml), { waitUntil: "load" });
|
|
118
|
+
await page.waitForFunction(() => document.querySelector(".mxgraph svg") !== null, { timeout: 15e3 });
|
|
119
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
120
|
+
const el = await page.$(".mxgraph");
|
|
121
|
+
if (!el) throw new Error("Élément .mxgraph introuvable après rendu.");
|
|
122
|
+
return await el.screenshot({
|
|
123
|
+
type: "png",
|
|
124
|
+
omitBackground: false
|
|
125
|
+
});
|
|
126
|
+
} finally {
|
|
127
|
+
await page.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { closeDrawioRenderer, extractGraphModel, renderDrawioToPng };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { convertDrawioBlocksToImages } from "./diagrams/drawio-diagrams.js";
|
|
2
|
+
import { preprocessStorage } from "./preprocess/preprocess.js";
|
|
3
|
+
import { buildTurndown } from "./turndown.js";
|
|
4
|
+
//#region src/services/conversion/confluenceToMarkdown/index.ts
|
|
5
|
+
function formatTableBlocks(md) {
|
|
6
|
+
return md.replace(/<table[\s\S]*?<\/table>/g, (table) => table.replace(/(<table[^>]*>)\s*(<colgroup>|<thead>|<tbody>)/g, "$1\n$2").replace(/<\/colgroup>\s*(<thead>|<tbody>)/g, "</colgroup>\n$1").replace(/(<thead>|<tbody>)\s*<tr>/g, "$1\n<tr>").replace(/<\/tr>\s*<tr>/g, "</tr>\n<tr>").replace(/<\/tr>\s*(<\/thead>|<\/tbody>)/g, "</tr>\n$1").replace(/<\/thead>\s*<tbody>/g, "</thead>\n<tbody>").replace(/<\/(thead|tbody)>\s*<\/table>/g, "</$1>\n</table>"));
|
|
7
|
+
}
|
|
8
|
+
function confluenceStorageToMarkdown(storageXml, imgRelPath, baseUrl) {
|
|
9
|
+
const html = preprocessStorage(storageXml, imgRelPath, baseUrl);
|
|
10
|
+
return formatTableBlocks(buildTurndown().turndown(html).trim());
|
|
11
|
+
}
|
|
12
|
+
/** Sens pull : page Confluence (Storage Format + pièces jointes) → Markdown. */
|
|
13
|
+
async function renderMarkdown(storageXml, options) {
|
|
14
|
+
return confluenceStorageToMarkdown(await convertDrawioBlocksToImages(storageXml, options.fetchAttachment, options.imagesDir, options.imgRelPath), options.imgRelPath, options.baseUrl);
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { confluenceStorageToMarkdown, preprocessStorage, renderMarkdown };
|