Passed
Push — analysis-bQr3Aj ( 2dac3e )
by Arnaud
16:27 queued 14s
created

PagesRender::process()   F

Complexity

Conditions 14
Paths 353

Size

Total Lines 111
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 14
eloc 62
c 5
b 0
f 0
nc 353
nop 0
dl 0
loc 111
rs 3.5208

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
 * This file is part of the Cecil/Cecil package.
4
 *
5
 * Copyright (c) Arnaud Ligny <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Cecil\Step;
12
13
use Cecil\Builder;
14
use Cecil\Collection\Page\Page;
15
use Cecil\Collection\Page\PrefixSuffix;
16
use Cecil\Exception\Exception;
17
use Cecil\Renderer\Layout;
18
use Cecil\Renderer\Site;
19
use Cecil\Renderer\Twig;
20
use Cecil\Util;
21
22
/**
23
 * Pages rendering.
24
 */
25
class PagesRender extends AbstractStep
26
{
27
    /**
28
     * {@inheritdoc}
29
     *
30
     * @throws Exception
31
     */
32
    public function init($options)
33
    {
34
        if (!is_dir($this->config->getLayoutsPath()) && !$this->config->hasTheme()) {
35
            throw new Exception(sprintf(
36
                "'%s' is not a valid layouts directory",
37
                $this->config->getLayoutsPath()
38
            ));
39
        }
40
41
        $this->process = true;
42
    }
43
44
    /**
45
     * {@inheritdoc}
46
     *
47
     * @throws Exception
48
     */
49
    public function process()
50
    {
51
        // prepares renderer
52
        $this->builder->setRenderer(new Twig($this->getAllLayoutsPaths(), $this->builder));
53
54
        // adds global variables
55
        $this->addGlobals();
56
57
        call_user_func_array($this->builder->getMessageCb(), ['RENDER', 'Rendering pages']);
58
59
        // collects published pages
60
        /** @var Page $page */
61
        $filteredPages = $this->builder->getPages()->filter(function (Page $page) {
62
            return !empty($page->getVariable('published'));
63
        });
64
        $max = count($filteredPages);
65
66
        // renders each page
67
        $count = 0;
68
        /** @var Page $page */
69
        foreach ($filteredPages as $page) {
70
            $count++;
71
            $formats = ['html'];
0 ignored issues
show
Unused Code introduced by
The assignment to $formats is dead and can be removed.
Loading history...
72
            $rendered = [];
73
74
            // i18n
75
            $pageLang = $page->getVariable('language');
76
            $locale = $this->config->getLanguageProperty('locale', $pageLang);
77
            // the PHP Intl extension is needed to use localized date
78
            if (extension_loaded('intl')) {
79
                \Locale::setDefault($locale);
80
            }
81
            // the PHP Gettext extension is needed to use translation
82
            if (extension_loaded('gettext')) {
83
                $localePath = realpath(Util::joinFile($this->config->getSourceDir(), 'locale'));
84
                $domain = 'messages';
85
                putenv("LC_ALL=$locale");
86
                putenv("LANGUAGE=$locale");
87
                setlocale(LC_ALL, "$locale.UTF-8");
88
                bindtextdomain($domain, $localePath);
89
            }
90
91
            // global site variables
92
            $this->builder->getRenderer()->addGlobal('site', new Site($this->builder, $pageLang));
93
94
            // get Page's output formats
95
            $formats = $this->getOutputFormats($page);
96
            $page->setVariable('output', $formats);
97
98
            // excluded format(s)?
99
            foreach ($formats as $key => $format) {
100
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
101
                    // ie:
102
                    //   formats:
103
                    //     atom:
104
                    //       [...]
105
                    //       exclude: [paginated]
106
                    if (!is_array($exclude)) {
107
                        $exclude = [$exclude];
108
                    }
109
                    foreach ($exclude as $variable) {
110
                        if ($page->hasVariable($variable)) {
111
                            unset($formats[$key]);
112
                        }
113
                    }
114
                }
115
            }
116
117
            // get and set alternates links
118
            $page->setVariable('alternates', $this->getAlternates($formats));
119
120
            // renders each output format
121
            foreach ($formats as $format) {
122
                // search for the template
123
                $layout = Layout::finder($page, $format, $this->config);
124
                // renders with Twig
125
                try {
126
                    $output = $this->builder->getRenderer()->render($layout['file'], ['page' => $page]);
127
                    $output = $this->postProcessOutput($output, $page, $format);
128
                    $rendered[$format]['output'] = $output;
129
                    $rendered[$format]['template']['scope'] = $layout['scope'];
130
                    $rendered[$format]['template']['file'] = $layout['file'];
131
                    // profiler
132
                    if (getenv('CECIL_DEBUG') == 'true') {
133
                        $dumper = new \Twig\Profiler\Dumper\HtmlDumper();
134
                        file_put_contents(
135
                            Util::joinFile($this->config->getOutputPath(), '_debug_twig_profile.html'),
136
                            $dumper->dump($this->builder->getRenderer()->profile)
137
                        );
138
                    }
139
                } catch (\Twig\Error\Error $e) {
140
                    throw new Exception(sprintf(
141
                        'Template "%s:%s"%s (for page "%s"): %s',
142
                        $layout['scope'],
143
                        $layout['file'],
144
                        $e->getTemplateLine() >= 0 ? sprintf(' line %s', $e->getTemplateLine()) : '',
145
                        $page->getId(),
146
                        $e->getMessage()
147
                    ));
148
                }
149
            }
150
            $page->setVariable('rendered', $rendered);
151
            $this->builder->getPages()->replace($page->getId(), $page);
152
153
            $templates = array_column($rendered, 'template');
154
            $message = sprintf(
155
                '%s [%s]',
156
                ($page->getId() ?: 'index'),
157
                Util::combineArrayToString($templates, 'scope', 'file')
158
            );
159
            call_user_func_array($this->builder->getMessageCb(), ['RENDER_PROGRESS', $message, $count, $max]);
160
        }
161
    }
162
163
    /**
164
     * Returns an array of layouts directories.
165
     *
166
     * @return array
167
     */
168
    protected function getAllLayoutsPaths(): array
169
    {
170
        $paths = [];
171
172
        // layouts/
173
        if (is_dir($this->config->getLayoutsPath())) {
174
            $paths[] = $this->config->getLayoutsPath();
175
        }
176
        // <theme>/layouts/
177
        if ($this->config->hasTheme()) {
178
            $themes = $this->config->getTheme();
179
            foreach ($themes as $theme) {
180
                $paths[] = $this->config->getThemeDirPath($theme);
181
            }
182
        }
183
        // res/layouts/
184
        if (is_dir($this->config->getInternalLayoutsPath())) {
185
            $paths[] = $this->config->getInternalLayoutsPath();
186
        }
187
188
        return $paths;
189
    }
190
191
    /**
192
     * Adds global variables.
193
     */
194
    protected function addGlobals()
195
    {
196
        $this->builder->getRenderer()->addGlobal('cecil', [
197
            'url'       => sprintf('https://cecil.app/#%s', Builder::getVersion()),
198
            'version'   => Builder::getVersion(),
199
            'poweredby' => sprintf('Cecil v%s', Builder::getVersion()),
200
        ]);
201
    }
202
203
    /**
204
     * Get available output formats.
205
     *
206
     * @param Page $page
207
     *
208
     * @return array
209
     */
210
    protected function getOutputFormats(Page $page): array
211
    {
212
        // Get available output formats for the page type.
213
        // ie:
214
        //   page: [html, json]
215
        $formats = $this->config->get('output.pagetypeformats.'.$page->getType());
216
217
        if (empty($formats)) {
218
            throw new Exception('Configuration key "pagetypeformats" can\'t be empty.');
219
        }
220
221
        if (!\is_array($formats)) {
222
            $formats = [$formats];
223
        }
224
225
        // Get page output format(s).
226
        // ie:
227
        //   output: txt
228
        if ($page->getVariable('output')) {
229
            $formats = $page->getVariable('output');
230
            if (!\is_array($formats)) {
231
                $formats = [$formats];
232
            }
233
        }
234
235
        return $formats;
236
    }
237
238
    /**
239
     * Get alternates.
240
     *
241
     * @param array $formats
242
     *
243
     * @return array
244
     */
245
    protected function getAlternates(array $formats): array
246
    {
247
        $alternates = [];
248
249
        if (count($formats) > 1 || in_array('html', $formats)) {
250
            foreach ($formats as $format) {
251
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
252
                $alternates[] = [
253
                    'rel'    => $rel,
254
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
255
                    'title'  => strtoupper($format),
256
                    'format' => $format,
257
                ];
258
            }
259
        }
260
261
        return $alternates;
262
    }
263
264
    /**
265
     * Apply post rendering on output.
266
     *
267
     * @param string $rendered
268
     * @param Page   $page
269
     * @param string $format
270
     *
271
     * @return string
272
     */
273
    private function postProcessOutput(string $rendered, Page $page, string $format): string
274
    {
275
        switch ($format) {
276
            case 'html':
277
                // add generator meta
278
                if (!preg_match('/<meta name="generator".*/i', $rendered)) {
279
                    $meta = \sprintf('<meta name="generator" content="Cecil %s" />', Builder::getVersion());
280
                    $rendered = preg_replace('/(<\/head>)/i', "\t$meta\n  $1", $rendered);
281
                }
282
                // replace excerpt or break tag by HTML anchor
283
                // https://regex101.com/r/Xl7d5I/3
284
                $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
285
                $replacement = '$1<span id="more"></span>$4';
286
                $rendered = preg_replace('/'.$pattern.'/is', $replacement, $rendered);
287
        }
288
289
        // replace internal link to *.md files with the right URL
290
        // https://regex101.com/r/dZ02zO/5
291
        $replace = 'href="../%s/%s"';
292
        if (empty($page->getFolder())) {
293
            $replace = 'href="%s/%s"';
294
        }
295
        $rendered = preg_replace_callback(
296
            '/href="([A-Za-z0-9_\.\-\/]+)\.md(\#[A-Za-z0-9\-]+)?"/is',
297
            function ($matches) use ($replace) {
298
                return \sprintf($replace, Page::slugify(PrefixSuffix::sub($matches[1])), $matches[2] ?? '');
299
            },
300
            $rendered
301
        );
302
303
        return $rendered;
304
    }
305
}
306