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

Cecilapp / Cecil / 15251788562

26 May 2025 10:35AM UTC coverage: 82.951%. First build
15251788562

Pull #2172

github

web-flow
Merge 71c3f1f15 into 156ac27a0
Pull Request #2172: Build Command : Allow to render a specific path

13 of 15 new or added lines in 3 files covered. (86.67%)

3075 of 3707 relevant lines covered (82.95%)

0.83 hits per line

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

85.23
/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\RuntimeException;
20
use Cecil\Renderer\Config;
21
use Cecil\Renderer\Layout;
22
use Cecil\Renderer\Site;
23
use Cecil\Renderer\Twig;
24
use Cecil\Step\AbstractStep;
25
use Cecil\Util;
26

27
/**
28
 * Pages rendering.
29
 */
30
class Render extends AbstractStep
31
{
32
    public const TMP_DIR = '.cecil';
33

34
    protected $renderOnlyPathRegex = '';
35

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

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

54
        if (!empty($options['render-only-path'])) {
1✔
NEW
55
            $this->renderOnlyPathRegex = $options['render-only-path'];
×
56
        }
57

58
        $this->canProcess = true;
1✔
59
    }
60

61
    /**
62
     * {@inheritdoc}
63
     *
64
     * @throws RuntimeException
65
     */
66
    public function process(): void
67
    {
68
        // prepares renderer
69
        $this->builder->setRenderer(new Twig($this->builder, $this->getAllLayoutsPaths()));
1✔
70

71
        // adds global variables
72
        $this->addGlobals();
1✔
73

74
        $renderOnlyPathRegex = $this->renderOnlyPathRegex;
1✔
75

76
        /** @var Collection $pages */
77
        $pages = $this->builder->getPages()
1✔
78
            // published only
1✔
79
            ->filter(function (Page $page) {
1✔
80
                return (bool) $page->getVariable('published');
1✔
81
            })
1✔
82
            ->filter(function (Page $page) use ($renderOnlyPathRegex) {
1✔
83
                if (empty($renderOnlyPathRegex)) {
1✔
84
                    return true;
1✔
85
                }
NEW
86
                return (bool) preg_match('/' . $renderOnlyPathRegex . '/', $page->getPath());
×
87
            })
1✔
88
            // enrichs some variables
1✔
89
            ->map(function (Page $page) {
1✔
90
                $formats = $this->getOutputFormats($page);
1✔
91
                // output formats
92
                $page->setVariable('output', $formats);
1✔
93
                // alternates formats
94
                $page->setVariable('alternates', $this->getAlternates($formats));
1✔
95
                // translations
96
                $page->setVariable('translations', $this->getTranslations($page));
1✔
97

98
                return $page;
1✔
99
            });
1✔
100
        $total = \count($pages);
1✔
101

102
        // renders each page
103
        $count = 0;
1✔
104
        $postprocessors = [];
1✔
105
        foreach ((array) $this->config->get('output.postprocessors') as $name => $postprocessor) {
1✔
106
            try {
107
                if (!class_exists($postprocessor)) {
1✔
108
                    throw new RuntimeException(\sprintf('Class "%s" not found', $postprocessor));
1✔
109
                }
110
                $postprocessors[] = new $postprocessor($this->builder);
1✔
111
                $this->builder->getLogger()->debug(\sprintf('Output post processor "%s" loaded', $name));
1✔
112
            } catch (\Exception $e) {
1✔
113
                $this->builder->getLogger()->error(\sprintf('Unable to load output post processor "%s": %s', $name, $e->getMessage()));
1✔
114
            }
115
        }
116
        /** @var Page $page */
117
        foreach ($pages as $page) {
1✔
118
            $count++;
1✔
119
            $rendered = [];
1✔
120

121
            // l10n
122
            $language = $page->getVariable('language', $this->config->getLanguageDefault());
1✔
123
            $locale = $this->config->getLanguageProperty('locale', $language);
1✔
124
            $this->builder->getRenderer()->setLocale($locale);
1✔
125

126
            // global site variables
127
            $this->builder->getRenderer()->addGlobal('site', new Site($this->builder, $language));
1✔
128

129
            // global config raw variables
130
            $this->builder->getRenderer()->addGlobal('config', new Config($this->builder, $language));
1✔
131

132
            // excluded format(s)?
133
            $formats = (array) $page->getVariable('output');
1✔
134
            foreach ($formats as $key => $format) {
1✔
135
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
1✔
136
                    // ie:
137
                    //   formats:
138
                    //     atom:
139
                    //       [...]
140
                    //       exclude: [paginated]
141
                    if (!\is_array($exclude)) {
1✔
142
                        $exclude = [$exclude];
×
143
                    }
144
                    foreach ($exclude as $variable) {
1✔
145
                        if ($page->hasVariable($variable)) {
1✔
146
                            unset($formats[$key]);
1✔
147
                        }
148
                    }
149
                }
150
            }
151

152
            // renders each output format
153
            foreach ($formats as $format) {
1✔
154
                // search for the template
155
                $layout = Layout::finder($page, $format, $this->config);
1✔
156
                // renders with Twig
157
                try {
158
                    $deprecations = [];
1✔
159
                    set_error_handler(function ($type, $msg) use (&$deprecations) {
1✔
160
                        if (E_USER_DEPRECATED === $type) {
1✔
161
                            $deprecations[] = $msg;
1✔
162
                        }
163
                    });
1✔
164
                    $output = $this->builder->getRenderer()->render($layout['file'], ['page' => $page]);
1✔
165
                    foreach ($deprecations as $value) {
1✔
166
                        $this->builder->getLogger()->warning($value);
1✔
167
                    }
168
                    foreach ($postprocessors as $postprocessor) {
1✔
169
                        $output = $postprocessor->process($page, $output, $format);
1✔
170
                    }
171
                    $rendered[$format] = [
1✔
172
                        'output'   => $output,
1✔
173
                        'template' => [
1✔
174
                            'scope' => $layout['scope'],
1✔
175
                            'file'  => $layout['file'],
1✔
176
                        ],
1✔
177
                    ];
1✔
178
                    $page->addRendered($rendered);
1✔
179
                } catch (\Twig\Error\Error $e) {
×
180
                    throw new RuntimeException(
×
181
                        \sprintf(
×
182
                            'Can\'t render template "%s" for page "%s".',
×
183
                            $e->getSourceContext()->getName(),
×
184
                            $page->getFileName() ?? $page->getId()
×
185
                        ),
×
186
                        previous: $e,
×
187
                        file: $e->getSourceContext()->getPath(),
×
188
                        line: $e->getTemplateLine(),
×
189
                    );
×
190
                } catch (\Exception $e) {
×
191
                    throw new RuntimeException($e->getMessage(), previous: $e);
×
192
                }
193
            }
194
            $this->builder->getPages()->replace($page->getId(), $page);
1✔
195

196
            $templates = array_column($rendered, 'template');
1✔
197
            $message = \sprintf(
1✔
198
                'Page "%s" rendered with [%s]',
1✔
199
                $page->getId() ?: 'index',
1✔
200
                Util\Str::combineArrayToString($templates, 'scope', 'file')
1✔
201
            );
1✔
202
            $this->builder->getLogger()->info($message, ['progress' => [$count, $total]]);
1✔
203
        }
204
        // profiler
205
        if ($this->builder->isDebug()) {
1✔
206
            try {
207
                // HTML
208
                $htmlDumper = new \Twig\Profiler\Dumper\HtmlDumper();
1✔
209
                $profileHtmlFile = Util::joinFile($this->config->getDestinationDir(), self::TMP_DIR, 'twig_profile.html');
1✔
210
                Util\File::getFS()->dumpFile($profileHtmlFile, $htmlDumper->dump($this->builder->getRenderer()->getDebugProfile()));
1✔
211
                // TXT
212
                $textDumper = new \Twig\Profiler\Dumper\TextDumper();
1✔
213
                $profileTextFile = Util::joinFile($this->config->getDestinationDir(), self::TMP_DIR, 'twig_profile.txt');
1✔
214
                Util\File::getFS()->dumpFile($profileTextFile, $textDumper->dump($this->builder->getRenderer()->getDebugProfile()));
1✔
215
                // log
216
                $this->builder->getLogger()->debug(\sprintf('Twig profile dumped in "%s"', Util::joinFile($this->config->getDestinationDir(), self::TMP_DIR)));
1✔
217
            } catch (\Symfony\Component\Filesystem\Exception\IOException $e) {
×
218
                throw new RuntimeException($e->getMessage());
×
219
            }
220
        }
221
    }
222

223
    /**
224
     * Returns an array of layouts directories.
225
     */
226
    protected function getAllLayoutsPaths(): array
227
    {
228
        $paths = [];
1✔
229

230
        // layouts/
231
        if (is_dir($this->config->getLayoutsPath())) {
1✔
232
            $paths[] = $this->config->getLayoutsPath();
1✔
233
        }
234
        // <theme>/layouts/
235
        if ($this->config->hasTheme()) {
1✔
236
            foreach ($this->config->getTheme() ?? [] as $theme) {
1✔
237
                $paths[] = $this->config->getThemeDirPath($theme);
1✔
238
            }
239
        }
240
        // resources/layouts/
241
        if (is_dir($this->config->getLayoutsInternalPath())) {
1✔
242
            $paths[] = $this->config->getLayoutsInternalPath();
1✔
243
        }
244

245
        return $paths;
1✔
246
    }
247

248
    /**
249
     * Adds global variables.
250
     */
251
    protected function addGlobals()
252
    {
253
        $this->builder->getRenderer()->addGlobal('cecil', [
1✔
254
            'url'       => \sprintf('https://cecil.app/#%s', Builder::getVersion()),
1✔
255
            'version'   => Builder::getVersion(),
1✔
256
            'poweredby' => \sprintf('Cecil v%s', Builder::getVersion()),
1✔
257
        ]);
1✔
258
    }
259

260
    /**
261
     * Get available output formats.
262
     *
263
     * @throws RuntimeException
264
     */
265
    protected function getOutputFormats(Page $page): array
266
    {
267
        // Get page output format(s) if defined.
268
        // ie:
269
        // ```yaml
270
        // output: txt
271
        // ```
272
        if ($page->getVariable('output')) {
1✔
273
            $formats = $page->getVariable('output');
1✔
274
            if (!\is_array($formats)) {
1✔
275
                $formats = [$formats];
1✔
276
            }
277

278
            return $formats;
1✔
279
        }
280

281
        // Get available output formats for the page type.
282
        // ie:
283
        // ```yaml
284
        // page: [html, json]
285
        // ```
286
        $formats = $this->config->get('output.pagetypeformats.' . $page->getType());
1✔
287
        if (empty($formats)) {
1✔
288
            throw new RuntimeException('Configuration key "pagetypeformats" can\'t be empty.');
×
289
        }
290
        if (!\is_array($formats)) {
1✔
291
            $formats = [$formats];
×
292
        }
293

294
        return array_unique($formats);
1✔
295
    }
296

297
    /**
298
     * Get alternates.
299
     */
300
    protected function getAlternates(array $formats): array
301
    {
302
        $alternates = [];
1✔
303

304
        if (\count($formats) > 1 || \in_array('html', $formats)) {
1✔
305
            foreach ($formats as $format) {
1✔
306
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
1✔
307
                $alternates[] = [
1✔
308
                    'rel'    => $rel,
1✔
309
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
1✔
310
                    'title'  => strtoupper($format),
1✔
311
                    'format' => $format,
1✔
312
                ];
1✔
313
            }
314
        }
315

316
        return $alternates;
1✔
317
    }
318

319
    /**
320
     * Returns the collection of translated pages for a given page.
321
     */
322
    protected function getTranslations(Page $refPage): \Cecil\Collection\Page\Collection
323
    {
324
        $pages = $this->builder->getPages()->filter(function (Page $page) use ($refPage) {
1✔
325
            return $page->getId() !== $refPage->getId()
1✔
326
                && $page->getVariable('langref') == $refPage->getVariable('langref')
1✔
327
                && $page->getType() == $refPage->getType()
1✔
328
                && !empty($page->getVariable('published'))
1✔
329
                && !$page->getVariable('paginated');
1✔
330
        });
1✔
331

332
        return $pages;
1✔
333
    }
334
}
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