From 3b23cb72595f61d18a83a343b319525d7e80b1f6 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 23 Apr 2024 12:32:14 +0100 Subject: [PATCH 1/2] Added Confluence writer method to replace code blocks with Confluence code widgets --- src/Writer/ConfluenceWriter.php | 64 +++++++++++++ test/unit/Writer/ConfluenceWriterTest.php | 111 ++++++++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/src/Writer/ConfluenceWriter.php b/src/Writer/ConfluenceWriter.php index f877b209..b0ae8fcf 100644 --- a/src/Writer/ConfluenceWriter.php +++ b/src/Writer/ConfluenceWriter.php @@ -35,6 +35,37 @@ /** @psalm-type ListOfExtractedImageData = list */ final class ConfluenceWriter implements OutputWriter { + /** @link https://confluence.atlassian.com/doc/code-block-macro-139390.html */ + private const ALLOWED_CONFLUENCE_CODE_FORMATS = [ + 'actionscript', + 'applescript', + 'bash', + 'csharp', + 'coldfusion', + 'cpp', + 'css', + 'delphi', + 'diff', + 'erlang', + 'groovy', + 'html', + 'java', + 'javafx', + 'javascript', + 'none', + 'perl', + 'php', + 'powershell', + 'python', + 'ruby', + 'sass', + 'scala', + 'sql', + 'xml', + 'vb', + 'yaml', + ]; + private const CONFLUENCE_HEADER = '

NOTE: This documentation is auto generated, do not edit this directly in Confluence, as your changes will be overwritten!

'; private readonly string $confluenceContentApiUrl; @@ -87,6 +118,7 @@ public function __invoke(array $docbookPages): void ); $confluenceContent = $this->replaceLocalMarkdownLinks($page, $mapPathsToConfluencePageIds, $confluenceContent); + $confluenceContent = $this->replaceCodeBlocks($confluenceContent); $hashUpdateMethod = 'POST'; $latestContentHash = md5($confluenceContent); @@ -280,6 +312,38 @@ function (array $m) use ($currentPagePath, $mapPathsToConfluencePageIds): string ); } + private function replaceCodeBlocks(string $renderedContent): string + { + return (string) preg_replace_callback( + '/
([^<]+)<\/code><\/pre>/',
+            static function (array $m): string {
+                /** @var array{1: string, 2: string} $m */
+                $confluenceCodeLanguage = match ($m[1]) {
+                    'json', 'js' => 'javascript',
+                    'shell' => 'bash',
+                    default => $m[1]
+                };
+
+                if (! in_array($confluenceCodeLanguage, self::ALLOWED_CONFLUENCE_CODE_FORMATS, true)) {
+                    $confluenceCodeLanguage = 'none';
+                }
+
+                return sprintf(
+                    <<<'XML'
+
+  %s
+  
+  
+
+XML,
+                    $confluenceCodeLanguage,
+                    $m[2],
+                );
+            },
+            $renderedContent,
+        );
+    }
+
     /**
      * @param array|null                                                        $bodyContent
      * @param array                                                               $overrideHeaders
diff --git a/test/unit/Writer/ConfluenceWriterTest.php b/test/unit/Writer/ConfluenceWriterTest.php
index 995a2f92..2f8e7320 100644
--- a/test/unit/Writer/ConfluenceWriterTest.php
+++ b/test/unit/Writer/ConfluenceWriterTest.php
@@ -379,4 +379,115 @@ public function testConfluenceUploadWithLinksToOtherPages(): void
             $this->testLogger->logMessages,
         );
     }
+
+    public function testConfluenceUploadReplacesCodeBlocks(): void
+    {
+        $guzzleLog = [];
+
+        $handlerStack = HandlerStack::create(new MockHandler([
+            // GET /{pageId}
+            0 => new Response(200, [], json_encode([
+                'id' => '123456789',
+                'type' => 'page type',
+                'title' => 'page title',
+                'space' => ['key' => 'space key'],
+                'version' => ['number' => '1'],
+            ], JSON_THROW_ON_ERROR)),
+            // GET /{pageId}/child/attachment
+            1 => new Response(200, [], json_encode([
+                'results' => [
+                    ['title' => 'attachment'],
+                ],
+            ], JSON_THROW_ON_ERROR)),
+            // PUT /{pageId}
+            2 => new Response(200, [], json_encode([], JSON_THROW_ON_ERROR)),
+        ]));
+        $handlerStack->push(Middleware::history($guzzleLog));
+
+        $confluence = new ConfluenceWriter(
+            new Client(['handler' => $handlerStack]),
+            'https://fake-confluence-url',
+            'Something',
+            $this->testLogger,
+            true,
+        );
+
+        $confluence->__invoke([
+            DocbookPage::fromSlugAndContent(
+                '/fake/path',
+                'page-slug',
+                <<<'HTML'
+

Regular text before.

+
just some plaintext code
+
{
+    "some": true,
+    "json": 123
+}
+
+
make build
+
this gobbledygook language is not a supported Confluence language
+
<html>
+<body>
+<pre><code>Example of some HTML to trip up the regex</code></pre>
+</body>
+</html>
+
+

Regular text after.

+HTML, + )->withFrontMatter(['confluencePageId' => 123456789]), + ]); + + /** @psalm-var array $guzzleLog */ + + $postedPageContent = $guzzleLog[2]['request']; + assert($postedPageContent instanceof RequestInterface); + $this->assertPostContentRequestWasCorrect( + $postedPageContent, + 123456789, + <<<'HTML' +

Regular text before.

+ + none + + + + + javascript + + + + + bash + + + + + none + + + + + html + + + +

Regular text after.

+HTML, + 2, + ); + + self::assertContains( + sprintf('[%s] - OK! Successfully updated confluence page 123456789 with page-slug ...', ConfluenceWriter::class), + $this->testLogger->logMessages, + ); + } } From 81a596ae717a3d23c22d218596efaebf34e169bf Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 23 Apr 2024 13:12:27 +0100 Subject: [PATCH 2/2] Ensure HTML entities are not double-escaped --- src/Writer/ConfluenceWriter.php | 3 ++- test/unit/Writer/ConfluenceWriterTest.php | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Writer/ConfluenceWriter.php b/src/Writer/ConfluenceWriter.php index b0ae8fcf..4755df23 100644 --- a/src/Writer/ConfluenceWriter.php +++ b/src/Writer/ConfluenceWriter.php @@ -20,6 +20,7 @@ use function array_merge; use function dirname; use function hash_equals; +use function html_entity_decode; use function in_array; use function md5; use function preg_replace_callback; @@ -337,7 +338,7 @@ static function (array $m): string { XML, $confluenceCodeLanguage, - $m[2], + html_entity_decode($m[2]), // Since this is rendered in CDATA, we should not escape HTML entities ); }, $renderedContent, diff --git a/test/unit/Writer/ConfluenceWriterTest.php b/test/unit/Writer/ConfluenceWriterTest.php index 2f8e7320..cadad4a4 100644 --- a/test/unit/Writer/ConfluenceWriterTest.php +++ b/test/unit/Writer/ConfluenceWriterTest.php @@ -472,11 +472,11 @@ public function testConfluenceUploadReplacesCodeBlocks(): void html - + +
Example of some HTML to trip up the regex
+ + ]]>