Passed
Pull Request — master (#1013)
by lee
07:38
created

PagesRender   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 295
Duplicated Lines 0 %

Test Coverage

Coverage 90.23%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 126
c 10
b 0
f 0
dl 0
loc 295
ccs 120
cts 133
cp 0.9023
rs 9.2
wmc 40

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getAllLayoutsPaths() 0 21 5
A getName() 0 3 1
A init() 0 11 3
A getAlternates() 0 17 5
A getOutputFormats() 0 26 5
F process() 0 116 16
A postProcessOutput() 0 33 4
A addGlobals() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like PagesRender often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PagesRender, and based on these observations, apply Extract Interface, too.

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