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

Cecilapp / Cecil / 15440770441

04 Jun 2025 11:11AM UTC coverage: 82.775% (-0.2%) from 82.968%
15440770441

Pull #2172

github

web-flow
Merge c88048483 into 33dbed85a
Pull Request #2172: Build Command : Allow to render a subset

23 of 39 new or added lines in 3 files covered. (58.97%)

173 existing lines in 4 files now uncovered.

3114 of 3762 relevant lines covered (82.78%)

0.83 hits per line

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

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

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <arnaud@ligny.fr>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
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
 * Pages rendering.
30
 */
31
class Render extends AbstractStep
32
{
33
    public const TMP_DIR = '.cecil';
34

35
    protected $subset = [];
36

37
    /**
38
     * {@inheritdoc}
39
     */
40
    public function getName(): string
41
    {
42
        return 'Rendering pages';
1✔
43
    }
44

45
    /**
46
     * {@inheritdoc}
47
     */
48
    public function init(array $options): void
49
    {
50
        if (!is_dir($this->config->getLayoutsPath()) && !$this->config->hasTheme()) {
1✔
51
            $message = \sprintf('"%s" is not a valid layouts directory', $this->config->getLayoutsPath());
×
52
            $this->builder->getLogger()->debug($message);
×
53
        }
54

55
        // render a subset of pages?
56
        if (!empty($options['render-subset'])) {
1✔
NEW
57
            $subset = \sprintf('pages.subsets.%s', (string) $options['render-subset']);
×
NEW
58
            if (!$this->config->has($subset)) {
×
NEW
59
                throw new ConfigException(\sprintf('Subset "%s" not found.', $subset));
×
60
            }
NEW
61
            $this->subset = (array) $this->config->get($subset);
×
62
        }
63

64
        $this->canProcess = true;
1✔
65
    }
66

67
    /**
68
     * {@inheritdoc}
69
     *
70
     * @throws RuntimeException
71
     */
72
    public function process(): void
73
    {
74
        // prepares renderer
75
        $this->builder->setRenderer(new Twig($this->builder, $this->getAllLayoutsPaths()));
1✔
76

77
        // adds global variables
78
        $this->addGlobals();
1✔
79

80
        $subset = $this->subset;
1✔
81

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

116
                return $page;
1✔
117
            });
1✔
118
        $total = \count($pages);
1✔
119

120
        // renders each page
121
        $count = 0;
1✔
122
        $postprocessors = [];
1✔
123
        foreach ((array) $this->config->get('output.postprocessors') as $name => $postprocessor) {
1✔
124
            try {
125
                if (!class_exists($postprocessor)) {
1✔
126
                    throw new RuntimeException(\sprintf('Class "%s" not found', $postprocessor));
1✔
127
                }
128
                $postprocessors[] = new $postprocessor($this->builder);
1✔
129
                $this->builder->getLogger()->debug(\sprintf('Output post processor "%s" loaded', $name));
1✔
130
            } catch (\Exception $e) {
1✔
131
                $this->builder->getLogger()->error(\sprintf('Unable to load output post processor "%s": %s', $name, $e->getMessage()));
1✔
132
            }
133
        }
134

135
        $cacheLocale = $cacheSite = $cacheConfig = [];
1✔
136

137
        /** @var Page $page */
138
        foreach ($pages as $page) {
1✔
139
            $count++;
1✔
140
            $rendered = [];
1✔
141

142
            // l10n
143
            $language = $page->getVariable('language', $this->config->getLanguageDefault());
1✔
144
            if (!isset($cacheLocale[$language])) {
1✔
145
                $cacheLocale[$language] = $this->config->getLanguageProperty('locale', $language);
1✔
146
            }
147
            $this->builder->getRenderer()->setLocale($cacheLocale[$language]);
1✔
148

149
            // global site variables
150
            if (!isset($cacheSite[$language])) {
1✔
151
                $cacheSite[$language] = new Site($this->builder, $language);
1✔
152
            }
153
            $this->builder->getRenderer()->addGlobal('site', $cacheSite[$language]);
1✔
154

155
            // global config raw variables
156
            if (!isset($cacheConfig[$language])) {
1✔
157
                $cacheConfig[$language] = new Config($this->builder, $language);
1✔
158
            }
159
            $this->builder->getRenderer()->addGlobal('config', $cacheConfig[$language]);
1✔
160

161
            // excluded format(s)?
162
            $formats = (array) $page->getVariable('output');
1✔
163
            foreach ($formats as $key => $format) {
1✔
164
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
1✔
165
                    // ie:
166
                    //   formats:
167
                    //     atom:
168
                    //       [...]
169
                    //       exclude: [paginated]
170
                    if (!\is_array($exclude)) {
1✔
171
                        $exclude = [$exclude];
×
172
                    }
173
                    foreach ($exclude as $variable) {
1✔
174
                        if ($page->hasVariable($variable)) {
1✔
175
                            unset($formats[$key]);
1✔
176
                        }
177
                    }
178
                }
179
            }
180

181
            // specific output format from subset
182
            if (!empty($this->subset['output'])) {
1✔
NEW
183
                $currentFormats = $formats;
×
NEW
184
                $formats = [];
×
NEW
185
                if (\in_array((string) $this->subset['output'], $currentFormats)) {
×
NEW
186
                    $formats = [(string) $this->subset['output']];
×
187
                }
188
            }
189

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

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

261
    /**
262
     * Returns an array of layouts directories.
263
     */
264
    protected function getAllLayoutsPaths(): array
265
    {
266
        $paths = [];
1✔
267

268
        // layouts/
269
        if (is_dir($this->config->getLayoutsPath())) {
1✔
270
            $paths[] = $this->config->getLayoutsPath();
1✔
271
        }
272
        // <theme>/layouts/
273
        if ($this->config->hasTheme()) {
1✔
274
            foreach ($this->config->getTheme() ?? [] as $theme) {
1✔
275
                $paths[] = $this->config->getThemeDirPath($theme);
1✔
276
            }
277
        }
278
        // resources/layouts/
279
        if (is_dir($this->config->getLayoutsInternalPath())) {
1✔
280
            $paths[] = $this->config->getLayoutsInternalPath();
1✔
281
        }
282

283
        return $paths;
1✔
284
    }
285

286
    /**
287
     * Adds global variables.
288
     */
289
    protected function addGlobals()
290
    {
291
        $this->builder->getRenderer()->addGlobal('cecil', [
1✔
292
            'url'       => \sprintf('https://cecil.app/#%s', Builder::getVersion()),
1✔
293
            'version'   => Builder::getVersion(),
1✔
294
            'poweredby' => \sprintf('Cecil v%s', Builder::getVersion()),
1✔
295
        ]);
1✔
296
    }
297

298
    /**
299
     * Get available output formats.
300
     *
301
     * @throws RuntimeException
302
     */
303
    protected function getOutputFormats(Page $page): array
304
    {
305
        // Get page output format(s) if defined.
306
        // ie:
307
        // ```yaml
308
        // output: txt
309
        // ```
310
        if ($page->getVariable('output')) {
1✔
311
            $formats = $page->getVariable('output');
1✔
312
            if (!\is_array($formats)) {
1✔
313
                $formats = [$formats];
1✔
314
            }
315

316
            return $formats;
1✔
317
        }
318

319
        // Get available output formats for the page type.
320
        // ie:
321
        // ```yaml
322
        // page: [html, json]
323
        // ```
324
        $formats = $this->config->get('output.pagetypeformats.' . $page->getType());
1✔
325
        if (empty($formats)) {
1✔
326
            throw new RuntimeException('Configuration key "pagetypeformats" can\'t be empty.');
×
327
        }
328
        if (!\is_array($formats)) {
1✔
329
            $formats = [$formats];
×
330
        }
331

332
        return array_unique($formats);
1✔
333
    }
334

335
    /**
336
     * Get alternates.
337
     */
338
    protected function getAlternates(array $formats): array
339
    {
340
        $alternates = [];
1✔
341

342
        if (\count($formats) > 1 || \in_array('html', $formats)) {
1✔
343
            foreach ($formats as $format) {
1✔
344
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
1✔
345
                $alternates[] = [
1✔
346
                    'rel'    => $rel,
1✔
347
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
1✔
348
                    'title'  => strtoupper($format),
1✔
349
                    'format' => $format,
1✔
350
                ];
1✔
351
            }
352
        }
353

354
        return $alternates;
1✔
355
    }
356

357
    /**
358
     * Returns the collection of translated pages for a given page.
359
     */
360
    protected function getTranslations(Page $refPage): Collection
361
    {
362
        $pages = $this->builder->getPages()->filter(function (Page $page) use ($refPage) {
1✔
363
            return $page->getId() !== $refPage->getId()
1✔
364
                && $page->getVariable('langref') == $refPage->getVariable('langref')
1✔
365
                && $page->getType() == $refPage->getType()
1✔
366
                && !empty($page->getVariable('published'))
1✔
367
                && !$page->getVariable('paginated');
1✔
368
        });
1✔
369

370
        return $pages;
1✔
371
    }
372
}
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