Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Confluence writer method to replace code blocks with Confluence code widgets #378

Merged
merged 2 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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(
asgrim marked this conversation as resolved.
Show resolved Hide resolved
'/<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">
asgrim marked this conversation as resolved.
Show resolved Hide resolved
<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,
);
}
}