Test Failed
Pull Request — master (#1670)
by Arnaud
07:53 queued 03:43
created

Render::postProcessOutput()   B

Complexity

Conditions 6
Paths 3

Size

Total Lines 41
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 6

Importance

Changes 3
Bugs 2 Features 0
Metric Value
cc 6
eloc 22
c 3
b 2
f 0
nc 3
nop 3
dl 0
loc 41
ccs 24
cts 24
cp 1
crap 6
rs 8.9457
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->outputPostProcess($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 1
                    $page->addRendered($rendered);
146
                    // profiler
147 1
                    if ($this->builder->isDebug()) {
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()->getDebugProfile())
152 1
                        );
153
                    }
154
                } catch (\Twig\Error\Error $e) {
155
                    $template = !empty($e->getSourceContext()->getPath()) ? $e->getSourceContext()->getPath() : $e->getSourceContext()->getName();
156
157
                    throw new RuntimeException(\sprintf(
158
                        'Template "%s%s" (page: %s): %s',
159
                        $template,
160
                        $e->getTemplateLine() >= 0 ? \sprintf(':%s', $e->getTemplateLine()) : '',
161
                        $page->getId(),
162
                        $e->getMessage()
163
                    ));
164
                }
165
            }
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 processors on rendered output.
293
     */
294 1
    private function outputPostProcess(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/ycWMe4/1
316 1
            '/href="(\/|)([A-Za-z0-9_\.\-\/]+)\.md(\#[A-Za-z0-9_\-]+)?"/is',
317 1
            function ($matches) use ($page) {
318
                // section spage
319 1
                $hrefPattern = 'href="../%s/%s"';
320
                // root page
321 1
                if (empty($page->getFolder())) {
322 1
                    $hrefPattern = 'href="%s/%s"';
323
                }
324
                // root link
325 1
                if ($matches[1] == '/') {
326 1
                    $hrefPattern = 'href="/%s/%s"';
327
                }
328
329 1
                return \sprintf($hrefPattern, Page::slugify(PrefixSuffix::sub($matches[2])), $matches[3] ?? '');
330 1
            },
331 1
            $output
332 1
        );
333
334 1
        return $output;
335
    }
336
}
337