Passed
Push — fix-rendering-post-process ( f025b0...b476c5 )
by Arnaud
18:08 queued 12:10
created

PagesRender::postProcessOutput()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 14
c 1
b 0
f 0
nc 3
nop 2
dl 0
loc 27
rs 9.7998
1
<?php
2
/*
3
 * Copyright (c) Arnaud Ligny <[email protected]>
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
9
namespace Cecil\Step;
10
11
use Cecil\Builder;
12
use Cecil\Collection\Page\Page;
13
use Cecil\Collection\Page\PrefixSuffix;
14
use Cecil\Exception\Exception;
15
use Cecil\Renderer\Layout;
16
use Cecil\Renderer\Site;
17
use Cecil\Renderer\Twig;
18
19
/**
20
 * Pages rendering.
21
 */
22
class PagesRender extends AbstractStep
23
{
24
    /**
25
     * {@inheritdoc}
26
     *
27
     * @throws Exception
28
     */
29
    public function init($options)
30
    {
31
        if (!is_dir($this->config->getLayoutsPath()) && !$this->config->hasTheme()) {
32
            throw new Exception(sprintf(
33
                "'%s' is not a valid layouts directory",
34
                $this->config->getLayoutsPath()
35
            ));
36
        }
37
38
        $this->process = true;
39
    }
40
41
    /**
42
     * {@inheritdoc}
43
     *
44
     * @throws Exception
45
     */
46
    public function process()
47
    {
48
        // prepares renderer
49
        $this->builder->setRenderer(new Twig($this->getAllLayoutsPaths(), $this->builder));
50
51
        // add globals variables
52
        $this->addGlobals();
53
54
        call_user_func_array($this->builder->getMessageCb(), ['RENDER', 'Rendering pages']);
55
56
        // collect published pages
57
        /* @var $page Page */
58
        $filteredPages = $this->builder->getPages()->filter(function (Page $page) {
59
            return !empty($page->getVariable('published'));
60
        });
61
        $max = count($filteredPages);
62
63
        // render each page
64
        $count = 0;
65
        /* @var $page Page */
66
        foreach ($filteredPages as $page) {
67
            $count++;
68
            $formats = ['html'];
0 ignored issues
show
Unused Code introduced by
The assignment to $formats is dead and can be removed.
Loading history...
69
            $rendered = [];
70
71
            // i18n
72
            $pageLang = $page->getVariable('language');
73
            $locale = $this->config->getLanguageProperty('locale', $pageLang);
74
            // The PHP Intl extension is needed to use localized date
75
            if (extension_loaded('intl')) {
76
                \Locale::setDefault($locale);
77
            }
78
            // The PHP Gettext extension is needed to use translation
79
            if (extension_loaded('gettext')) {
80
                $localePath = realpath($this->config->getSourceDir().'/locale');
81
                $domain = 'messages';
82
                putenv("LC_ALL=$locale");
83
                putenv("LANGUAGE=$locale");
84
                setlocale(LC_ALL, "$locale.UTF-8");
85
                bindtextdomain($domain, $localePath);
86
            }
87
88
            // global site variables
89
            $this->builder->getRenderer()->addGlobal('site', new Site($this->builder, $pageLang));
90
91
            // get page's output formats
92
            $formats = $this->getOutputFormats($page);
93
            $page->setVariable('output', $formats);
94
95
            // excluded format(s)?
96
            foreach ($formats as $key => $format) {
97
                if ($exclude = $this->config->getOutputFormatProperty($format, 'exclude')) {
98
                    // ie:
99
                    //   formats:
100
                    //     atom:
101
                    //       [...]
102
                    //       exclude: [paginated]
103
                    if (!is_array($exclude)) {
104
                        $exclude = [$exclude];
105
                    }
106
                    foreach ($exclude as $variable) {
107
                        if ($page->hasVariable($variable)) {
108
                            unset($formats[$key]);
109
                        }
110
                    }
111
                }
112
            }
113
114
            // get and set alternates links
115
            $page->setVariable('alternates', $this->getAlternates($formats));
116
117
            // render each output format
118
            foreach ($formats as $format) {
119
                // search for the template
120
                $layout = Layout::finder($page, $format, $this->config);
121
                // render with Twig
122
                try {
123
                    $rendered[$format]['output'] = $this->postProcessOutput($this->builder->getRenderer()->render(
124
                        $layout,
125
                        ['page' => $page]
126
                    ), $format);
127
                    $rendered[$format]['template'] = $layout;
128
                } catch (\Exception $e) {
129
                    throw new Exception(sprintf(
130
                        "Error in template \"%s\" for page \"%s\":\n%s",
131
                        $layout,
132
                        $page->getId(),
133
                        $e->getMessage()
134
                    ));
135
                }
136
            }
137
            $page->setVariable('rendered', $rendered);
138
            $this->builder->getPages()->replace($page->getId(), $page);
139
140
            $layouts = implode(', ', array_column($rendered, 'template'));
141
            $message = sprintf('%s [%s]', ($page->getId() ?: 'index'), $layouts);
142
            call_user_func_array($this->builder->getMessageCb(), ['RENDER_PROGRESS', $message, $count, $max]);
143
        }
144
    }
145
146
    /**
147
     * Return an array of layouts directories.
148
     *
149
     * @return array
150
     */
151
    protected function getAllLayoutsPaths(): array
152
    {
153
        $paths = [];
154
155
        // layouts/
156
        if (is_dir($this->config->getLayoutsPath())) {
157
            $paths[] = $this->config->getLayoutsPath();
158
        }
159
        // <theme>/layouts/
160
        if ($this->config->hasTheme()) {
161
            $themes = $this->config->getTheme();
162
            foreach ($themes as $theme) {
163
                $paths[] = $this->config->getThemeDirPath($theme);
164
            }
165
        }
166
        // res/layouts/
167
        if (is_dir($this->config->getInternalLayoutsPath())) {
168
            $paths[] = $this->config->getInternalLayoutsPath();
169
        }
170
171
        return $paths;
172
    }
173
174
    /**
175
     * Add globals variables.
176
     */
177
    protected function addGlobals()
178
    {
179
        $this->builder->getRenderer()->addGlobal('cecil', [
180
            'url'       => sprintf('https://cecil.app/#%s', Builder::getVersion()),
181
            'version'   => Builder::getVersion(),
182
            'poweredby' => sprintf('Cecil v%s', Builder::getVersion()),
183
        ]);
184
    }
185
186
    /**
187
     * Get available output formats.
188
     *
189
     * @param Page $page
190
     *
191
     * @return array
192
     */
193
    protected function getOutputFormats(Page $page): array
194
    {
195
        $formats = [];
196
197
        // Get available output formats for the page type.
198
        // ie:
199
        //   page: [html, json]
200
        if (\is_array($this->config->get('output.pagetypeformats.'.$page->getType()))) {
201
            $formats = $this->config->get('output.pagetypeformats.'.$page->getType());
202
        }
203
        // Get page output format(s).
204
        // ie:
205
        //   output: txt
206
        if ($page->getVariable('output')) {
207
            $formats = $page->getVariable('output');
208
            if (!\is_array($formats)) {
209
                $formats = [$formats];
210
            }
211
        }
212
213
        return $formats;
214
    }
215
216
    /**
217
     * Get alternates.
218
     *
219
     * @param array $formats
220
     *
221
     * @return array
222
     */
223
    protected function getAlternates(array $formats): array
224
    {
225
        $alternates = [];
226
227
        if (count($formats) > 1 || in_array('html', $formats)) {
228
            foreach ($formats as $format) {
229
                $format == 'html' ? $rel = 'canonical' : $rel = 'alternate';
230
                $alternates[] = [
231
                    'rel'    => $rel,
232
                    'type'   => $this->config->getOutputFormatProperty($format, 'mediatype'),
233
                    'title'  => strtoupper($format),
234
                    'format' => $format,
235
                ];
236
            }
237
        }
238
239
        return $alternates;
240
    }
241
242
    /**
243
     * Apply post rendering on output.
244
     *
245
     * @param string $rendered
246
     * @param string $format
247
     *
248
     * @return string
249
     */
250
    private function postProcessOutput(string $rendered, string $format): string
251
    {
252
        switch ($format) {
253
            case 'html':
254
                // add generator meta
255
                if (!preg_match('/<meta name="generator".*/i', $rendered)) {
256
                    $meta = \sprintf('<meta name="generator" content="Cecil %s" />', Builder::getVersion());
257
                    $rendered = preg_replace('/(<\/head>)/i', "\t$meta\n  $1", $rendered);
258
                }
259
                // replace excerpt or break tag by HTML anchor
260
                // https://regex101.com/r/Xl7d5I/3
261
                $pattern = '(.*)(<!--[[:blank:]]?(excerpt|break)[[:blank:]]?-->)(.*)';
262
                $replacement = '$1<span id="more"></span>$4';
263
                $rendered = preg_replace('/'.$pattern.'/is', $replacement, $rendered);
264
        }
265
266
        // replace internal link to *.md files with the right URL
267
        // https://regex101.com/r/dZ02zO/5
268
        $rendered = preg_replace_callback(
269
            '/href="([A-Za-z0-9_\.\-\/]+)\.md(\#[A-Za-z0-9\-]+)?"/is',
270
            function ($matches) {
271
                return \sprintf('href="../%s%s/"', Page::slugify(PrefixSuffix::sub($matches[1])), $matches[2] ?? '');
272
            },
273
            $rendered
274
        );
275
276
        return $rendered;
277
    }
278
}
279