feature #4890 Add support for tempest/markdown in markdown-extra (ker0x)

This PR was merged into the 3.x branch.

Discussion
----------

Add support for tempest/markdown in markdown-extra

Adds `TempestMarkdown`, an adapter for [`tempest/markdown`](https://github.com/tempestphp/markdown), alongside the existing `LeagueMarkdown`, `MichelfMarkdown` and `ErusevMarkdown` implementations. It follows the same pattern as the others and accepts a pre-configured `Tempest\Markdown\Markdown` in its constructor, so rules and the highlighter can be customized.

It is also appended as the **last** branch of `DefaultMarkdown`'s discovery chain, so projects that already have another library installed keep resolving to it exactly as before.

### PHP requirement

Every published version of `tempest/markdown` requires PHP `^8.5`, while `twig/markdown-extra` supports `>=8.1`. So:

- it is declared in `require-dev` only;
- a CI step removes it before `composer install` on PHP < 8.5, mirroring the existing conditional step used for `twig-extra-bundle`;
- `FunctionalTest` only adds it to the converter matrix when `Tempest\Markdown\Markdown` exists.

The suite passes both with and without the library installed.

### Test data change

Three cases in `getMarkdownTests()` used Setext headings (`Hello` underlined with `=====`). `tempest/markdown` only implements ATX headings, so those were switched to `# Hello`. Those cases exercise the filter plumbing (`{% apply %}`, indentation stripping, `include()|markdown_to_html`) rather than the Markdown dialect, so no coverage is lost.

Two patterns were also relaxed for the same reason: `<h1[^>]*>` because Tempest emits auto heading ids, and `<p>…\s*</p>` because it keeps the source's trailing newline inside the final paragraph. Both remain accurate for the other converters.

These differences, plus the fact that front matter is parsed out rather than rendered, are documented in a note in `doc/filters/markdown_to_html.rst`.

### Unrelated one-liner

The last commit also adds `.php-cs-fixer.cache` to `.gitignore` — it is generated by the project's own `php-cs-fixer` dev dependency and was showing up as untracked. Happy to split it out if you'd rather keep this PR to a single concern.

Commits
-------

aa17f59877 Add support for tempest/markdown in markdown-extra
This commit is contained in:
Fabien Potencier
2026-08-13 10:02:40 +02:00
8 changed files with 68 additions and 16 deletions
+5
View File
@@ -151,6 +151,11 @@ jobs:
working-directory: extra/${{ matrix.extension }}
run: "composer require --no-update 'symfony/translation-contracts:^1.1|^2.0'"
- name: "Prevent installing tempest/markdown, which requires PHP 8.5"
if: "matrix.extension == 'markdown-extra' && matrix.php-version != '8.5'"
working-directory: extra/${{ matrix.extension }}
run: composer remove --dev --no-update tempest/markdown
- name: "Composer install ${{ matrix.extension }}"
working-directory: extra/${{ matrix.extension }}
run: composer install
+1
View File
@@ -3,4 +3,5 @@
/composer.lock
/phpunit.xml
/vendor
.php-cs-fixer.cache
.phpunit.result.cache
+1
View File
@@ -1,5 +1,6 @@
# 3.29.0 (2026-XX-XX)
* Add `TempestMarkdown` to use `tempest/markdown` as the `markdown_to_html` converter
* Fix imported macros not resolving their own template-level macro imports
* Add the `Twig\Sandbox\SandboxInterface` interface and `Twig\Sandbox\Sandbox` class to render untrusted templates through a dedicated, always-sandboxed environment crafted for it
* Add the `Twig\Extension\SandboxBridgeExtension` to render sandboxed templates from trusted templates with an explicit output escaping strategy
+13 -4
View File
@@ -84,10 +84,11 @@ Using a Custom Converter
The ``markdown_to_html`` filter delegates the conversion to a class implementing
``Twig\Extra\Markdown\MarkdownInterface``. Several implementations are provided:
``LeagueMarkdown`` (``league/commonmark``), ``MichelfMarkdown``
(``michelf/php-markdown``), and ``ErusevMarkdown`` (``erusev/parsedown``). Each
accepts a pre-configured converter in its constructor, so you can tune the
underlying library or switch to another implementation (for instance
``ParsedownExtra``, which extends ``Parsedown``)::
(``michelf/php-markdown``), ``ErusevMarkdown`` (``erusev/parsedown``), and
``TempestMarkdown`` (``tempest/markdown``). Each accepts a pre-configured
converter in its constructor, so you can tune the underlying library or switch
to another implementation (for instance ``ParsedownExtra``, which extends
``Parsedown``)::
use Twig\Extra\Markdown\ErusevMarkdown;
@@ -96,6 +97,14 @@ underlying library or switch to another implementation (for instance
$markdown = new ErusevMarkdown($parsedown);
.. note::
``tempest/markdown`` requires PHP 8.5 or later. It also differs from the
other libraries on two points worth knowing about: Setext headings
(``Title`` underlined with ``===``) are not supported, so use ATX headings
(``# Title``) instead, and any YAML front matter is parsed out of the
rendered HTML rather than being rendered.
When using ``twig/extra-bundle``, register your converter as the
``twig.markdown.default`` service to make it the one used by the filter:
+3
View File
@@ -13,6 +13,7 @@ namespace Twig\Extra\Markdown;
use League\CommonMark\CommonMarkConverter;
use Michelf\MarkdownExtra;
use Tempest\Markdown\Markdown;
class DefaultMarkdown implements MarkdownInterface
{
@@ -26,6 +27,8 @@ class DefaultMarkdown implements MarkdownInterface
$this->converter = new MichelfMarkdown();
} elseif (class_exists(\Parsedown::class)) {
$this->converter = new ErusevMarkdown();
} elseif (class_exists(Markdown::class)) {
$this->converter = new TempestMarkdown();
} else {
throw new \LogicException('You cannot use the "markdown_to_html" filter as no Markdown library is available; try running "composer require league/commonmark".');
}
+29
View File
@@ -0,0 +1,29 @@
<?php
/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Twig\Extra\Markdown;
use Tempest\Markdown\Markdown;
class TempestMarkdown implements MarkdownInterface
{
private $converter;
public function __construct(?Markdown $converter = null)
{
$this->converter = $converter ?: new Markdown();
}
public function convert(string $body): string
{
return $this->converter->parse($body)->html;
}
}
+14 -11
View File
@@ -20,6 +20,7 @@ use Twig\Extra\Markdown\MarkdownExtension;
use Twig\Extra\Markdown\MarkdownInterface;
use Twig\Extra\Markdown\MarkdownRuntime;
use Twig\Extra\Markdown\MichelfMarkdown;
use Twig\Extra\Markdown\TempestMarkdown;
use Twig\Loader\ArrayLoader;
use Twig\RuntimeLoader\RuntimeLoaderInterface;
@@ -30,12 +31,16 @@ class FunctionalTest extends TestCase
*/
public function testMarkdown(string $template, string $expected): void
{
foreach ([LeagueMarkdown::class, ErusevMarkdown::class, /* MichelfMarkdown::class, */ DefaultMarkdown::class] as $class) {
$classes = [LeagueMarkdown::class, ErusevMarkdown::class, /* MichelfMarkdown::class, */ DefaultMarkdown::class];
if (class_exists(\Tempest\Markdown\Markdown::class)) {
$classes[] = TempestMarkdown::class;
}
foreach ($classes as $class) {
$twig = new Environment(new ArrayLoader([
'index' => $template,
'html' => <<<EOF
Hello
=====
# Hello
Great!
EOF,
@@ -63,21 +68,19 @@ EOF,
return [
[<<<EOF
{% apply markdown_to_html %}
Hello
=====
# Hello
Great!
{% endapply %}
EOF, "<h1>Hello</h1>\n+<p>Great!</p>"],
EOF, "<h1[^>]*>Hello</h1>\n+<p>Great!\s*</p>"],
[<<<EOF
{% apply markdown_to_html %}
Hello
=====
# Hello
Great!
{% endapply %}
EOF, "<h1>Hello</h1>\n+<p>Great!</p>"],
["{{ include('html')|markdown_to_html }}", "<h1>Hello</h1>\n+<p>Great!</p>"],
EOF, "<h1[^>]*>Hello</h1>\n+<p>Great!\s*</p>"],
["{{ include('html')|markdown_to_html }}", "<h1[^>]*>Hello</h1>\n+<p>Great!\s*</p>"],
[<<<EOF
{% apply markdown_to_html %}
@@ -85,7 +88,7 @@ Paragraph 1
Paragraph 2
{% endapply %}
EOF, "<p>Paragraph 1</p>\n+<p>Paragraph 2</p>"],
EOF, "<p>Paragraph 1</p>\n+<p>Paragraph 2\s*</p>"],
];
}
+2 -1
View File
@@ -24,7 +24,8 @@
"erusev/parsedown": "dev-master as 1.x-dev",
"league/commonmark": "^2.7",
"league/html-to-markdown": "^4.8|^5.0",
"michelf/php-markdown": "^1.8|^2.0"
"michelf/php-markdown": "^1.8|^2.0",
"tempest/markdown": "^1.2"
},
"autoload": {
"files": [ "Resources/functions.php" ],