Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
55.36% covered (warning)
55.36%
31 / 56
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
TemplateParser
55.36% covered (warning)
55.36%
31 / 56
66.67% covered (warning)
66.67%
2 / 3
18.90
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 compile
41.86% covered (danger)
41.86%
18 / 43
0.00% covered (danger)
0.00%
0 / 1
20.58
 processTemplate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare( strict_types = 1 );
3
4/**
5 * TemplateParser.php
6 *
7 * This file is part of the Codex design system, the official design system
8 * for Wikimedia projects. It provides the `TemplateParser` class, which is responsible
9 * for compiling and rendering Mustache templates with localization and helper support.
10 *
11 * The TemplateParser centralizes template compilation and rendering logic,
12 * enhancing reusability and maintainability.
13 *
14 * @category Parser
15 * @package  Codex\Parser
16 * @since    0.3.0
17 * @author   Doğu Abaris <abaris@null.net>
18 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
19 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
20 */
21
22namespace Wikimedia\Codex\Parser;
23
24use LightnCandy\Flags;
25use LightnCandy\LightnCandy;
26use RuntimeException;
27
28/**
29 * TemplateParser is responsible for compiling and rendering Mustache templates.
30 *
31 * This class provides methods to compile Mustache templates into PHP rendering functions.
32 *
33 * @category Parser
34 * @package  Codex\Parser
35 * @since    0.3.0
36 * @author   Doğu Abaris <abaris@null.net>
37 * @license  https://www.gnu.org/copyleft/gpl.html GPL-2.0-or-later
38 * @link     https://doc.wikimedia.org/codex/main/ Codex Documentation
39 */
40class TemplateParser {
41
42    /**
43     * Path to the Mustache templates directory.
44     */
45    private string $templateDir;
46
47    /**
48     * @var array<string,callable> Array of cached rendering functions.
49     */
50    private array $renderers = [];
51
52    /**
53     * Compilation flags for LightnCandy.
54     */
55    private int $compileFlags;
56
57    /**
58     * Constructor to initialize the TemplateParser.
59     *
60     * @since 0.3.0
61     *
62     * @param string $templateDir Path to the template directory.
63     */
64    public function __construct( string $templateDir ) {
65        $this->templateDir = $templateDir;
66
67        $this->compileFlags =
68            Flags::FLAG_ERROR_EXCEPTION |
69            Flags::FLAG_HANDLEBARS |
70            Flags::FLAG_ADVARNAME |
71            Flags::FLAG_RUNTIMEPARTIAL |
72            Flags::FLAG_RENDER_DEBUG |
73            Flags::FLAG_MUSTACHE |
74            Flags::FLAG_ERROR_EXCEPTION |
75            Flags::FLAG_NOHBHELPERS |
76            Flags::FLAG_MUSTACHELOOKUP;
77    }
78
79    /**
80     * Compiles the Mustache template into a PHP rendering function.
81     *
82     * @since 0.3.0
83     *
84     * @param string $templateName Name of the template file (without extension).
85     *
86     * @return callable Render function for the template.
87     * @throws RuntimeException If the template file cannot be found or compilation fails.
88     * @suppress PhanTypeMismatchArgument
89     */
90    public function compile( string $templateName ): callable {
91        if ( isset( $this->renderers[$templateName] ) ) {
92            return $this->renderers[$templateName];
93        }
94
95        $templatePath = "$this->templateDir/$templateName.mustache";
96
97        if ( !file_exists( $templatePath ) ) {
98            throw new RuntimeException( "Template file not found: $templatePath" );
99        }
100
101        $templateContent = file_get_contents( $templatePath );
102
103        if ( $templateContent === false ) {
104            throw new RuntimeException( "Unable to read template file: $templatePath" );
105        }
106
107        $phpCode = LightnCandy::compile( $templateContent, [
108            'flags' => $this->compileFlags,
109            'basedir' => $this->templateDir,
110            'fileext' => '.mustache',
111            'partialresolver' => function ( $cx, $partialName ) use ( $templateName ) {
112                $filename = "$this->templateDir/$partialName.mustache";
113                if ( !file_exists( $filename ) ) {
114                    throw new RuntimeException(
115                        sprintf(
116                            'Could not compile template `%s`: Could not find partial `%s` at %s',
117                            $templateName,
118                            $partialName,
119                            $filename
120                        )
121                    );
122                }
123
124                $fileContents = file_get_contents( $filename );
125
126                if ( $fileContents === false ) {
127                    throw new RuntimeException(
128                        sprintf(
129                            'Could not compile template `%s`: Could not find partial `%s` at %s',
130                            $templateName,
131                            $partialName,
132                            $filename
133                        )
134                    );
135                }
136
137                return $fileContents;
138            },
139        ] );
140
141        if ( !$phpCode ) {
142            throw new RuntimeException( "Failed to compile template: $templateName" );
143        }
144
145        // phpcs:ignore MediaWiki.Usage.ForbiddenFunctions.eval
146        $renderFunction = eval( $phpCode );
147        if ( !is_callable( $renderFunction ) ) {
148            throw new RuntimeException( "Compiled template is not callable: $templateName" );
149        }
150
151        $this->renderers[$templateName] = $renderFunction;
152
153        return $renderFunction;
154    }
155
156    /**
157     * Processes the template with provided data and returns the rendered HTML.
158     *
159     * @since 0.3.0
160     *
161     * @param string $templateName Name of the template file (without extension).
162     * @param array $data Data to render within the template.
163     *
164     * @return string Rendered HTML.
165     * @throws RuntimeException If rendering fails.
166     */
167    public function processTemplate( string $templateName, array $data ): string {
168        $renderFunction = $this->compile( $templateName );
169
170        return $renderFunction( $data );
171    }
172}