@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
JavaScript
// 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;