Passed
Push — i18n ( 3172bc )
by Arnaud
03:47
created

PagesRender::init()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4.679

Importance

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