Passed
Push — master ( ad5c6a...ab8cf4 )
by Arnaud
05:32
created

Render::process()   D

Complexity

Conditions 15
Paths 257

Size

Total Lines 117
Code Lines 65

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 65
CRAP Score 15.5329

Importance

Changes 5
Bugs 2 Features 1
Metric Value
cc 15
eloc 65
c 5
b 2
f 1
nc 257
nop 0
dl 0
loc 117
ccs 65
cts 75
cp 0.8667
crap 15.5329
rs 4.3708

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\PrefixSuffix;
20
use Cecil\Exception\RuntimeException;
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
        /** @var Collection $pages */
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
            // enrichs some variables
73 1
            ->map(function (Page $page) {
74 1
                $formats = $this->getOutputFormats($page);
75
                // output formats
76 1
                $page->setVariable('output', $formats);
77
                // alternates formats
78 1
                $page->setVariable('alternates', $this->getAlternates($formats));
79
                // translations
80 1
                $page->setVariable('translations', $this->getTranslations($page));
81
82 1
                return $page;
83 1
            });
84 1
        $total = count($pages);
85
86
        // renders each page
87 1
        $count = 0;
88
        /** @var Page $page */
89 1
        foreach ($pages as $page) {
90 1
            $count++;
91 1
            $rendered = [];
92
93
            // l10n
94 1
            $language = $page->getVariable('language', $this->config->getLanguageDefault());
95 1
            $locale = $this->config->getLanguageProperty('locale', $language);
96 1
            $this->builder->getRenderer()->setLocale($locale);
97
98
            // global site variables
99 1
            $this->builder->getRenderer()->addGlobal('site', new Site($this->builder, $language));
100
101
            // excluded format(s)?
102 1
            $formats = (array) $page->getVariable('output');
103 1
            foreach ($formats as $key => $format) {
104 1
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
105
                    // ie:
106
                    //   formats:
107
                    //     atom:
108
                    //       [...]
109
                    //       exclude: [paginated]
110 1
                    if (!is_array($exclude)) {
111
                        $exclude = [$exclude];
112
                    }
113 1
                    foreach ($exclude as $variable) {
114 1
                        if ($page->hasVariable($variable)) {
115 1
                            unset($formats[$key]);
116
                        }
117
                    }
118
                }
119
            }
120
121
            // renders each output format
122 1
            foreach ($formats as $format) {
123
                // search for the template
124 1
                $layout = Layout::finder($page, $format, $this->config);
125
                // renders with Twig
126
                try {
127 1
                    $deprecations = [];
128 1
                    set_error_handler(function ($type, $msg) use (&$deprecations) {
129 1
                        if (E_USER_DEPRECATED === $type) {
130 1
                            $deprecations[] = $msg;
131
                        }
132 1
                    });
133 1
                    $output = $this->builder->getRenderer()->render($layout['file'], ['page' => $page]);
134 1
                    foreach ($deprecations as $value) {
135 1
                        $this->builder->getLogger()->warning($value);
136
                    }
137 1
                    $output = $this->postProcessOutput($output, $page, $format);
138 1
                    $rendered[$format] = [
139 1
                        'output'   => $output,
140 1
                        'template' => [
141 1
                            'scope' => $layout['scope'],
142 1
                            'file'  => $layout['file'],
143 1
                        ]
144 1
                    ];
145
                    // profiler
146 1
                    if ($this->builder->isDebug()) {
147 1
                        $dumper = new \Twig\Profiler\Dumper\HtmlDumper();
148 1
                        file_put_contents(
149 1
                            Util::joinFile($this->config->getOutputPath(), '_debug_twig_profile.html'),
150 1
                            $dumper->dump($this->builder->getRenderer()->getDebugProfile())
151 1
                        );
152
                    }
153
                } catch (\Twig\Error\Error $e) {
154
                    $template = !empty($e->getSourceContext()->getPath()) ? $e->getSourceContext()->getPath() : $e->getSourceContext()->getName();
155
156
                    throw new RuntimeException(\sprintf(
157
                        'Template "%s%s" (page: %s): %s',
158
                        $template,
159
                        $e->getTemplateLine() >= 0 ? \sprintf(':%s', $e->getTemplateLine()) : '',
160
                        $page->getId(),
161
                        $e->getMessage()
162
                    ));
163
                }
164
            }
165 1
            $page->setRendered($rendered);
166 1
            $this->builder->getPages()->replace($page->getId(), $page);
167
168 1
            $templates = array_column($rendered, 'template');
169 1
            $message = \sprintf(
170 1
                'Page "%s" rendered with template(s) "%s"',
171 1
                ($page->getId() ?: 'index'),
172 1
                Util\Str::combineArrayToString($templates, 'scope', 'file')
173 1
            );
174 1
            $this->builder->getLogger()->info($message, ['progress' => [$count, $total]]);
175
        }
176
    }
177
178
    /**
179
     * Returns an array of layouts directories.
180
     */
181 1
    protected function getAllLayoutsPaths(): array
182
    {
183 1
        $paths = [];
184
185
        // layouts/
186 1
        if (is_dir($this->config->getLayoutsPath())) {
187 1
            $paths[] = $this->config->getLayoutsPath();
188
        }
189
        // <theme>/layouts/
190 1
        if ($this->config->hasTheme()) {
191 1
            $themes = $this->config->getTheme();
192 1
            foreach ($themes as $theme) {
193 1
                $paths[] = $this->config->getThemeDirPath($theme);
194
            }
195
        }
196
        // resources/layouts/
197 1
        if (is_dir($this->config->getInternalLayoutsPath())) {
198 1
            $paths[] = $this->config->getInternalLayoutsPath();
199
        }
200
201 1
        return $paths;
202
    }
203
204
    /**
205
     * Adds global variables.
206
     */
207 1
    protected function addGlobals()
208
    {
209 1
        $this->builder->getRenderer()->addGlobal('cecil', [
210 1
            'url'       => \sprintf('https://cecil.app/#%s', Builder::getVersion()),
211 1
            'version'   => Builder::getVersion(),
212 1
            'poweredby' => \sprintf('Cecil v%s', Builder::getVersion()),
213 1
        ]);
214
    }
215
216
    /**
217
     * Get available output formats.
218
     *
219
     * @throws RuntimeException
220
     */
221 1
    protected function getOutputFormats(Page $page): array
222
    {
223
        // Get page output format(s) if defined.
224
        // ie:
225
        // ```yaml
226
        // output: txt
227
        // ```
228 1
        if ($page->getVariable('output')) {
229 1
            $formats = $page->getVariable('output');
230 1
            if (!\is_array($formats)) {
231 1
                $formats = [$formats];
232
            }
233
234 1
            return $formats;
235
        }
236
237
        // Get available output formats for the page type.
238
        // ie:
239
        // ```yaml
240
        // page: [html, json]
241
        // ```
242 1
        $formats = $this->config->get('output.pagetypeformats.' . $page->getType());
243 1
        if (empty($formats)) {
244
            throw new RuntimeException('Configuration key "pagetypeformats" can\'t be empty.');
245
        }
246 1
        if (!\is_array($formats)) {
247
            $formats = [$formats];
248
        }
249
250 1
        return $formats;
251
    }
252
253
    /**
254
     * Get alternates.
255
     */
256 1
    protected function getAlternates(array $formats): array
257
    {
258 1
        $alternates = [];
259
260 1
        if (count($formats) > 1 || in_array('html', $formats)) {
261 1
            foreach ($formats as $format) {
262 1
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
263 1
                $alternates[] = [
264 1
                    'rel'    => $rel,
265 1
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
266 1
                    'title'  => strtoupper($format),
267 1
                    'format' => $format,
268 1
                ];
269
            }
270
        }
271
272 1
        return $alternates;
273
    }
274
275
    /**
276
     * Returns the collection of translated pages for a given page.
277
     */
278 1
    protected function getTranslations(Page $refPage): \Cecil\Collection\Page\Collection
279
    {
280 1
        $pages = $this->builder->getPages()->filter(function (Page $page) use ($refPage) {
281 1
            return $page->getId() !== $refPage->getId()
282 1
                && $page->getVariable('langref') == $refPage->getVariable('langref')
283 1
                && $page->getType() == $refPage->getType()
284 1
                && !empty($page->getVariable('published'))
285 1
                && !$page->getVariable('paginated');
286 1
        });
287
288 1
        return $pages;
289
    }
290
291
    /**
292
     * Apply post rendering on output.
293
     */
294 1
    private function postProcessOutput(string $output, Page $page, string $format): string
295
    {
296
        switch ($format) {
297 1
            case 'html':
298
                // add generator meta tag
299 1
                if (!preg_match('/<meta name="generator".*/i', $output)) {
300 1
                    $meta = \sprintf('<meta name="generator" content="Cecil %s" />', Builder::getVersion());
301 1
                    $output = preg_replace_callback('/([[:blank:]]*)(<\/head>)/i', function ($matches) use ($meta) {
302 1
                        return str_repeat($matches[1] ?: ' ', 2) . $meta . "\n" . $matches[1] . $matches[2];
303 1
                    }, $output);
304
                }
305
                // replace excerpt or break tag by HTML anchor
306
                // https://regex101.com/r/Xl7d5I/3
307 1
                $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
308 1
                $replacement = '$1<span id="more"></span>$4';
309 1
                $excerpt = preg_replace('/' . $pattern . '/is', $replacement, $output, 1);
310 1
                $output = $excerpt ?? $output;
311
        }
312
313
        // replace internal link to *.md files with the right URL
314 1
        $output = preg_replace_callback(
315
            // https://regex101.com/r/dZ02zO/6
316
            //'/href="([A-Za-z0-9_\.\-\/]+)\.md(\#[A-Za-z0-9\-]+)?"/is',
317
            // https://regex101.com/r/ycWMe4/1
318 1
            '/href="(\/|)([A-Za-z0-9_\.\-\/]+)\.md(\#[A-Za-z0-9\-]+)?"/is',
319 1
            function ($matches) use ($page) {
320
                // section spage
321 1
                $hrefPattern = 'href="../%s/%s"';
322
                // root page
323 1
                if (empty($page->getFolder())) {
324 1
                    $hrefPattern = 'href="%s/%s"';
325
                }
326
                // root link
327 1
                if ($matches[1] == '/') {
328
                    $hrefPattern = 'href="/%s/%s"';
329
                }
330
331 1
                return \sprintf($hrefPattern, Page::slugify(PrefixSuffix::sub($matches[2])), $matches[3] ?? '');
332 1
            },
333 1
            $output
334 1
        );
335
336 1
        return $output;
337
    }
338
}
339