Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions components/markdown-confluence-sync/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

#### Added

* feat: Add code blocks transformation to Confluence code macro format.
Code blocks are now converted to Confluence's structured code macro
with syntax highlighting support. This feature is enabled by default
and can be disabled via `confluence.rehype.codeBlocks` configuration
option for compatibility with older Confluence versions.

#### Changed
#### Fixed
#### Deprecated
Expand Down
28 changes: 28 additions & 0 deletions components/markdown-confluence-sync/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ The namespace for the configuration of this library is `markdown-confluence-sync
| `confluence.rootPageName` | `string` | Customize Confluence page titles by adding a prefix to all of them for improved organization and clarity | |
| `confluence.noticeMessage` | `string` | Notice message to add at the beginning of the Confluence pages. | |
| `confluence.noticeTemplate` | `string` | Template string to use for the notice message. | |
| `confluence.rehype` | `object` | Rehype plugin options to customize markdown to Confluence HTML conversion. | |
| `confluence.rehype.codeBlocks` | `boolean` | Enable conversion of code blocks to Confluence code macro format with syntax highlighting. When disabled, code blocks remain as plain HTML pre/code tags. | `true` |
| `confluence.dryRun` | `boolean` | Log create, update or delete requests to Confluence instead of really making them | `false` |
| `dryRun` | `boolean` | Process markdown files without sending them to `confluence-sync`. Useful to early detection of possible errors in configuration, etc. Note that, requests that would be made to Confluence won't be logged, use `confluence.dryRun` for that, which also connects to Confluence to calculate the requests to do | `false` |
| `config.readArguments` | `boolean` | Read configuration from arguments or not | `false` |
Expand Down Expand Up @@ -493,6 +495,32 @@ Apart of supporting the most common markdown features, the library also supports
<ac:rich-text-body><p>This is the content of the details.</p></ac:rich-text-body>
</ac:structured-macro>
```
* Code blocks - Markdown fenced code blocks are converted to
Confluence code macro format with syntax highlighting support. This
feature is enabled by default but can be disabled via the
`confluence.rehype.codeBlocks` configuration option.
* The plugin converts fenced code blocks to Confluence's
`<ac:structured-macro ac:name="code">` format.
* Language syntax highlighting is preserved when specified in the
markdown code fence.
* This feature can be disabled for compatibility with older
Confluence versions by setting
`confluence.rehype.codeBlocks: false`.
* For example, the following markdown code block:
````markdown
```javascript
const hello = "world";
console.log(hello);
```
````
will be converted to:
```markdown
<ac:structured-macro ac:name="code">
<ac:parameter ac:name="language">javascript</ac:parameter>
<ac:plain-text-body><![CDATA[const hello = "world";
console.log(hello);]]></ac:plain-text-body>
</ac:structured-macro>
```

### Unsupported features

Expand Down
2 changes: 1 addition & 1 deletion components/markdown-confluence-sync/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@telefonica/markdown-confluence-sync",
"description": "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",
"version": "2.2.0",
"version": "2.3.0",
"license": "Apache-2.0",
"author": "Telefónica Innovación Digital",
"repository": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { InvalidTemplateError } from "./errors/InvalidTemplateError.js";
import rehypeAddAttachmentsImages from "./support/rehype/rehype-add-attachments-images.js";
import type { ImagesMetadata } from "./support/rehype/rehype-add-attachments-images.types.js";
import rehypeAddNotice from "./support/rehype/rehype-add-notice.js";
import rehypeReplaceCodeBlocks from "./support/rehype/rehype-replace-code-blocks.js";
import rehypeReplaceDetails from "./support/rehype/rehype-replace-details.js";
import rehypeReplaceImgTags from "./support/rehype/rehype-replace-img-tags.js";
import rehypeReplaceInternalReferences from "./support/rehype/rehype-replace-internal-references.js";
Expand All @@ -50,13 +51,15 @@ export const ConfluencePageTransformer: ConfluencePageTransformerConstructor = c
private readonly _rootPageName?: string;
private readonly _spaceKey: string;
private readonly _logger?: LoggerInterface;
private readonly _rehypeCodeBlocksEnabled: boolean;

constructor({
noticeMessage,
noticeTemplate,
rootPageName,
spaceKey,
logger,
rehype,
}: ConfluencePageTransformerOptions) {
this._noticeMessage = noticeMessage;
this._noticeTemplateRaw = noticeTemplate;
Expand All @@ -66,6 +69,7 @@ export const ConfluencePageTransformer: ConfluencePageTransformerConstructor = c
this._rootPageName = rootPageName;
this._spaceKey = spaceKey;
this._logger = logger;
this._rehypeCodeBlocksEnabled = rehype?.codeBlocks ?? true;
}

public async transform(
Expand All @@ -88,7 +92,7 @@ export const ConfluencePageTransformer: ConfluencePageTransformerConstructor = c
DEFAULT_MERMAID_DIAGRAMS_LOCATION,
);
try {
const content = remark()
let processor = remark()
.use(remarkGfm)
.use(remarkFrontmatter)
.use(remarkRemoveFootnotes)
Expand All @@ -101,7 +105,14 @@ export const ConfluencePageTransformer: ConfluencePageTransformerConstructor = c
.use(rehypeAddNotice, { noticeMessage })
.use(rehypeReplaceDetails)
.use(rehypeReplaceStrikethrough)
.use(rehypeReplaceTaskList)
.use(rehypeReplaceTaskList);

// Conditionally add code blocks plugin
if (this._rehypeCodeBlocksEnabled) {
processor = processor.use(rehypeReplaceCodeBlocks);
}

const content = processor
.use(rehypeAddAttachmentsImages)
.use(rehypeReplaceImgTags)
.use(rehypeReplaceInternalReferences, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import type { ConfluenceInputPage } from "@telefonica/confluence-sync";

import type { ConfluenceSyncPage } from "../ConfluenceSync.types.js";

export interface RehypePluginOptions {
/**
* Enable code blocks transformation to Confluence code macro.
* When enabled, markdown code blocks will be converted to Confluence's
* structured code macro format with syntax highlighting support.
* @default true
*/
codeBlocks?: boolean;
}

export interface ConfluencePageTransformerOptions {
/** Confluence page notice message */
noticeMessage?: string;
Expand All @@ -24,6 +34,8 @@ export interface ConfluencePageTransformerOptions {
spaceKey: string;
/** Logger */
logger?: LoggerInterface;
/** Rehype plugin options */
rehype?: RehypePluginOptions;
}

/** Creates a ConfluencePageTransformer interface */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2025 Telefónica Innovación Digital
// SPDX-License-Identifier: Apache-2.0

import type { Element as HastElement, Root, Text as HastText } from "hast";
import type { Plugin as UnifiedPlugin } from "unified";

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: UnifiedPlugin<[], Root> =
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 as HastElement).tagName === "code",
) as HastElement | undefined;

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: HastElement[] = [];

// Add language parameter if present
if (language) {
macroChildren.push({
type: "element" as const,
tagName: "ac:parameter",
properties: {
"ac:name": "language",
},
children: [
{
type: "text" as const,
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" as const,
tagName: "ac:plain-text-body",
properties: {},
children: [
{
type: "text" as const,
value: `<![CDATA[${codeContent}]]>`,
},
],
});

return {
type: "element" as const,
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: HastElement): string | undefined {
const className = codeElement.properties?.className;

if (!className) {
return undefined;
}

// className can be a string or an array of strings
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: HastElement): string {
let text = "";

for (const child of element.children) {
if (child.type === "text") {
text += (child as HastText).value;
} else if (child.type === "element") {
text += extractTextContent(child as HastElement);
}
}

return text;
}

export default rehypeReplaceCodeBlocks;
Loading
Loading