• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Cecilapp / Cecil / 21676346600

04 Feb 2026 02:57PM UTC coverage: 82.378% (-0.2%) from 82.577%
21676346600

push

github

ArnaudLigny
Persist assets to temp file in debug mode

When running in debug mode, append asset paths to a temporary file (assets-<buildId>.txt) under the configured destination/TMP_DIR in addToAssetsList, and read that file in getAssetsList. If the file read fails, getAssetsList returns an empty array. Non-debug behavior (using the in-memory $assets array) is unchanged. This ensures the assets list is persisted to disk for debug builds.

14 of 15 new or added lines in 1 file covered. (93.33%)

40 existing lines in 4 files now uncovered.

3319 of 4029 relevant lines covered (82.38%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.95
/src/Step/Pages/Render.php
1
<?php
2

3
/**
4
 * This file is part of Cecil.
5
 *
6
 * (c) Arnaud Ligny <arnaud@ligny.fr>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace Cecil\Step\Pages;
15

16
use Cecil\Builder;
17
use Cecil\Collection\Page\Collection;
18
use Cecil\Collection\Page\Page;
19
use Cecil\Exception\ConfigException;
20
use Cecil\Exception\RuntimeException;
21
use Cecil\Renderer\Config;
22
use Cecil\Renderer\Layout;
23
use Cecil\Renderer\Site;
24
use Cecil\Renderer\Twig;
25
use Cecil\Step\AbstractStep;
26
use Cecil\Util;
27

28
/**
29
 * Render step.
30
 *
31
 * This step is responsible for rendering pages using Twig templates.
32
 * It processes each page, applies the appropriate templates, and generates
33
 * the final output formats. It also handles subsets of pages if specified,
34
 * and adds global variables to the renderer. The rendered pages are then
35
 * stored in the builder's pages collection for further processing or output.
36
 */
37
class Render extends AbstractStep
38
{
39
    /**
40
     * Subset of pages to render.
41
     *
42
     * @var array
43
     */
44
    protected $subset = [];
45

46
    /**
47
     * {@inheritdoc}
48
     */
49
    public function getName(): string
50
    {
51
        return 'Rendering pages';
1✔
52
    }
53

54
    /**
55
     * {@inheritdoc}
56
     */
57
    public function init(array $options): void
58
    {
59
        if (!is_dir($this->config->getLayoutsPath()) && !$this->config->hasTheme()) {
1✔
UNCOV
60
            $message = \sprintf('"%s" is not a valid layouts directory', $this->config->getLayoutsPath());
×
UNCOV
61
            $this->builder->getLogger()->debug($message);
×
62
        }
63

64
        // render a subset of pages?
65
        if (!empty($options['render-subset'])) {
1✔
66
            $subset = \sprintf('pages.subsets.%s', (string) $options['render-subset']);
×
UNCOV
67
            if (!$this->config->has($subset)) {
×
68
                throw new ConfigException(\sprintf('Subset "%s" not found.', $subset));
×
69
            }
UNCOV
70
            $this->subset = (array) $this->config->get($subset);
×
71
        }
72

73
        $this->canProcess = true;
1✔
74
    }
75

76
    /**
77
     * {@inheritdoc}
78
     *
79
     * @throws RuntimeException
80
     */
81
    public function process(): void
82
    {
83
        // prepares renderer
84
        $this->builder->setRenderer(new Twig($this->builder, $this->getAllLayoutsPaths()));
1✔
85

86
        // adds global variables
87
        $this->addGlobals();
1✔
88

89
        $subset = $this->subset;
1✔
90

91
        /** @var Collection $pages */
92
        $pages = $this->builder->getPages()
1✔
93
            // published only
1✔
94
            ->filter(function (Page $page) {
1✔
95
                return (bool) $page->getVariable('published');
1✔
96
            })
1✔
97
            ->filter(function (Page $page) use ($subset) {
1✔
98
                if (empty($subset)) {
1✔
99
                    return true;
1✔
100
                }
101
                if (
UNCOV
102
                    !empty($subset['path'])
×
103
                    && !((bool) preg_match('/' . (string) $subset['path'] . '/i', $page->getPath()))
×
104
                ) {
105
                    return false;
×
106
                }
107
                if (!empty($subset['language'])) {
×
108
                    $language = $page->getVariable('language', $this->config->getLanguageDefault());
×
UNCOV
109
                    if ($language !== (string) $subset['language']) {
×
UNCOV
110
                        return false;
×
111
                    }
112
                }
UNCOV
113
                return true;
×
114
            })
1✔
115
            // enrichs some variables
1✔
116
            ->map(function (Page $page) {
1✔
117
                $formats = $this->getOutputFormats($page);
1✔
118
                // output formats
119
                $page->setVariable('output', $formats);
1✔
120
                // alternates formats
121
                $page->setVariable('alternates', $this->getAlternates($formats));
1✔
122
                // translations
123
                $page->setVariable('translations', $this->getTranslations($page));
1✔
124

125
                return $page;
1✔
126
            });
1✔
127
        $total = \count($pages);
1✔
128

129
        // renders each page
130
        $count = 0;
1✔
131
        $postprocessors = [];
1✔
132
        foreach ((array) $this->config->get('output.postprocessors') as $name => $postprocessor) {
1✔
133
            try {
134
                if (!class_exists($postprocessor)) {
1✔
135
                    throw new RuntimeException(\sprintf('Class "%s" not found', $postprocessor));
1✔
136
                }
137
                $postprocessors[] = new $postprocessor($this->builder);
1✔
138
                $this->builder->getLogger()->debug(\sprintf('Output post processor "%s" loaded', $name));
1✔
139
            } catch (\Exception $e) {
1✔
140
                $this->builder->getLogger()->error(\sprintf('Unable to load output post processor "%s": %s', $name, $e->getMessage()));
1✔
141
            }
142
        }
143

144
        // some cache to avoid multiple calls
145
        $cache = [];
1✔
146

147
        /** @var Page $page */
148
        foreach ($pages as $page) {
1✔
149
            $count++;
1✔
150
            $rendered = [];
1✔
151

152
            // l10n
153
            $language = $page->getVariable('language', $this->config->getLanguageDefault());
1✔
154
            if (!isset($cache['locale'][$language])) {
1✔
155
                $cache['locale'][$language] = $this->config->getLanguageProperty('locale', $language);
1✔
156
            }
157
            $this->builder->getRenderer()->setLocale($cache['locale'][$language]);
1✔
158

159
            // global site variables
160
            if (!isset($cache['site'][$language])) {
1✔
161
                $cache['site'][$language] = new Site($this->builder, $language);
1✔
162
            }
163
            $this->builder->getRenderer()->addGlobal('site', $cache['site'][$language]);
1✔
164

165
            // global config raw variables
166
            if (!isset($cache['config'][$language])) {
1✔
167
                $cache['config'][$language] = new Config($this->builder, $language);
1✔
168
            }
169
            $this->builder->getRenderer()->addGlobal('config', $cache['config'][$language]);
1✔
170

171
            // excluded format(s)?
172
            $formats = (array) $page->getVariable('output');
1✔
173
            foreach ($formats as $key => $format) {
1✔
174
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
1✔
175
                    // ie:
176
                    //   formats:
177
                    //     atom:
178
                    //       [...]
179
                    //       exclude: [paginated]
180
                    if (!\is_array($exclude)) {
1✔
UNCOV
181
                        $exclude = [$exclude];
×
182
                    }
183
                    foreach ($exclude as $variable) {
1✔
184
                        if ($page->hasVariable($variable)) {
1✔
185
                            unset($formats[$key]);
1✔
186
                        }
187
                    }
188
                }
189
            }
190

191
            // specific output format from subset
192
            if (!empty($this->subset['output'])) {
1✔
193
                $currentFormats = $formats;
×
194
                $formats = [];
×
UNCOV
195
                if (\in_array((string) $this->subset['output'], $currentFormats)) {
×
UNCOV
196
                    $formats = [(string) $this->subset['output']];
×
197
                }
198
            }
199

200
            // renders each output format
201
            foreach ($formats as $format) {
1✔
202
                // search for the template
203
                $layout = Layout::finder($page, $format, $this->config);
1✔
204
                // renders with Twig
205
                try {
206
                    $deprecations = [];
1✔
207
                    set_error_handler(function ($type, $msg) use (&$deprecations) {
1✔
208
                        if (E_USER_DEPRECATED === $type) {
1✔
209
                            $deprecations[] = $msg;
1✔
210
                        }
211
                    });
1✔
212
                    $output = $this->builder->getRenderer()->render($layout['file'], ['page' => $page]);
1✔
213
                    foreach ($deprecations as $value) {
1✔
214
                        $this->builder->getLogger()->warning($value);
1✔
215
                    }
216
                    foreach ($postprocessors as $postprocessor) {
1✔
217
                        $output = $postprocessor->process($page, $output, $format);
1✔
218
                    }
219
                    $rendered[$format] = [
1✔
220
                        'output'   => $output,
1✔
221
                        'template' => [
1✔
222
                            'scope' => $layout['scope'],
1✔
223
                            'file'  => $layout['file'],
1✔
224
                        ],
1✔
225
                    ];
1✔
226
                    $page->addRendered($rendered);
1✔
227
                } catch (\Twig\Error\Error $e) {
×
228
                    throw new RuntimeException(
×
229
                        \sprintf(
×
230
                            'Unable to render template "%s" for page "%s".',
×
231
                            $e->getSourceContext()->getName(),
×
232
                            $page->getFileName() ?? $page->getId()
×
233
                        ),
×
234
                        previous: $e,
×
235
                        file: $e->getSourceContext()->getPath(),
×
236
                        line: $e->getTemplateLine(),
×
237
                    );
×
UNCOV
238
                } catch (\Exception $e) {
×
UNCOV
239
                    throw new RuntimeException($e->getMessage(), previous: $e);
×
240
                }
241
            }
242
            $this->builder->getPages()->replace($page->getId(), $page);
1✔
243

244
            $templates = array_column($rendered, 'template');
1✔
245
            $message = \sprintf(
1✔
246
                'Page "%s" rendered with [%s]',
1✔
247
                $page->getId() ?: 'index',
1✔
248
                Util\Str::combineArrayToString($templates, 'scope', 'file')
1✔
249
            );
1✔
250
            $this->builder->getLogger()->info($message, ['progress' => [$count, $total]]);
1✔
251
        }
252
        // profiler
253
        if ($this->builder->isDebug()) {
1✔
254
            try {
255
                // HTML
256
                $htmlDumper = new \Twig\Profiler\Dumper\HtmlDumper();
1✔
257
                $profileHtmlFile = Util::joinFile($this->config->getDestinationDir(), Builder::TMP_DIR, 'twig_profile.html');
1✔
258
                Util\File::getFS()->dumpFile($profileHtmlFile, $htmlDumper->dump($this->builder->getRenderer()->getDebugProfile()));
1✔
259
                // TXT
260
                $textDumper = new \Twig\Profiler\Dumper\TextDumper();
1✔
261
                $profileTextFile = Util::joinFile($this->config->getDestinationDir(), Builder::TMP_DIR, 'twig_profile.txt');
1✔
262
                Util\File::getFS()->dumpFile($profileTextFile, $textDumper->dump($this->builder->getRenderer()->getDebugProfile()));
1✔
263
                // log
264
                $this->builder->getLogger()->debug(\sprintf('Twig profile dumped in "%s"', Util::joinFile($this->config->getDestinationDir(), Builder::TMP_DIR)));
1✔
UNCOV
265
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
×
UNCOV
266
                throw new RuntimeException($e->getMessage());
×
267
            }
268
        }
269
    }
270

271
    /**
272
     * Returns an array of layouts directories.
273
     */
274
    protected function getAllLayoutsPaths(): array
275
    {
276
        $paths = [];
1✔
277

278
        // layouts/
279
        if (is_dir($this->config->getLayoutsPath())) {
1✔
280
            $paths[] = $this->config->getLayoutsPath();
1✔
281
        }
282
        // <theme>/layouts/
283
        if ($this->config->hasTheme()) {
1✔
284
            foreach ($this->config->getTheme() ?? [] as $theme) {
1✔
285
                $paths[] = $this->config->getThemeDirPath($theme);
1✔
286
            }
287
        }
288
        // resources/layouts/
289
        if (is_dir($this->config->getLayoutsInternalPath())) {
1✔
290
            $paths[] = $this->config->getLayoutsInternalPath();
1✔
291
        }
292

293
        return $paths;
1✔
294
    }
295

296
    /**
297
     * Adds global variables.
298
     */
299
    protected function addGlobals()
300
    {
301
        $this->builder->getRenderer()->addGlobal('cecil', [
1✔
302
            'url'       => \sprintf('https://cecil.app/#%s', Builder::getVersion()),
1✔
303
            'version'   => Builder::getVersion(),
1✔
304
            'poweredby' => \sprintf('Cecil v%s', Builder::getVersion()),
1✔
305
        ]);
1✔
306
    }
307

308
    /**
309
     * Get available output formats.
310
     *
311
     * @throws RuntimeException
312
     */
313
    protected function getOutputFormats(Page $page): array
314
    {
315
        // Get page output format(s) if defined.
316
        // ie:
317
        // ```yaml
318
        // output: txt
319
        // ```
320
        if ($page->getVariable('output')) {
1✔
321
            $formats = $page->getVariable('output');
1✔
322
            if (!\is_array($formats)) {
1✔
323
                $formats = [$formats];
1✔
324
            }
325

326
            return $formats;
1✔
327
        }
328

329
        // Get available output formats for the page type.
330
        // ie:
331
        // ```yaml
332
        // page: [html, json]
333
        // ```
334
        $formats = $this->config->get('output.pagetypeformats.' . $page->getType());
1✔
335
        if (empty($formats)) {
1✔
UNCOV
336
            throw new RuntimeException('Configuration key "pagetypeformats" can\'t be empty.');
×
337
        }
338
        if (!\is_array($formats)) {
1✔
UNCOV
339
            $formats = [$formats];
×
340
        }
341

342
        return array_unique($formats);
1✔
343
    }
344

345
    /**
346
     * Get alternates.
347
     */
348
    protected function getAlternates(array $formats): array
349
    {
350
        $alternates = [];
1✔
351

352
        if (\count($formats) > 1 || \in_array('html', $formats)) {
1✔
353
            foreach ($formats as $format) {
1✔
354
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
1✔
355
                $alternates[] = [
1✔
356
                    'rel'    => $rel,
1✔
357
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
1✔
358
                    'title'  => strtoupper($format),
1✔
359
                    'format' => $format,
1✔
360
                ];
1✔
361
            }
362
        }
363

364
        return $alternates;
1✔
365
    }
366

367
    /**
368
     * Returns the collection of translated pages for a given page.
369
     */
370
    protected function getTranslations(Page $refPage): Collection
371
    {
372
        $pages = $this->builder->getPages()->filter(function (Page $page) use ($refPage) {
1✔
373
            return $page->getVariable('langref') == $refPage->getVariable('langref')
1✔
374
                && $page->getType() == $refPage->getType()
1✔
375
                && $page->getId() !== $refPage->getId()
1✔
376
                && !empty($page->getVariable('published'))
1✔
377
                && !$page->getVariable('paginated')
1✔
378
            ;
1✔
379
        });
1✔
380

381
        return $pages;
1✔
382
    }
383
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc