Passed
Push — assets ( 4367fa...0679f6 )
by Arnaud
02:44 queued 11s
created

PagesRender   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Importance

Changes 9
Bugs 0 Features 0
Metric Value
eloc 113
c 9
b 0
f 0
dl 0
loc 270
rs 9.6
wmc 35

7 Methods

Rating   Name   Duplication   Size   Complexity  
A getAllLayoutsPaths() 0 21 5
A postProcessOutput() 0 31 4
A init() 0 10 3
A getAlternates() 0 17 5
A getOutputFormats() 0 26 5
A addGlobals() 0 6 1
C process() 0 102 12
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
            $rendered = [];
72
73
            // i18n
74
            $pageLang = $page->getVariable('language');
75
            $locale = $this->config->getLanguageProperty('locale', $pageLang);
76
            // the PHP Intl extension is needed to use localized date
77
            if (extension_loaded('intl')) {
78
                \Locale::setDefault($locale);
79
            }
80
            // the PHP Gettext extension is needed to use translation
81
            if (extension_loaded('gettext')) {
82
                $localePath = realpath(Util::joinFile($this->config->getSourceDir(), 'locale'));
83
                $domain = 'messages';
84
                putenv("LC_ALL=$locale");
85
                putenv("LANGUAGE=$locale");
86
                setlocale(LC_ALL, "$locale.UTF-8");
87
                bindtextdomain($domain, $localePath);
88
            }
89
90
            // global site variables
91
            $this->builder->getRenderer()->addGlobal('site', new Site($this->builder, $pageLang));
92
93
            // get Page's output formats
94
            $formats = $this->getOutputFormats($page);
95
            $page->setVariable('output', $formats);
96
97
            // excluded format(s)?
98
            foreach ($formats as $key => $format) {
99
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
100
                    // ie:
101
                    //   formats:
102
                    //     atom:
103
                    //       [...]
104
                    //       exclude: [paginated]
105
                    if (!is_array($exclude)) {
106
                        $exclude = [$exclude];
107
                    }
108
                    foreach ($exclude as $variable) {
109
                        if ($page->hasVariable($variable)) {
110
                            unset($formats[$key]);
111
                        }
112
                    }
113
                }
114
            }
115
116
            // get and set alternates links
117
            $page->setVariable('alternates', $this->getAlternates($formats));
118
119
            // renders each output format
120
            foreach ($formats as $format) {
121
                // search for the template
122
                $layout = Layout::finder($page, $format, $this->config);
123
                // renders with Twig
124
                try {
125
                    $output = $this->builder->getRenderer()->render($layout['file'], ['page' => $page]);
126
                    $output = $this->postProcessOutput($output, $page, $format);
127
                    $rendered[$format]['output'] = $output;
128
                    $rendered[$format]['template']['scope'] = $layout['scope'];
129
                    $rendered[$format]['template']['file'] = $layout['file'];
130
                } catch (\Twig\Error\Error $e) {
131
                    throw new Exception(sprintf(
132
                        "%s\nChecks template \"%s:%s\" line %s, used by page \"%s\".",
133
                        $e->getMessage(),
134
                        $layout['scope'],
135
                        $layout['file'],
136
                        $e->getLine(),
137
                        $page->getId()
138
                    ));
139
                }
140
            }
141
            $page->setVariable('rendered', $rendered);
142
            $this->builder->getPages()->replace($page->getId(), $page);
143
144
            $templates = array_column($rendered, 'template');
145
            $message = sprintf(
146
                '%s [%s]',
147
                ($page->getId() ?: 'index'),
148
                Util::combineArrayToString($templates, 'scope', 'file')
149
            );
150
            call_user_func_array($this->builder->getMessageCb(), ['RENDER_PROGRESS', $message, $count, $max]);
151
        }
152
    }
153
154
    /**
155
     * Returns an array of layouts directories.
156
     *
157
     * @return array
158
     */
159
    protected function getAllLayoutsPaths(): array
160
    {
161
        $paths = [];
162
163
        // layouts/
164
        if (is_dir($this->config->getLayoutsPath())) {
165
            $paths[] = $this->config->getLayoutsPath();
166
        }
167
        // <theme>/layouts/
168
        if ($this->config->hasTheme()) {
169
            $themes = $this->config->getTheme();
170
            foreach ($themes as $theme) {
171
                $paths[] = $this->config->getThemeDirPath($theme);
172
            }
173
        }
174
        // res/layouts/
175
        if (is_dir($this->config->getInternalLayoutsPath())) {
176
            $paths[] = $this->config->getInternalLayoutsPath();
177
        }
178
179
        return $paths;
180
    }
181
182
    /**
183
     * Adds global variables.
184
     */
185
    protected function addGlobals()
186
    {
187
        $this->builder->getRenderer()->addGlobal('cecil', [
188
            'url'       => sprintf('https://cecil.app/#%s', Builder::getVersion()),
189
            'version'   => Builder::getVersion(),
190
            'poweredby' => sprintf('Cecil v%s', Builder::getVersion()),
191
        ]);
192
    }
193
194
    /**
195
     * Get available output formats.
196
     *
197
     * @param Page $page
198
     *
199
     * @return array
200
     */
201
    protected function getOutputFormats(Page $page): array
202
    {
203
        // Get available output formats for the page type.
204
        // ie:
205
        //   page: [html, json]
206
        $formats = $this->config->get('output.pagetypeformats.'.$page->getType());
207
208
        if (empty($formats)) {
209
            throw new Exception('Configuration key "pagetypeformats" can\'t be empty.');
210
        }
211
212
        if (!\is_array($formats)) {
213
            $formats = [$formats];
214
        }
215
216
        // Get page output format(s).
217
        // ie:
218
        //   output: txt
219
        if ($page->getVariable('output')) {
220
            $formats = $page->getVariable('output');
221
            if (!\is_array($formats)) {
222
                $formats = [$formats];
223
            }
224
        }
225
226
        return $formats;
227
    }
228
229
    /**
230
     * Get alternates.
231
     *
232
     * @param array $formats
233
     *
234
     * @return array
235
     */
236
    protected function getAlternates(array $formats): array
237
    {
238
        $alternates = [];
239
240
        if (count($formats) > 1 || in_array('html', $formats)) {
241
            foreach ($formats as $format) {
242
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
243
                $alternates[] = [
244
                    'rel'    => $rel,
245
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
246
                    'title'  => strtoupper($format),
247
                    'format' => $format,
248
                ];
249
            }
250
        }
251
252
        return $alternates;
253
    }
254
255
    /**
256
     * Apply post rendering on output.
257
     *
258
     * @param string $rendered
259
     * @param Page   $page
260
     * @param string $format
261
     *
262
     * @return string
263
     */
264
    private function postProcessOutput(string $rendered, Page $page, string $format): string
265
    {
266
        switch ($format) {
267
            case 'html':
268
                // add generator meta
269
                if (!preg_match('/<meta name="generator".*/i', $rendered)) {
270
                    $meta = \sprintf('<meta name="generator" content="Cecil %s" />', Builder::getVersion());
271
                    $rendered = preg_replace('/(<\/head>)/i', "\t$meta\n  $1", $rendered);
272
                }
273
                // replace excerpt or break tag by HTML anchor
274
                // https://regex101.com/r/Xl7d5I/3
275
                $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
276
                $replacement = '$1<span id="more"></span>$4';
277
                $rendered = preg_replace('/'.$pattern.'/is', $replacement, $rendered);
278
        }
279
280
        // replace internal link to *.md files with the right URL
281
        // https://regex101.com/r/dZ02zO/5
282
        $replace = 'href="../%s/%s"';
283
        if (empty($page->getFolder())) {
284
            $replace = 'href="%s/%s"';
285
        }
286
        $rendered = preg_replace_callback(
287
            '/href="([A-Za-z0-9_\.\-\/]+)\.md(\#[A-Za-z0-9\-]+)?"/is',
288
            function ($matches) use ($replace) {
289
                return \sprintf($replace, Page::slugify(PrefixSuffix::sub($matches[1])), $matches[2] ?? '');
290
            },
291
            $rendered
292
        );
293
294
        return $rendered;
295
    }
296
}
297