Passed
Pull Request — master (#2148)
by Arnaud
09:31 queued 04:12
created

Render::getTranslations()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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