@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
118 lines (117 loc) • 4.33 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";
/**
* UnifiedPlugin to replace `<pre><code>` HastElements with Confluence's
* structured code macro format.
*
* @see {@link https://developer.atlassian.com/server/confluence/confluence-storage-format/ | Confluence Storage Format }
*
* @example
* <pre><code class="language-javascript">const x = 42;</code></pre>
* // becomes
* <ac:structured-macro ac:name="code">
* <ac:parameter ac:name="language">javascript</ac:parameter>
* <ac:plain-text-body><![CDATA[const x = 42;]]></ac:plain-text-body>
* </ac:structured-macro>
*/
const rehypeReplaceCodeBlocks = function rehypeReplaceCodeBlocks() {
return function transformer(tree) {
replace(tree, { type: "element", tagName: "pre" }, (node) => {
// Check if this pre element contains a code element
const codeElement = node.children.find((child) => child.type === "element" &&
child.tagName === "code");
if (!codeElement) {
// If there's no code element, return the pre element unchanged
return node;
}
// Extract the language from the code element's className
const language = extractLanguage(codeElement);
// Extract the text content from the code element
const codeContent = extractTextContent(codeElement);
// Build the Confluence code macro
const macroChildren = [];
// Add language parameter if present
if (language) {
macroChildren.push({
type: "element",
tagName: "ac:parameter",
properties: {
"ac:name": "language",
},
children: [
{
type: "raw",
value: language,
},
],
});
}
// Add the code content
// Note: We use a text node with the raw CDATA markup
// The rehypeStringify with allowDangerousHtml will preserve it
macroChildren.push({
type: "element",
tagName: "ac:plain-text-body",
properties: {},
children: [
{
type: "raw",
value: `<![CDATA[${codeContent}]]>`,
},
],
});
return {
type: "element",
tagName: "ac:structured-macro",
properties: {
"ac:name": "code",
},
children: macroChildren,
};
});
};
};
/**
* Extract the language from the code element's className property.
* Markdown renderers typically add classes like "language-javascript"
* to code elements.
*
* @param codeElement - The code element to extract the language from
* @returns The language identifier or undefined if not found
*/
function extractLanguage(codeElement) {
const className = codeElement.properties?.className;
if (!className) {
return undefined;
}
// className is always an array of strings, but we check it for safety
// istanbul ignore next
const classNames = Array.isArray(className) ? className : [className];
// Look for a class that starts with "language-"
for (const cls of classNames) {
if (typeof cls === "string" && cls.startsWith("language-")) {
return cls.substring(9); // Remove "language-" prefix
}
}
return undefined;
}
/**
* Extract all text content from an element recursively.
*
* @param element - The element to extract text from
* @returns The concatenated text content
*/
function extractTextContent(element) {
let text = "";
for (const child of element.children) {
if (child.type === "text") {
text += child.value;
}
else if (child.type === "element") {
text += extractTextContent(child);
}
}
return text;
}
export default rehypeReplaceCodeBlocks;