Passed
Push — nested-sections ( 37f302...6ff6f7 )
by Arnaud
04:43
created

Render::findParent()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 8

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 14
c 1
b 0
f 0
nc 12
nop 1
dl 0
loc 23
ccs 8
cts 8
cp 1
crap 8
rs 8.4444
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Cecil.
7
 *
8
 * Copyright (c) Arnaud Ligny <[email protected]>
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\Page;
18
use Cecil\Collection\Page\Type;
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
    /**
33
     * {@inheritdoc}
34
     */
35 1
    public function getName(): string
36
    {
37 1
        return 'Rendering pages';
38
    }
39
40
    /**
41
     * {@inheritdoc}
42
     */
43 1
    public function init(array $options): void
44
    {
45 1
        if (!is_dir($this->config->getLayoutsPath()) && !$this->config->hasTheme()) {
46
            $message = sprintf("'%s' is not a valid layouts directory", $this->config->getLayoutsPath());
47
            $this->builder->getLogger()->debug($message);
48
        }
49
50 1
        $this->canProcess = true;
51
    }
52
53
    /**
54
     * {@inheritdoc}
55
     *
56
     * @throws RuntimeException
57
     */
58 1
    public function process(): void
59
    {
60
        // prepares renderer
61 1
        $this->builder->setRenderer(new Twig($this->builder, $this->getAllLayoutsPaths()));
62
63
        // adds global variables
64 1
        $this->addGlobals();
65
66
        // prepares pages collections
67 1
        $pages = $this->builder->getPages()
68 1
            // published only
69 1
            ->filter(function (Page $page) {
70 1
                return (bool) $page->getVariable('published');
71 1
            })
72 1
            // enriched variables
73 1
            ->map(function (Page $page) {
74 1
                $formats = $this->getOutputFormats($page);
75
                // alternates formats
76 1
                $page->setVariable('output', $formats);
77 1
                $page->setVariable('alternates', $this->getAlternates($formats));
78
                // translations
79 1
                $page->setVariable('translations', $this->getTranslations($page));
80
                // parent
81 1
                if (($parent = $this->findParent($page)) !== null) {
82 1
                    $page->setParent($parent);
83 1
                }
84 1
85
                return $page;
86 1
            });
87 1
88
        // loads post processors
89 1
        $postprocessors = [];
90 1
        foreach ($this->config->get('output.postprocessors') as $name => $postprocessor) {
91 1
            if (!class_exists($postprocessor)) {
92 1
                $this->builder->getLogger()->error(sprintf('Unable to load output post processor "%s"', $postprocessor));
93 1
                break;
94 1
            }
95
            $postprocessors[] = new $postprocessor($this->builder);
96 1
            $this->builder->getLogger()->debug(sprintf('Output post processor "%s" loaded', $name));
97
        }
98
99 1
        // renders each page
100 1
        $total = \count($pages);
101
        $count = 0;
102
        foreach ($pages as $page) {
103 1
            $count++;
104 1
            $rendered = [];
105 1
106 1
            // l10n
107 1
            $language = $page->getVariable('language', $this->config->getLanguageDefault());
108
            $locale = $this->config->getLanguageProperty('locale', $language);
109 1
            $this->builder->getRenderer()->setLocale($locale);
110 1
111
            // global site variables
112
            $this->builder->getRenderer()->addGlobal('site', new Site($this->builder, $language));
113
114 1
            // global config variables
115 1
            $this->builder->getRenderer()->addGlobal('config', new Config($this->builder, $language));
116 1
117 1
            // excluded format(s)?
118 1
            $formats = (array) $page->getVariable('output');
119
            foreach ($formats as $key => $format) {
120
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
121 1
                    // ie:
122 1
                    //   formats:
123 1
                    //     atom:
124
                    //       [...]
125
                    //       exclude: [paginated]
126 1
                    if (!\is_array($exclude)) {
127
                        $exclude = [$exclude];
128
                    }
129 1
                    foreach ($exclude as $variable) {
130
                        if ($page->hasVariable($variable)) {
131
                            unset($formats[$key]);
132 1
                        }
133 1
                    }
134 1
                }
135
            }
136
137
            // renders each output format
138
            foreach ($formats as $format) {
139
                // search for the template
140 1
                $layout = Layout::finder($page, $format, $this->config);
141
                // renders with Twig
142
                try {
143 1
                    $deprecations = [];
144 1
                    set_error_handler(function ($type, $msg) use (&$deprecations) {
145 1
                        if (E_USER_DEPRECATED === $type) {
146
                            $deprecations[] = $msg;
147
                        }
148
                    });
149
                    $output = $this->builder->getRenderer()->render($layout['file'], ['page' => $page]);
150
                    foreach ($deprecations as $value) {
151
                        $this->builder->getLogger()->warning($value);
152 1
                    }
153
                    foreach ($postprocessors as $postprocessor) {
154 1
                        $output = $postprocessor->process($page, $output, $format);
155
                    }
156
                    $rendered[$format] = [
157 1
                        'output'   => $output,
158 1
                        'template' => [
159 1
                            'scope' => $layout['scope'],
160
                            'file'  => $layout['file'],
161
                        ],
162 1
                    ];
163 1
                    $page->addRendered($rendered);
164 1
                    // profiler
165
                    if ($this->builder->isDebug()) {
166
                        $dumper = new \Twig\Profiler\Dumper\HtmlDumper();
167 1
                        file_put_contents(
168 1
                            Util::joinFile($this->config->getOutputPath(), '_debug_twig_profile.html'),
169
                            $dumper->dump($this->builder->getRenderer()->getDebugProfile())
170 1
                        );
171 1
                    }
172 1
                } catch (\Twig\Error\Error $e) {
173 1
                    $template = !empty($e->getSourceContext()->getPath()) ? $e->getSourceContext()->getPath() : $e->getSourceContext()->getName();
174 1
175 1
                    throw new RuntimeException(sprintf(
176 1
                        'Template "%s%s" (page: %s): %s',
177 1
                        $template,
178
                        $e->getTemplateLine() >= 0 ? sprintf(':%s', $e->getTemplateLine()) : '',
179 1
                        $page->getId(),
180 1
                        $e->getMessage()
181 1
                    ));
182 1
                }
183 1
            }
184 1
            $this->builder->getPages()->replace($page->getId(), $page);
185
186
            $templates = array_column($rendered, 'template');
187
            $message = sprintf(
188
                'Page "%s" rendered with [%s]',
189
                $page->getId() ?: 'index',
190
                Util\Str::combineArrayToString($templates, 'scope', 'file')
191
            );
192
            $this->builder->getLogger()->info($message, ['progress' => [$count, $total]]);
193
        }
194
    }
195
196
    /**
197
     * Returns an array of layouts directories.
198 1
     */
199
    protected function getAllLayoutsPaths(): array
200 1
    {
201 1
        $paths = [];
202 1
203 1
        // layouts/
204 1
        if (is_dir($this->config->getLayoutsPath())) {
205 1
            $paths[] = $this->config->getLayoutsPath();
206 1
        }
207
        // <theme>/layouts/
208
        if ($this->config->hasTheme()) {
209
            $themes = $this->config->getTheme();
210
            foreach ($themes as $theme) {
211
                $paths[] = $this->config->getThemeDirPath($theme);
212
            }
213 1
        }
214
        // resources/layouts/
215 1
        if (is_dir($this->config->getLayoutsInternalPath())) {
216
            $paths[] = $this->config->getLayoutsInternalPath();
217
        }
218 1
219 1
        return $paths;
220
    }
221
222 1
    /**
223 1
     * Adds global variables.
224 1
     */
225 1
    protected function addGlobals()
226
    {
227
        $this->builder->getRenderer()->addGlobal('cecil', [
228
            'url'       => sprintf('https://cecil.app/#%s', Builder::getVersion()),
229 1
            'version'   => Builder::getVersion(),
230 1
            'poweredby' => sprintf('Cecil v%s', Builder::getVersion()),
231
        ]);
232
    }
233 1
234
    /**
235
     * Get available output formats.
236
     *
237
     * @throws RuntimeException
238
     */
239 1
    protected function getOutputFormats(Page $page): array
240
    {
241 1
        // Get page output format(s) if defined.
242 1
        // ie:
243 1
        // ```yaml
244 1
        // output: txt
245 1
        // ```
246
        if ($page->getVariable('output')) {
247
            $formats = $page->getVariable('output');
248
            if (!\is_array($formats)) {
249
                $formats = [$formats];
250
            }
251
252
            return $formats;
253 1
        }
254
255
        // Get available output formats for the page type.
256
        // ie:
257
        // ```yaml
258
        // page: [html, json]
259
        // ```
260 1
        $formats = $this->config->get('output.pagetypeformats.' . $page->getType());
261 1
        if (empty($formats)) {
262 1
            throw new RuntimeException('Configuration key "pagetypeformats" can\'t be empty.');
263 1
        }
264
        if (!\is_array($formats)) {
265
            $formats = [$formats];
266 1
        }
267
268
        return $formats;
269
    }
270
271
    /**
272
     * Get alternates.
273
     */
274 1
    protected function getAlternates(array $formats): array
275 1
    {
276
        $alternates = [];
277
278 1
        if (\count($formats) > 1 || \in_array('html', $formats)) {
279
            foreach ($formats as $format) {
280
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
281
                $alternates[] = [
282 1
                    'rel'    => $rel,
283
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
284
                    'title'  => strtoupper($format),
285
                    'format' => $format,
286
                ];
287
            }
288 1
        }
289
290 1
        return $alternates;
291
    }
292 1
293 1
    /**
294 1
     * Returns the collection of translated pages for a given page.
295 1
     */
296 1
    protected function getTranslations(Page $refPage): \Cecil\Collection\Page\Collection
297 1
    {
298 1
        $pages = $this->builder->getPages()->filter(function (Page $page) use ($refPage) {
299 1
            return $page->getId() !== $refPage->getId()
300 1
                && $page->getVariable('langref') == $refPage->getVariable('langref')
301
                && $page->getType() == $refPage->getType()
302
                && !empty($page->getVariable('published'))
303
                && !$page->getVariable('paginated');
304 1
        });
305
306
        return $pages;
307
    }
308
309
    /**
310 1
     * Find page parent or null.
311
     */
312 1
    protected function findParent(Page $page): ?Page
313 1
    {
314 1
        $parent = null;
315 1
        $langPrefix = '';
316 1
        if ($page->getVariable('language') !== null && $page->getVariable('language') != $this->config->getLanguageDefault()) {
317 1
            $langPrefix = $page->getVariable('language') . "/";
318 1
        }
319
        // home page by default
320 1
        if ($page->getType() !== Type::HOMEPAGE->value) {
321
            $parent = $this->builder->getPages()->get($langPrefix . 'index');
322
        }
323
        // recursive folder search
324
        $folderAsArray = explode('/', (string) $page->getFolder());
325
        while (\count($folderAsArray) >= 1 && !empty($folderAsArray[0])) {
326
            $parentFolder = implode('/', $folderAsArray);
327
            if ($this->builder->getPages()->has($langPrefix . $parentFolder) && $this->builder->getPages()->get($langPrefix . $parentFolder)->getId() !== $page->getId()) {
328
                $parent = $this->builder->getPages()->get($langPrefix . $parentFolder);
329
                break;
330
            }
331
            array_pop($folderAsArray);
332
        }
333
334
        return $parent;
335
    }
336
}
337