Passed
Push — assets ( 82f473...4367fa )
by Arnaud
08:49 queued 05:47
created

PagesRender::getAllLayoutsPaths()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 8
nop 0
dl 0
loc 21
rs 9.6111
c 0
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
     * @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
                } catch (\Twig\Error\Error $e) {
132
                    throw new Exception(sprintf(
133
                        "%s\nChecks template \"%s:%s\" line %s, used by page \"%s\".",
134
                        $e->getMessage(),
135
                        $layout['scope'],
136
                        $layout['file'],
137
                        $e->getLine(),
138
                        $page->getId()
139
                    ));
140
                } catch (\Exception $e) {
141
                    throw new Exception(sprintf(
142
                        'Error in template "%s:%s" for page "%s"',
143
                        $layout['scope'],
144
                        $layout['file'],
145
                        $page->getId()
146
                    ));
147
                }
148
            }
149
            $page->setVariable('rendered', $rendered);
150
            $this->builder->getPages()->replace($page->getId(), $page);
151
152
            $templates = array_column($rendered, 'template');
153
            $message = sprintf(
154
                '%s [%s]',
155
                ($page->getId() ?: 'index'),
156
                Util::combineArrayToString($templates, 'scope', 'file')
157
            );
158
            call_user_func_array($this->builder->getMessageCb(), ['RENDER_PROGRESS', $message, $count, $max]);
159
        }
160
    }
161
162
    /**
163
     * Returns an array of layouts directories.
164
     *
165
     * @return array
166
     */
167
    protected function getAllLayoutsPaths(): array
168
    {
169
        $paths = [];
170
171
        // layouts/
172
        if (is_dir($this->config->getLayoutsPath())) {
173
            $paths[] = $this->config->getLayoutsPath();
174
        }
175
        // <theme>/layouts/
176
        if ($this->config->hasTheme()) {
177
            $themes = $this->config->getTheme();
178
            foreach ($themes as $theme) {
179
                $paths[] = $this->config->getThemeDirPath($theme);
180
            }
181
        }
182
        // res/layouts/
183
        if (is_dir($this->config->getInternalLayoutsPath())) {
184
            $paths[] = $this->config->getInternalLayoutsPath();
185
        }
186
187
        return $paths;
188
    }
189
190
    /**
191
     * Adds global variables.
192
     */
193
    protected function addGlobals()
194
    {
195
        $this->builder->getRenderer()->addGlobal('cecil', [
196
            'url'       => sprintf('https://cecil.app/#%s', Builder::getVersion()),
197
            'version'   => Builder::getVersion(),
198
            'poweredby' => sprintf('Cecil v%s', Builder::getVersion()),
199
        ]);
200
    }
201
202
    /**
203
     * Get available output formats.
204
     *
205
     * @param Page $page
206
     *
207
     * @return array
208
     */
209
    protected function getOutputFormats(Page $page): array
210
    {
211
        // Get available output formats for the page type.
212
        // ie:
213
        //   page: [html, json]
214
        $formats = $this->config->get('output.pagetypeformats.'.$page->getType());
215
216
        if (empty($formats)) {
217
            throw new Exception('Configuration key "pagetypeformats" can\'t be empty.');
218
        }
219
220
        if (!\is_array($formats)) {
221
            $formats = [$formats];
222
        }
223
224
        // Get page output format(s).
225
        // ie:
226
        //   output: txt
227
        if ($page->getVariable('output')) {
228
            $formats = $page->getVariable('output');
229
            if (!\is_array($formats)) {
230
                $formats = [$formats];
231
            }
232
        }
233
234
        return $formats;
235
    }
236
237
    /**
238
     * Get alternates.
239
     *
240
     * @param array $formats
241
     *
242
     * @return array
243
     */
244
    protected function getAlternates(array $formats): array
245
    {
246
        $alternates = [];
247
248
        if (count($formats) > 1 || in_array('html', $formats)) {
249
            foreach ($formats as $format) {
250
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
251
                $alternates[] = [
252
                    'rel'    => $rel,
253
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
254
                    'title'  => strtoupper($format),
255
                    'format' => $format,
256
                ];
257
            }
258
        }
259
260
        return $alternates;
261
    }
262
263
    /**
264
     * Apply post rendering on output.
265
     *
266
     * @param string $rendered
267
     * @param Page   $page
268
     * @param string $format
269
     *
270
     * @return string
271
     */
272
    private function postProcessOutput(string $rendered, Page $page, string $format): string
273
    {
274
        switch ($format) {
275
            case 'html':
276
                // add generator meta
277
                if (!preg_match('/<meta name="generator".*/i', $rendered)) {
278
                    $meta = \sprintf('<meta name="generator" content="Cecil %s" />', Builder::getVersion());
279
                    $rendered = preg_replace('/(<\/head>)/i', "\t$meta\n  $1", $rendered);
280
                }
281
                // replace excerpt or break tag by HTML anchor
282
                // https://regex101.com/r/Xl7d5I/3
283
                $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
284
                $replacement = '$1<span id="more"></span>$4';
285
                $rendered = preg_replace('/'.$pattern.'/is', $replacement, $rendered);
286
        }
287
288
        // replace internal link to *.md files with the right URL
289
        // https://regex101.com/r/dZ02zO/5
290
        $replace = 'href="../%s/%s"';
291
        if (empty($page->getFolder())) {
292
            $replace = 'href="%s/%s"';
293
        }
294
        $rendered = preg_replace_callback(
295
            '/href="([A-Za-z0-9_\.\-\/]+)\.md(\#[A-Za-z0-9\-]+)?"/is',
296
            function ($matches) use ($replace) {
297
                return \sprintf($replace, Page::slugify(PrefixSuffix::sub($matches[1])), $matches[2] ?? '');
298
            },
299
            $rendered
300
        );
301
302
        return $rendered;
303
    }
304
}
305