Skip to content

Commit 42accd4

Browse files
authored
Merge pull request #378 from Roave/replace-code-blocks-with-confluence-code-widgets
Added Confluence writer method to replace code blocks with Confluence code widgets
2 parents 07eb4db + 81a596a commit 42accd4

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

src/Writer/ConfluenceWriter.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use function array_merge;
2121
use function dirname;
2222
use function hash_equals;
23+
use function html_entity_decode;
2324
use function in_array;
2425
use function md5;
2526
use function preg_replace_callback;
@@ -35,6 +36,37 @@
3536
/** @psalm-type ListOfExtractedImageData = list<array{hashFilename: string, data: string}> */
3637
final class ConfluenceWriter implements OutputWriter
3738
{
39+
/** @link https://confluence.atlassian.com/doc/code-block-macro-139390.html */
40+
private const ALLOWED_CONFLUENCE_CODE_FORMATS = [
41+
'actionscript',
42+
'applescript',
43+
'bash',
44+
'csharp',
45+
'coldfusion',
46+
'cpp',
47+
'css',
48+
'delphi',
49+
'diff',
50+
'erlang',
51+
'groovy',
52+
'html',
53+
'java',
54+
'javafx',
55+
'javascript',
56+
'none',
57+
'perl',
58+
'php',
59+
'powershell',
60+
'python',
61+
'ruby',
62+
'sass',
63+
'scala',
64+
'sql',
65+
'xml',
66+
'vb',
67+
'yaml',
68+
];
69+
3870
private const CONFLUENCE_HEADER = '<p><strong style="color: #ff0000;">NOTE: This documentation is auto generated, do not edit this directly in Confluence, as your changes will be overwritten!</strong></p>';
3971

4072
private readonly string $confluenceContentApiUrl;
@@ -87,6 +119,7 @@ public function __invoke(array $docbookPages): void
87119
);
88120

89121
$confluenceContent = $this->replaceLocalMarkdownLinks($page, $mapPathsToConfluencePageIds, $confluenceContent);
122+
$confluenceContent = $this->replaceCodeBlocks($confluenceContent);
90123

91124
$hashUpdateMethod = 'POST';
92125
$latestContentHash = md5($confluenceContent);
@@ -280,6 +313,38 @@ function (array $m) use ($currentPagePath, $mapPathsToConfluencePageIds): string
280313
);
281314
}
282315

316+
private function replaceCodeBlocks(string $renderedContent): string
317+
{
318+
return (string) preg_replace_callback(
319+
'/<pre><code(?: class="lang-([^"]+)"|)>([^<]+)<\/code><\/pre>/',
320+
static function (array $m): string {
321+
/** @var array{1: string, 2: string} $m */
322+
$confluenceCodeLanguage = match ($m[1]) {
323+
'json', 'js' => 'javascript',
324+
'shell' => 'bash',
325+
default => $m[1]
326+
};
327+
328+
if (! in_array($confluenceCodeLanguage, self::ALLOWED_CONFLUENCE_CODE_FORMATS, true)) {
329+
$confluenceCodeLanguage = 'none';
330+
}
331+
332+
return sprintf(
333+
<<<'XML'
334+
<ac:structured-macro ac:name="code" ac:schema-version="1">
335+
<ac:parameter ac:name="language">%s</ac:parameter>
336+
<ac:plain-text-body><![CDATA[%s]]>
337+
</ac:plain-text-body>
338+
</ac:structured-macro>
339+
XML,
340+
$confluenceCodeLanguage,
341+
html_entity_decode($m[2]), // Since this is rendered in CDATA, we should not escape HTML entities
342+
);
343+
},
344+
$renderedContent,
345+
);
346+
}
347+
283348
/**
284349
* @param array<array-key, mixed>|null $bodyContent
285350
* @param array<string, string> $overrideHeaders

test/unit/Writer/ConfluenceWriterTest.php

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,4 +379,115 @@ public function testConfluenceUploadWithLinksToOtherPages(): void
379379
$this->testLogger->logMessages,
380380
);
381381
}
382+
383+
public function testConfluenceUploadReplacesCodeBlocks(): void
384+
{
385+
$guzzleLog = [];
386+
387+
$handlerStack = HandlerStack::create(new MockHandler([
388+
// GET /{pageId}
389+
0 => new Response(200, [], json_encode([
390+
'id' => '123456789',
391+
'type' => 'page type',
392+
'title' => 'page title',
393+
'space' => ['key' => 'space key'],
394+
'version' => ['number' => '1'],
395+
], JSON_THROW_ON_ERROR)),
396+
// GET /{pageId}/child/attachment
397+
1 => new Response(200, [], json_encode([
398+
'results' => [
399+
['title' => 'attachment'],
400+
],
401+
], JSON_THROW_ON_ERROR)),
402+
// PUT /{pageId}
403+
2 => new Response(200, [], json_encode([], JSON_THROW_ON_ERROR)),
404+
]));
405+
$handlerStack->push(Middleware::history($guzzleLog));
406+
407+
$confluence = new ConfluenceWriter(
408+
new Client(['handler' => $handlerStack]),
409+
'https://fake-confluence-url',
410+
'Something',
411+
$this->testLogger,
412+
true,
413+
);
414+
415+
$confluence->__invoke([
416+
DocbookPage::fromSlugAndContent(
417+
'/fake/path',
418+
'page-slug',
419+
<<<'HTML'
420+
<p>Regular text before.</p>
421+
<pre><code>just some plaintext code</code></pre>
422+
<pre><code class="lang-json">{
423+
"some": true,
424+
"json": 123
425+
}
426+
</code></pre>
427+
<pre><code class="lang-shell">make build</code></pre>
428+
<pre><code class="lang-gobbledygook">this gobbledygook language is not a supported Confluence language</code></pre>
429+
<pre><code class="lang-html">&lt;html&gt;
430+
&lt;body&gt;
431+
&lt;pre&gt;&lt;code&gt;Example of some HTML to trip up the regex&lt;/code&gt;&lt;/pre&gt;
432+
&lt;/body&gt;
433+
&lt;/html&gt;
434+
</code></pre>
435+
<p>Regular text after.</p>
436+
HTML,
437+
)->withFrontMatter(['confluencePageId' => 123456789]),
438+
]);
439+
440+
/** @psalm-var array<self::*,array{request:RequestInterface}> $guzzleLog */
441+
442+
$postedPageContent = $guzzleLog[2]['request'];
443+
assert($postedPageContent instanceof RequestInterface);
444+
$this->assertPostContentRequestWasCorrect(
445+
$postedPageContent,
446+
123456789,
447+
<<<'HTML'
448+
<p>Regular text before.</p>
449+
<ac:structured-macro ac:name="code" ac:schema-version="1">
450+
<ac:parameter ac:name="language">none</ac:parameter>
451+
<ac:plain-text-body><![CDATA[just some plaintext code]]>
452+
</ac:plain-text-body>
453+
</ac:structured-macro>
454+
<ac:structured-macro ac:name="code" ac:schema-version="1">
455+
<ac:parameter ac:name="language">javascript</ac:parameter>
456+
<ac:plain-text-body><![CDATA[{
457+
"some": true,
458+
"json": 123
459+
}
460+
]]>
461+
</ac:plain-text-body>
462+
</ac:structured-macro>
463+
<ac:structured-macro ac:name="code" ac:schema-version="1">
464+
<ac:parameter ac:name="language">bash</ac:parameter>
465+
<ac:plain-text-body><![CDATA[make build]]>
466+
</ac:plain-text-body>
467+
</ac:structured-macro>
468+
<ac:structured-macro ac:name="code" ac:schema-version="1">
469+
<ac:parameter ac:name="language">none</ac:parameter>
470+
<ac:plain-text-body><![CDATA[this gobbledygook language is not a supported Confluence language]]>
471+
</ac:plain-text-body>
472+
</ac:structured-macro>
473+
<ac:structured-macro ac:name="code" ac:schema-version="1">
474+
<ac:parameter ac:name="language">html</ac:parameter>
475+
<ac:plain-text-body><![CDATA[<html>
476+
<body>
477+
<pre><code>Example of some HTML to trip up the regex</code></pre>
478+
</body>
479+
</html>
480+
]]>
481+
</ac:plain-text-body>
482+
</ac:structured-macro>
483+
<p>Regular text after.</p>
484+
HTML,
485+
2,
486+
);
487+
488+
self::assertContains(
489+
sprintf('[%s] - OK! Successfully updated confluence page 123456789 with page-slug ...', ConfluenceWriter::class),
490+
$this->testLogger->logMessages,
491+
);
492+
}
382493
}

0 commit comments

Comments
 (0)