Passed
Pull Request — master (#1704)
by Arnaud
09:41 queued 03:58
created

Render::getOutputFormats()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 30
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.1158

Importance

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