Skip to content

Commit 7819627

Browse files
authored
Merge pull request #66 from FrankLedo/feat/65/code-blocks-confluence-macro
feat: Add code blocks transformation to Confluence code macro
2 parents a0c87be + 0dedab1 commit 7819627

File tree

7 files changed

+421
-3
lines changed

7 files changed

+421
-3
lines changed

components/markdown-confluence-sync/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
#### Added
9+
10+
* feat: Add code blocks transformation to Confluence code macro format.
11+
Code blocks are now converted to Confluence's structured code macro
12+
with syntax highlighting support. This feature is disabled by default
13+
and can be enabled via `codeBlocks` configuration option.
14+
915
#### Changed
1016
#### Fixed
1117
#### Deprecated

components/markdown-confluence-sync/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ The namespace for the configuration of this library is `markdown-confluence-sync
301301
| `confluence.noticeMessage` | `string` | Notice message to add at the beginning of the Confluence pages. | |
302302
| `confluence.noticeTemplate` | `string` | Template string to use for the notice message. | |
303303
| `confluence.dryRun` | `boolean` | Log create, update or delete requests to Confluence instead of really making them | `false` |
304+
| `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. | `false` |
304305
| `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` |
305306
| `config.readArguments` | `boolean` | Read configuration from arguments or not | `false` |
306307
| `config.readFile` | `boolean` | Read configuration from file or not | `false` |
@@ -493,6 +494,31 @@ Apart of supporting the most common markdown features, the library also supports
493494
<ac:rich-text-body><p>This is the content of the details.</p></ac:rich-text-body>
494495
</ac:structured-macro>
495496
```
497+
* Code blocks - Markdown fenced code blocks can be converted to
498+
Confluence code macro format with syntax highlighting support. This
499+
feature is disabled by default but can be enabled via the
500+
`codeBlocks` configuration option.
501+
* The plugin converts fenced code blocks to Confluence's
502+
`<ac:structured-macro ac:name="code">` format.
503+
* Language syntax highlighting is preserved when specified in the
504+
markdown code fence.
505+
* This feature is disabled by default for compatibility with older
506+
Confluence versions. Enable it by setting `codeBlocks: true`.
507+
* For example, the following markdown code block:
508+
````markdown
509+
```javascript
510+
const hello = "world";
511+
console.log(hello);
512+
```
513+
````
514+
will be converted to:
515+
```markdown
516+
<ac:structured-macro ac:name="code">
517+
<ac:parameter ac:name="language">javascript</ac:parameter>
518+
<ac:plain-text-body><![CDATA[const hello = "world";
519+
console.log(hello);]]></ac:plain-text-body>
520+
</ac:structured-macro>
521+
```
496522
497523
### Unsupported features
498524

components/markdown-confluence-sync/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@telefonica/markdown-confluence-sync",
33
"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",
4-
"version": "2.2.0",
4+
"version": "2.3.0",
55
"license": "Apache-2.0",
66
"author": "Telefónica Innovación Digital",
77
"repository": {

components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { InvalidTemplateError } from "./errors/InvalidTemplateError.js";
2727
import rehypeAddAttachmentsImages from "./support/rehype/rehype-add-attachments-images.js";
2828
import type { ImagesMetadata } from "./support/rehype/rehype-add-attachments-images.types.js";
2929
import rehypeAddNotice from "./support/rehype/rehype-add-notice.js";
30+
import rehypeReplaceCodeBlocks from "./support/rehype/rehype-replace-code-blocks.js";
3031
import rehypeReplaceDetails from "./support/rehype/rehype-replace-details.js";
3132
import rehypeReplaceImgTags from "./support/rehype/rehype-replace-img-tags.js";
3233
import rehypeReplaceInternalReferences from "./support/rehype/rehype-replace-internal-references.js";
@@ -50,13 +51,15 @@ export const ConfluencePageTransformer: ConfluencePageTransformerConstructor = c
5051
private readonly _rootPageName?: string;
5152
private readonly _spaceKey: string;
5253
private readonly _logger?: LoggerInterface;
54+
private readonly _rehypeCodeBlocksEnabled: boolean;
5355

5456
constructor({
5557
noticeMessage,
5658
noticeTemplate,
5759
rootPageName,
5860
spaceKey,
5961
logger,
62+
codeBlocks,
6063
}: ConfluencePageTransformerOptions) {
6164
this._noticeMessage = noticeMessage;
6265
this._noticeTemplateRaw = noticeTemplate;
@@ -66,6 +69,7 @@ export const ConfluencePageTransformer: ConfluencePageTransformerConstructor = c
6669
this._rootPageName = rootPageName;
6770
this._spaceKey = spaceKey;
6871
this._logger = logger;
72+
this._rehypeCodeBlocksEnabled = codeBlocks ?? false;
6973
}
7074

7175
public async transform(
@@ -88,7 +92,7 @@ export const ConfluencePageTransformer: ConfluencePageTransformerConstructor = c
8892
DEFAULT_MERMAID_DIAGRAMS_LOCATION,
8993
);
9094
try {
91-
const content = remark()
95+
let processor = remark()
9296
.use(remarkGfm)
9397
.use(remarkFrontmatter)
9498
.use(remarkRemoveFootnotes)
@@ -101,7 +105,14 @@ export const ConfluencePageTransformer: ConfluencePageTransformerConstructor = c
101105
.use(rehypeAddNotice, { noticeMessage })
102106
.use(rehypeReplaceDetails)
103107
.use(rehypeReplaceStrikethrough)
104-
.use(rehypeReplaceTaskList)
108+
.use(rehypeReplaceTaskList);
109+
110+
// Conditionally add code blocks plugin
111+
if (this._rehypeCodeBlocksEnabled) {
112+
processor = processor.use(rehypeReplaceCodeBlocks);
113+
}
114+
115+
const content = processor
105116
.use(rehypeAddAttachmentsImages)
106117
.use(rehypeReplaceImgTags)
107118
.use(rehypeReplaceInternalReferences, {

components/markdown-confluence-sync/src/lib/confluence/transformer/ConfluencePageTransformer.types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ export interface ConfluencePageTransformerOptions {
2424
spaceKey: string;
2525
/** Logger */
2626
logger?: LoggerInterface;
27+
/**
28+
* Enable code blocks transformation to Confluence code macro.
29+
* When enabled, markdown code blocks will be converted to Confluence's
30+
* structured code macro format with syntax highlighting support.
31+
* @default false
32+
*/
33+
codeBlocks?: boolean;
2734
}
2835

2936
/** Creates a ConfluencePageTransformer interface */
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// SPDX-FileCopyrightText: 2025 Telefónica Innovación Digital
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type { Element as HastElement, Root, Text as HastText } from "hast";
5+
import type { Plugin as UnifiedPlugin } from "unified";
6+
7+
import { replace } from "../../../../support/unist/unist-util-replace.js";
8+
9+
/**
10+
* UnifiedPlugin to replace `<pre><code>` HastElements with Confluence's
11+
* structured code macro format.
12+
*
13+
* @see {@link https://developer.atlassian.com/server/confluence/confluence-storage-format/ | Confluence Storage Format }
14+
*
15+
* @example
16+
* <pre><code class="language-javascript">const x = 42;</code></pre>
17+
* // becomes
18+
* <ac:structured-macro ac:name="code">
19+
* <ac:parameter ac:name="language">javascript</ac:parameter>
20+
* <ac:plain-text-body><![CDATA[const x = 42;]]></ac:plain-text-body>
21+
* </ac:structured-macro>
22+
*/
23+
const rehypeReplaceCodeBlocks: UnifiedPlugin<[], Root> =
24+
function rehypeReplaceCodeBlocks() {
25+
return function transformer(tree) {
26+
replace(tree, { type: "element", tagName: "pre" }, (node) => {
27+
// Check if this pre element contains a code element
28+
const codeElement = node.children.find(
29+
(child) =>
30+
child.type === "element" &&
31+
(child as HastElement).tagName === "code",
32+
) as HastElement | undefined;
33+
34+
if (!codeElement) {
35+
// If there's no code element, return the pre element unchanged
36+
return node;
37+
}
38+
39+
// Extract the language from the code element's className
40+
const language = extractLanguage(codeElement);
41+
42+
// Extract the text content from the code element
43+
const codeContent = extractTextContent(codeElement);
44+
45+
// Build the Confluence code macro
46+
const macroChildren: HastElement[] = [];
47+
48+
// Add language parameter if present
49+
if (language) {
50+
macroChildren.push({
51+
type: "element" as const,
52+
tagName: "ac:parameter",
53+
properties: {
54+
"ac:name": "language",
55+
},
56+
children: [
57+
{
58+
type: "text" as const,
59+
value: language,
60+
},
61+
],
62+
});
63+
}
64+
65+
// Add the code content
66+
// Note: We use a text node with the raw CDATA markup
67+
// The rehypeStringify with allowDangerousHtml will preserve it
68+
macroChildren.push({
69+
type: "element" as const,
70+
tagName: "ac:plain-text-body",
71+
properties: {},
72+
children: [
73+
{
74+
type: "text" as const,
75+
value: `<![CDATA[${codeContent}]]>`,
76+
},
77+
],
78+
});
79+
80+
return {
81+
type: "element" as const,
82+
tagName: "ac:structured-macro",
83+
properties: {
84+
"ac:name": "code",
85+
},
86+
children: macroChildren,
87+
};
88+
});
89+
};
90+
};
91+
92+
/**
93+
* Extract the language from the code element's className property.
94+
* Markdown renderers typically add classes like "language-javascript"
95+
* to code elements.
96+
*
97+
* @param codeElement - The code element to extract the language from
98+
* @returns The language identifier or undefined if not found
99+
*/
100+
function extractLanguage(codeElement: HastElement): string | undefined {
101+
const className = codeElement.properties?.className;
102+
103+
if (!className) {
104+
return undefined;
105+
}
106+
107+
// className can be a string or an array of strings
108+
const classNames = Array.isArray(className) ? className : [className];
109+
110+
// Look for a class that starts with "language-"
111+
for (const cls of classNames) {
112+
if (typeof cls === "string" && cls.startsWith("language-")) {
113+
return cls.substring(9); // Remove "language-" prefix
114+
}
115+
}
116+
117+
return undefined;
118+
}
119+
120+
/**
121+
* Extract all text content from an element recursively.
122+
*
123+
* @param element - The element to extract text from
124+
* @returns The concatenated text content
125+
*/
126+
function extractTextContent(element: HastElement): string {
127+
let text = "";
128+
129+
for (const child of element.children) {
130+
if (child.type === "text") {
131+
text += (child as HastText).value;
132+
} else if (child.type === "element") {
133+
text += extractTextContent(child as HastElement);
134+
}
135+
}
136+
137+
return text;
138+
}
139+
140+
export default rehypeReplaceCodeBlocks;

0 commit comments

Comments
 (0)