UNPKG

@telefonica/markdown-confluence-sync

Version:

Creates/updates/deletes Confluence pages based on markdown files in a directory. Supports Mermaid diagrams and per-page configuration using frontmatter metadata. Works great with Docusaurus

188 lines (187 loc) 6.24 kB
// SPDX-FileCopyrightText: 2025 Telefónica Innovación Digital // SPDX-License-Identifier: Apache-2.0 import { replace } from "../../../../support/unist/unist-util-replace.js"; /** * Confluence macro names for different alert types */ const ALERT_TO_MACRO = { NOTE: "info", TIP: "tip", IMPORTANT: "note", WARNING: "warning", CAUTION: "warning", }; /** * Default titles for alert types */ const ALERT_TITLES = { NOTE: "Note", TIP: "Tip", IMPORTANT: "Important", WARNING: "Warning", CAUTION: "Caution", }; /** * UnifiedPlugin to replace GitHub alert blockquotes with Confluence's * structured info/note/warning/tip macro format. * * @see {@link https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts | GitHub Alerts } * @see {@link https://developer.atlassian.com/server/confluence/confluence-storage-format/ | Confluence Storage Format } * * @example * <blockquote> * <p>[!NOTE]<br/>This is a note</p> * </blockquote> * // becomes * <ac:structured-macro ac:name="info"> * <ac:parameter ac:name="title">Note</ac:parameter> * <ac:rich-text-body> * <p>This is a note</p> * </ac:rich-text-body> * </ac:structured-macro> */ const rehypeReplaceGithubAlerts = function rehypeReplaceGithubAlerts() { return function transformer(tree) { replace(tree, { type: "element", tagName: "blockquote" }, (node) => { // Check if this blockquote is a GitHub alert const alertInfo = extractAlertInfo(node); if (!alertInfo) { // Not a GitHub alert, return unchanged return node; } // Build the Confluence macro const macroName = ALERT_TO_MACRO[alertInfo.type]; const macroChildren = []; // Add title parameter macroChildren.push({ type: "element", tagName: "ac:parameter", properties: { "ac:name": "title", }, children: [ { type: "raw", value: ALERT_TITLES[alertInfo.type], }, ], }); // Add the content in a rich text body macroChildren.push({ type: "element", tagName: "ac:rich-text-body", properties: {}, children: alertInfo.content, }); return { type: "element", tagName: "ac:structured-macro", properties: { "ac:name": macroName, }, children: macroChildren, }; }); }; }; /** * Extract alert information from a blockquote element if it contains a * GitHub alert marker. * * @param blockquote - The blockquote element to check * @returns Alert information if this is a GitHub alert, undefined otherwise */ function extractAlertInfo(blockquote) { if (blockquote.children.length === 0) { return undefined; } // Find the first non-whitespace child (skip whitespace text nodes) let contentChild = blockquote.children[0]; let childIndex = 0; while (contentChild && contentChild.type === "text" && !contentChild.value.trim()) { childIndex++; if (childIndex >= blockquote.children.length) { // istanbul ignore next - Defensive check, should not happen return undefined; } contentChild = blockquote.children[childIndex]; } // Handle two cases: text node directly or paragraph element let textNode; let isDirectText = false; if (contentChild.type === "text") { // Direct text node with actual content textNode = contentChild; isDirectText = true; } else if (contentChild.type === "element" && contentChild.tagName === "p") { // Paragraph element const paragraph = contentChild; const firstNode = paragraph.children[0]; if (firstNode && firstNode.type === "text") { textNode = firstNode; } } if (!textNode) { // istanbul ignore next - Defensive check, should not happen return undefined; } const text = textNode.value; // Check if it starts with an alert marker const alertMatch = text.match(/^\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/); if (!alertMatch) { return undefined; } const alertType = alertMatch[1]; // Remove the alert marker from the text const remainingText = text.substring(alertMatch[0].length).trim(); // Build content based on structure const content = []; if (isDirectText) { // For direct text, wrap remaining content in a paragraph if (remainingText) { content.push({ type: "element", tagName: "p", properties: {}, children: [ { type: "text", value: remainingText, }, ], }); } // Add any other children from the blockquote (after the text node we used) content.push(...blockquote.children.slice(childIndex + 1)); } else { // For paragraph structure const paragraph = contentChild; const newParagraphChildren = [...paragraph.children]; if (remainingText) { newParagraphChildren[0] = { type: "text", value: remainingText, }; } else { newParagraphChildren.shift(); } if (newParagraphChildren.length > 0) { content.push({ ...paragraph, children: newParagraphChildren, }); } // Add any other children from the blockquote (after the paragraph we used) content.push(...blockquote.children.slice(childIndex + 1)); } return { type: alertType, content, }; } export default rehypeReplaceGithubAlerts;