diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index af537adf1..659b46f91 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -26,7 +26,7 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
@@ -72,7 +72,7 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
@@ -90,6 +90,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
@@ -116,7 +121,7 @@ jobs:
steps:
- name: "Checkout code"
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: "Install PHP with extensions"
uses: shivammathur/setup-php@v2
diff --git a/.gitignore b/.gitignore
index 485ba481f..ce3a7a451 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
/phpunit.phar
/phpunit.xml
/vendor
+.php-cs-fixer.cache
.phpunit.cache
.phpunit.result.cache
diff --git a/doc/filters/markdown_to_html.rst b/doc/filters/markdown_to_html.rst
index 780f0112b..4218ed37c 100644
--- a/doc/filters/markdown_to_html.rst
+++ b/doc/filters/markdown_to_html.rst
@@ -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:
diff --git a/doc/functions/include.rst b/doc/functions/include.rst
index 4e51ae120..ac31b21bb 100644
--- a/doc/functions/include.rst
+++ b/doc/functions/include.rst
@@ -8,29 +8,25 @@ The ``include`` function returns the rendered content of a template:
{{ include('template.html.twig') }}
{{ include(some_var) }}
-The returned content is a ``\Twig\Markup`` instance, so it is considered safe
-and is not escaped again when you store it in a variable and print it later:
-
-.. code-block:: twig
-
- {% set body = include('body.html.twig') %}
- {{ body }} {# rendered as-is, not re-escaped #}
-
-Beware that, like any safe value, it is not re-escaped for the context it ends
-up in, so only embed it in the same context it was rendered for (typically
-HTML).
-
Included templates have access to the variables of the active context.
-If you are using the filesystem loader, the templates are looked for in the
-paths defined by it.
+.. tip::
-The context is passed by default to the template but you can also pass
+ Prefer the :doc:`include_only() function ` when
+ you can. Sharing the whole context lets a template silently rely on
+ variables defined by the caller, which hides its real inputs and couples it
+ to wherever it is included from. ``include_only`` takes only the variables
+ you pass, making the data flow explicit and partials easier to reuse.
+
+Its documentation also covers the template loading, ``ignore_missing`` and
+return-value behavior shared by both functions.
+
+The current context is passed by default to the template but you can also pass
additional variables:
.. code-block:: twig
- {# template.html.twig will have access to the variables from the current context and the additional ones provided #}
+ {# The included template can access "name" and the current context. #}
{{ include('template.html.twig', {name: 'Fabien'}) }}
You can disable access to the context by setting ``with_context`` to
@@ -38,40 +34,9 @@ You can disable access to the context by setting ``with_context`` to
.. code-block:: twig
- {# only the name variable will be accessible #}
+ {# Only the "name" variable will be accessible. #}
{{ include('template.html.twig', {name: 'Fabien'}, with_context: false) }}
-.. code-block:: twig
-
- {# no variables will be accessible #}
- {{ include('template.html.twig', with_context: false) }}
-
-And if the expression evaluates to a ``\Twig\Template`` or a
-``\Twig\TemplateWrapper`` instance, Twig will use it directly::
-
- // {{ include(template) }}
-
- $template = $twig->load('some_template.html.twig');
-
- $twig->display('template.html.twig', ['template' => $template]);
-
-When you set the ``ignore_missing`` flag, Twig will return an empty string if
-the template does not exist:
-
-.. code-block:: twig
-
- {{ include('sidebar.html.twig', ignore_missing: true) }}
-
-You can also provide a list of templates that are checked for existence before
-inclusion. The first template that exists will be rendered:
-
-.. code-block:: twig
-
- {{ include(['page_detailed.html.twig', 'page.html.twig']) }}
-
-If ``ignore_missing`` is set, it will fall back to rendering nothing if none
-of the templates exist, otherwise it will throw an exception.
-
When including a template created by an end user, you should
:doc:`sandbox<../sandbox>` it: render the untrusted template with the
``Twig\Sandbox\Sandbox`` class from PHP or the
diff --git a/doc/functions/include_only.rst b/doc/functions/include_only.rst
new file mode 100644
index 000000000..be3ffe9a6
--- /dev/null
+++ b/doc/functions/include_only.rst
@@ -0,0 +1,97 @@
+``include_only``
+================
+
+.. versionadded:: 3.29
+
+ The ``include_only`` function was added in Twig 3.29.
+
+The ``include_only`` function returns the rendered content of a template
+without giving it access to the current context:
+
+.. code-block:: twig
+
+ {{ include_only('template.html.twig') }}
+ {{ include_only(some_var) }}
+
+Variables from the active context are not passed implicitly. This makes the
+data a template relies on explicit, which is often clearer and easier to
+reason about.
+
+Returned Value
+--------------
+
+The returned content is a ``\Twig\Markup`` instance, so it is considered safe
+and is not escaped again when you store it in a variable and print it later:
+
+.. code-block:: twig
+
+ {% set body = include_only('body.html.twig') %}
+ {{ body }} {# rendered as-is, not re-escaped #}
+
+Beware that, like any safe value, it is not re-escaped for the context it ends
+up in, so only embed it in the same context it was rendered for (typically
+HTML).
+
+Passing Variables
+-----------------
+
+As the context is not passed, variables a template needs must be passed
+explicitly:
+
+.. code-block:: twig
+
+ {# template.html.twig will only have access to the "name" variable #}
+ {{ include_only('template.html.twig', {name: 'Fabien'}) }}
+
+When passing a variable from the current context, you can use the following
+shortcut:
+
+.. code-block:: twig
+
+ {{ include_only('template.html.twig', {name, email}) }}
+
+ {# is equivalent to #}
+
+ {{ include_only('template.html.twig', {name: name, email: email}) }}
+
+Loading Templates
+-----------------
+
+If you are using the filesystem loader, the templates are looked for in the
+paths defined by it.
+
+If the expression evaluates to a ``\Twig\TemplateWrapper`` instance, Twig
+will use it directly::
+
+ // {{ include_only(template) }}
+
+ $template = $twig->load('some_template.html.twig');
+
+ $twig->display('template.html.twig', ['template' => $template]);
+
+When you set the ``ignore_missing`` flag, Twig will return an empty string if
+the template does not exist:
+
+.. code-block:: twig
+
+ {{ include_only('sidebar.html.twig', ignore_missing: true) }}
+
+You can also provide a list of templates that are checked for existence before
+inclusion. The first template that exists will be rendered:
+
+.. code-block:: twig
+
+ {{ include_only(['page_detailed.html.twig', 'page.html.twig']) }}
+
+If ``ignore_missing`` is set, it will fall back to rendering nothing if none
+of the templates exist, otherwise it will throw an exception.
+
+To render a template created by an end user, use the
+:doc:`render_sandboxed() function `.
+
+Arguments
+---------
+
+* ``template``: The template to render
+* ``variables``: The variables to pass to the template
+* ``ignore_missing``: Whether to ignore missing templates or not
diff --git a/doc/functions/index.rst b/doc/functions/index.rst
index c5ca047e5..1d1d815b2 100644
--- a/doc/functions/index.rst
+++ b/doc/functions/index.rst
@@ -16,6 +16,7 @@ Functions
html_classes
html_cva
include
+ include_only
max
min
parent
diff --git a/extra/markdown-extra/DefaultMarkdown.php b/extra/markdown-extra/DefaultMarkdown.php
index a20993d45..389022082 100644
--- a/extra/markdown-extra/DefaultMarkdown.php
+++ b/extra/markdown-extra/DefaultMarkdown.php
@@ -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".');
}
diff --git a/extra/markdown-extra/TempestMarkdown.php b/extra/markdown-extra/TempestMarkdown.php
new file mode 100644
index 000000000..1e037ac99
--- /dev/null
+++ b/extra/markdown-extra/TempestMarkdown.php
@@ -0,0 +1,29 @@
+converter = $converter ?: new Markdown();
+ }
+
+ public function convert(string $body): string
+ {
+ return $this->converter->parse($body)->html;
+ }
+}
diff --git a/extra/markdown-extra/Tests/FunctionalTest.php b/extra/markdown-extra/Tests/FunctionalTest.php
index b5e58d30b..77741db41 100644
--- a/extra/markdown-extra/Tests/FunctionalTest.php
+++ b/extra/markdown-extra/Tests/FunctionalTest.php
@@ -21,6 +21,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;
@@ -29,12 +30,16 @@ class FunctionalTest extends TestCase
#[DataProvider('getMarkdownTests')]
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' => << Great! Great!\s* Great! Great! Great!\s* Great!\s*]*>Hello
\n+Hello
\n+]*>Hello
\n+]*>Hello
\n+
Paragraph 2
"], +EOF, "Paragraph 1
\n+Paragraph 2\s*
"], ]; } diff --git a/extra/markdown-extra/composer.json b/extra/markdown-extra/composer.json index 7967a0f77..edbb05b45 100644 --- a/extra/markdown-extra/composer.json +++ b/extra/markdown-extra/composer.json @@ -24,7 +24,8 @@ "league/commonmark": "^2.7", "league/html-to-markdown": "^4.8|^5.0", "michelf/php-markdown": "^1.8|^2.0", - "symfony/phpunit-bridge": "^6.4|^7.0" + "symfony/phpunit-bridge": "^6.4|^7.0", + "tempest/markdown": "^1.2" }, "autoload": { "psr-4" : { "Twig\\Extra\\Markdown\\" : "" }, diff --git a/src/Extension/CoreExtension.php b/src/Extension/CoreExtension.php index a64af72d7..b0bbf1534 100644 --- a/src/Extension/CoreExtension.php +++ b/src/Extension/CoreExtension.php @@ -304,6 +304,7 @@ final class CoreExtension extends AbstractExtension new TwigFunction('random', self::random(...), ['needs_charset' => true]), new TwigFunction('date', $this->convertDate(...)), new TwigFunction('include', self::include(...), ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), + new TwigFunction('include_only', self::includeOnly(...), ['needs_environment' => true, 'is_safe' => ['all']]), new TwigFunction('source', self::source(...), ['needs_environment' => true, 'is_safe' => ['all']]), new TwigFunction('enum_cases', self::enumCases(...), ['node_class' => EnumCasesFunction::class]), new TwigFunction('enum', self::enum(...), ['node_class' => EnumFunction::class]), @@ -1450,6 +1451,22 @@ final class CoreExtension extends AbstractExtension return '' === $rendered ? '' : new Markup($rendered, $env->getCharset()); } + /** + * Renders a template without giving it access to the current context. + * + * @param string|array