Skip to content

Commit

Permalink
Merge pull request #378 from Roave/replace-code-blocks-with-confluenc…
Browse files Browse the repository at this point in the history
…e-code-widgets

Added Confluence writer method to replace code blocks with Confluence code widgets
  • Loading branch information
asgrim authored Apr 23, 2024
2 parents 07eb4db + 81a596a commit 42accd4
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 0 deletions.
65 changes: 65 additions & 0 deletions src/Writer/ConfluenceWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -35,6 +36,37 @@
/** @psalm-type ListOfExtractedImageData = list<array{hashFilename: string, data: string}> */
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 = '<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>';

private readonly string $confluenceContentApiUrl;
Expand Down Expand Up @@ -87,6 +119,7 @@ public function __invoke(array $docbookPages): void
);

$confluenceContent = $this->replaceLocalMarkdownLinks($page, $mapPathsToConfluencePageIds, $confluenceContent);
$confluenceContent = $this->replaceCodeBlocks($confluenceContent);

$hashUpdateMethod = 'POST';
$latestContentHash = md5($confluenceContent);
Expand Down Expand Up @@ -280,6 +313,38 @@ function (array $m) use ($currentPagePath, $mapPathsToConfluencePageIds): string
);
}

private function replaceCodeBlocks(string $renderedContent): string
{
return (string) preg_replace_callback(
'/<pre><code(?: class="lang-([^"]+)"|)>([^<]+)<\/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'
<ac:structured-macro ac:name="code" ac:schema-version="1">
<ac:parameter ac:name="language">%s</ac:parameter>
<ac:plain-text-body><![CDATA[%s]]>
</ac:plain-text-body>
</ac:structured-macro>
XML,
$confluenceCodeLanguage,
html_entity_decode($m[2]), // Since this is rendered in CDATA, we should not escape HTML entities
);
},
$renderedContent,
);
}

/**
* @param array<array-key, mixed>|null $bodyContent
* @param array<string, string> $overrideHeaders
Expand Down
111 changes: 111 additions & 0 deletions test/unit/Writer/ConfluenceWriterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
<p>Regular text before.</p>
<pre><code>just some plaintext code</code></pre>
<pre><code class="lang-json">{
"some": true,
"json": 123
}
</code></pre>
<pre><code class="lang-shell">make build</code></pre>
<pre><code class="lang-gobbledygook">this gobbledygook language is not a supported Confluence language</code></pre>
<pre><code class="lang-html">&lt;html&gt;
&lt;body&gt;
&lt;pre&gt;&lt;code&gt;Example of some HTML to trip up the regex&lt;/code&gt;&lt;/pre&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Regular text after.</p>
HTML,
)->withFrontMatter(['confluencePageId' => 123456789]),
]);

/** @psalm-var array<self::*,array{request:RequestInterface}> $guzzleLog */

$postedPageContent = $guzzleLog[2]['request'];
assert($postedPageContent instanceof RequestInterface);
$this->assertPostContentRequestWasCorrect(
$postedPageContent,
123456789,
<<<'HTML'
<p>Regular text before.</p>
<ac:structured-macro ac:name="code" ac:schema-version="1">
<ac:parameter ac:name="language">none</ac:parameter>
<ac:plain-text-body><![CDATA[just some plaintext code]]>
</ac:plain-text-body>
</ac:structured-macro>
<ac:structured-macro ac:name="code" ac:schema-version="1">
<ac:parameter ac:name="language">javascript</ac:parameter>
<ac:plain-text-body><![CDATA[{
"some": true,
"json": 123
}
]]>
</ac:plain-text-body>
</ac:structured-macro>
<ac:structured-macro ac:name="code" ac:schema-version="1">
<ac:parameter ac:name="language">bash</ac:parameter>
<ac:plain-text-body><![CDATA[make build]]>
</ac:plain-text-body>
</ac:structured-macro>
<ac:structured-macro ac:name="code" ac:schema-version="1">
<ac:parameter ac:name="language">none</ac:parameter>
<ac:plain-text-body><![CDATA[this gobbledygook language is not a supported Confluence language]]>
</ac:plain-text-body>
</ac:structured-macro>
<ac:structured-macro ac:name="code" ac:schema-version="1">
<ac:parameter ac:name="language">html</ac:parameter>
<ac:plain-text-body><![CDATA[<html>
<body>
<pre><code>Example of some HTML to trip up the regex</code></pre>
</body>
</html>
]]>
</ac:plain-text-body>
</ac:structured-macro>
<p>Regular text after.</p>
HTML,
2,
);

self::assertContains(
sprintf('[%s] - OK! Successfully updated confluence page 123456789 with page-slug ...', ConfluenceWriter::class),
$this->testLogger->logMessages,
);
}
}

0 comments on commit 42accd4

Please sign in to comment.