UNPKG

antora-confluence

Version:

A tool to convert and publish Antora documentation to Confluence

346 lines (345 loc) 16.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getRenamedPages = exports.getPagesToBeRemoved = exports.deletePages = exports.publish = exports.buildPageStructure = void 0; const Enum_1 = require("../constants/Enum"); const HtmlDomParser_1 = require("../parser/HtmlDomParser"); const node_path_1 = __importDefault(require("node:path")); const RESTApiService_1 = require("./RESTApiService"); const node_html_parser_1 = __importDefault(require("node-html-parser")); const fs_1 = require("fs"); const HashCalculatorService_1 = require("./HashCalculatorService"); const path_1 = __importDefault(require("path")); const Logger_1 = require("../Logger"); const types_1 = require("../types"); const FileExclusionService_1 = require("./FileExclusionService"); const HtmlToConfluenceConverter_1 = require("./HtmlToConfluenceConverter"); const LOGGER = (0, Logger_1.getLogger)(); const matchesFilter = (file, filters) => { const matches = filters?.filter((filter) => { const pathFilter = filter.path; const fileFilter = filter.files; if (pathFilter) { return path_1.default.dirname(file.path).startsWith(pathFilter); } else if (fileFilter) { return fileFilter.includes(file.path); } else { LOGGER.error(`Invalid filter defined ${JSON.stringify(filter)}`); throw new Error("Filter must be a valid type of PathFilter or FileFilter"); } }); return matches.length > 0; }; const buildPageStructure = async (files, target, config) => { for await (const file of files) { const fileName = file.path; const mappers = config.mapper; const filters = config.filter; const excludedFiles = config.excludeFiles; if (fileName.startsWith("_") || !fileName.endsWith(".html") || fileName.includes(Enum_1.ANTORA_DEFAULTS.NOT_FOUND_PAGE)) { LOGGER.debug(`Skipping ${fileName}`); continue; } if (excludedFiles && (0, FileExclusionService_1.fileIsExcluded)(fileName, excludedFiles)) { LOGGER.debug(`Skipping ${fileName} because it has been explicitly excluded`); continue; } if (filters && !matchesFilter(file, filters)) { LOGGER.debug(`Skipping ${fileName} because it does not match filter`); continue; } let parts = fileName.split("/"); let currentObject = target; let mapper; if (mappers) { mapper = mappers.find((mapper) => { return path_1.default.dirname(fileName).startsWith(mapper.path); }); if (mapper) { if (!currentObject[mapper.target]) { currentObject[mapper.target] = {}; } currentObject = currentObject[mapper.target]; parts = fileName .replace(`${path_1.default.normalize(mapper.path)}/`, "") .split("/"); } else { LOGGER.debug(`No mapper found for ${fileName}, now checking if this page is a direct sibling of a mapper...`); const mapper = mappers.find((mapper) => { return node_path_1.default.dirname(fileName) === node_path_1.default.dirname(mapper.path); }); if (mapper) { parts = fileName .replace(`${node_path_1.default.dirname(mapper.path)}/`, "") .split("/"); } } } if (!mapper || parts.length > 1) { for (let i = 0; i < parts.length - 1; i++) { let part; if (i === 0) { part = parts[i]; } else { part = `[${parts[0]}]-${parts[i]}`; } if (!currentObject[part]) { currentObject[part] = {}; } currentObject = currentObject[part]; } } const lastPart = parts[parts.length - 1]; let pageTitle = (0, HtmlDomParser_1.getPageTitle)(file.contents.toString("utf-8")); pageTitle = target.get("inventory").get(pageTitle) ? `${parts[parts.length - 2]}-${pageTitle}` : pageTitle; const page = { fileName: lastPart, pageTitle, parent: parts[parts.length - 2], fqfn: file.path, id: (0, HashCalculatorService_1.calculateHash)(file.path), }; if (target.get("inventory").get(pageTitle)) { target .get("inventory") .set(`${parts[parts.length - 2]}-${pageTitle}`, page); target.get("flat").push({ ...page, pageTitle: `${parts[parts.length - 2]}-${pageTitle}`, }); } else { target.get("inventory").set(pageTitle, page); target.get("flat").push(page); } if (!currentObject["sibling_pages"]) { currentObject["sibling_pages"] = [{ ...page, content: file.contents }]; } else { currentObject["sibling_pages"].push({ ...page, content: file.contents }); } } }; exports.buildPageStructure = buildPageStructure; const createParentIfNotExists = async (confluenceClient, parent, parentParent) => { if (!parent) { return; } const componentPage = await (0, RESTApiService_1.sendRequest)(confluenceClient.fetchPageIdByName(parent)); if (componentPage.results && componentPage.results.length > 0) { const { id, version } = componentPage.results[0]; LOGGER.debug(`Component page exists, skipping...[${parent}] with id [${id}]`); return { id, version }; } else { let parentParentId; if (parentParent) { const parentPage = await (0, RESTApiService_1.sendRequest)(confluenceClient.fetchPageIdByName(parentParent)); if (parentPage.results && parentPage.results.length > 0) { parentParentId = parentPage.results[0].id; } } LOGGER.debug(`Component page does not exist, creating...[${parent}] with parent [${parentParentId}]`); const { id } = (await (0, RESTApiService_1.sendRequest)(confluenceClient.createPage({ title: parent, content: `<h1>${parent}</h1>`, parentPageId: parentParentId, }, types_1.ConfluencePageStatus.CURRENT))); return { id, version: 1 }; } }; const deletePages = async (confluenceClient, removals) => { for await (const page of removals) { const response = await (0, RESTApiService_1.sendRequest)(confluenceClient.fetchPageIdByName(page.pageTitle)); if (response.results && response.results.length > 0) { LOGGER.info(`Deleting page ${page.pageTitle} with ID ${response.results[0].id}`); await (0, RESTApiService_1.sendRequest)(confluenceClient.deletePage(response.results[0].id)); } } }; exports.deletePages = deletePages; const publish = async (confluenceClient, outPutDir, pageTree, showBanner, flatPages, renames, parent) => { for (const key in pageTree) { if (Array.isArray(pageTree[key])) { const parentPage = await createParentIfNotExists(confluenceClient, parent); const parentId = parentPage?.id; const pages = pageTree[key]; for (const page of pages) { const confluencePage = processPage(page, outPutDir, showBanner, flatPages); if (confluencePage) { const localHash = confluencePage.hash; let pageId; if (page.fileName === "index.html") { confluencePage.title = parent; const { id } = await (0, RESTApiService_1.sendRequest)(confluenceClient.updatePage(confluencePage, parentId, parentPage?.version.number + 1)); pageId = id; } else { const rename = renames?.filter((rename) => { return confluencePage.title === rename.newOne.pageTitle; })[0]; const fetchTitle = rename ? rename.oldOne.pageTitle : confluencePage.title; const componentPage = await (0, RESTApiService_1.sendRequest)(confluenceClient.fetchPageIdByName(fetchTitle)); if (componentPage.results && componentPage.results.length > 0 && !rename) { const { id, version } = componentPage.results[0]; const pageComp = (0, node_html_parser_1.default)(componentPage.results[0].body.storage.value); const remoteHash = pageComp.querySelector(`.${Enum_1.PageIdentifier.LOCAL_HASH_TAG_ID}`)?.rawText; if (remoteHash !== localHash) { LOGGER.debug(`Component page exists, update...[${parent}] with id [${id}]`); pageId = id; await (0, RESTApiService_1.sendRequest)(confluenceClient.updatePage({ title: `${confluencePage.title}`, content: confluencePage.content, parentPageId: parentId, }, pageId, version.number + 1)); } else { LOGGER.info("Page hasn't changed!"); } } else { if (componentPage.results && componentPage.results.length > 0) { const { id } = componentPage.results[0]; LOGGER.info(`Deleting old page with id ${id}`); await confluenceClient.deletePage(id); } const { id } = await (0, RESTApiService_1.sendRequest)(confluenceClient.createPage({ title: `${confluencePage.title}`, content: confluencePage.content, parentPageId: parentId, }, types_1.ConfluencePageStatus.CURRENT)); pageId = id; } } if (pageId) { for (const upload of confluencePage.attachments) { try { const buffer = (0, fs_1.readFileSync)(upload.filePath); const localHashAttachment = (0, HashCalculatorService_1.calculateHashOfStream)(buffer); const attachment = await (0, RESTApiService_1.sendRequest)(confluenceClient.getAttachment(pageId, upload.fileName)); if (attachment.results && attachment.results.length > 0) { if (localHashAttachment === attachment.results[0].extensions.comment.replace(/.*#([^#]+)#.*/s, "$1")) { LOGGER.info("Attachment hasn't changed!"); } else { LOGGER.info(`Attachment has changed, updating... ${attachment.results[0].id}`); await (0, RESTApiService_1.sendRequest)(confluenceClient.updateAttachment({ pageId, fileName: upload.fileName, file: buffer, comment: upload.comment + `\r\n#${localHashAttachment}#`, attachmentId: attachment.results[0].id, })); } } else { LOGGER.debug(`Attachment ${upload.fileName} does not exist, creating...`); await (0, RESTApiService_1.sendRequest)(confluenceClient.createAttachment({ pageId, fileName: upload.fileName, file: buffer, comment: upload.comment + `\r\n#${localHashAttachment}#`, })); } } catch (e) { LOGGER.error(`Error uploading attachment ${upload.fileName}`); LOGGER.error(e); } } } } } } else if (typeof pageTree[key] === "object") { await createParentIfNotExists(confluenceClient, key, parent); await publish(confluenceClient, outPutDir, pageTree[key], showBanner, flatPages, renames, key); } } }; exports.publish = publish; const processPage = (page, outPutDir, showBanner, flatPages) => { LOGGER.info(`Processing ${page.fileName}`); const baseUrl = node_path_1.default.join(process.cwd(), outPutDir, path_1.default.dirname(page.fqfn)); const htmlFileContent = page.content.toString("utf-8"); const dom = (0, node_html_parser_1.default)(htmlFileContent, { blockTextElements: { code: true }, voidTag: { closingSlash: true, }, }); const htmlInput = dom.querySelector("article.doc"); if (htmlInput) { const { uploads, content } = (0, HtmlToConfluenceConverter_1.convertHtmlToConfluence)(htmlInput, baseUrl, page, flatPages); let htmlContent = content .toString() .replaceAll("<br>", "<br/>") .replaceAll("</br>", "<br/>") .replaceAll("<a([^>]*)></a>", "") .replaceAll("checked />", 'checked="checked" />') .replaceAll(Enum_1.Placeholder.CDATA_PLACEHOLDER_START, "<![CDATA[") .replaceAll(Enum_1.Placeholder.CDATA_PLACEHOLDER_END, "]]>"); if (showBanner) { htmlContent = `<ac:structured-macro ac:name="note" ac:schema-version="1"><ac:rich-text-body> <p>This page has been published via Antora plugin. Every change to this site will be lost, if you run Antora the next time. You can still use comments, as they will be preserved.</p> </ac:rich-text-body></ac:structured-macro>${htmlContent}`; } const localHash = (0, HashCalculatorService_1.calculateHash)(htmlContent); htmlContent += `<ac:placeholder><p class="${Enum_1.PageIdentifier.LOCAL_HASH_TAG_ID}">${localHash}</p></ac:placeholder>`; return { title: page.pageTitle, content: htmlContent, attachments: uploads, hash: localHash, meta: { id: page.id, }, }; } }; const getPagesToBeRemoved = (stateValues, pageStructure) => { const stateIds = stateValues.map((entry) => entry.id); const removalIds = stateIds.filter((id) => { return !pageStructure .get("flat") .find((page) => page.id === id); }); return stateValues.filter((stateObject) => { return removalIds.includes(stateObject.id); }); }; exports.getPagesToBeRemoved = getPagesToBeRemoved; const getRenamedPages = (stateValues, pageStructure) => { const renames = []; stateValues.forEach((statePage) => { const rename = pageStructure .get("flat") .find((page) => page.id === statePage.id && page.pageTitle !== statePage.pageTitle); if (rename) { renames.push({ newOne: rename, oldOne: statePage, }); } }); LOGGER.debug(`Found pages that need to be renamed ${JSON.stringify(renames)}`); return renames; }; exports.getRenamedPages = getRenamedPages;