Test Failed
Push — master ( 4516e7...6ee9d0 )
by Esteban De La Fuente
04:23
created

MarkdownRendererStrategy   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 261
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 110
dl 0
loc 261
ccs 0
cts 70
cp 0
rs 10
c 1
b 0
f 0
wmc 18

5 Methods

Rating   Name   Duplication   Size   Complexity  
B render() 0 51 6
A loadConfigurations() 0 23 1
A getMarkdown() 0 19 3
A __construct() 0 3 2
A resolveTemplate() 0 31 6
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Derafu: Biblioteca PHP (Núcleo).
7
 * Copyright (C) Derafu <https://www.derafu.org>
8
 *
9
 * Este programa es software libre: usted puede redistribuirlo y/o modificarlo
10
 * bajo los términos de la Licencia Pública General Affero de GNU publicada por
11
 * la Fundación para el Software Libre, ya sea la versión 3 de la Licencia, o
12
 * (a su elección) cualquier versión posterior de la misma.
13
 *
14
 * Este programa se distribuye con la esperanza de que sea útil, pero SIN
15
 * GARANTÍA ALGUNA; ni siquiera la garantía implícita MERCANTIL o de APTITUD
16
 * PARA UN PROPÓSITO DETERMINADO. Consulte los detalles de la Licencia Pública
17
 * General Affero de GNU para obtener una información más detallada.
18
 *
19
 * Debería haber recibido una copia de la Licencia Pública General Affero de GNU
20
 * junto a este programa.
21
 *
22
 * En caso contrario, consulte <http://www.gnu.org/licenses/agpl.html>.
23
 */
24
25
namespace Derafu\Lib\Core\Package\Prime\Component\Template\Worker\Renderer\Strategy;
26
27
use Derafu\Lib\Core\Foundation\Abstract\AbstractStrategy;
28
use Derafu\Lib\Core\Helper\Arr;
29
use Derafu\Lib\Core\Package\Prime\Component\Template\Contract\Renderer\Strategy\MarkdownRendererStrategyInterface;
30
use Derafu\Lib\Core\Package\Prime\Component\Template\Exception\TemplateException;
31
use Embed\Embed;
32
use League\CommonMark\MarkdownConverter;
33
use League\CommonMark\Environment\Environment;
34
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
35
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
36
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
37
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
38
use League\CommonMark\Extension\Footnote\FootnoteExtension;
39
use League\CommonMark\Extension\DescriptionList\DescriptionListExtension;
40
use League\CommonMark\Extension\Attributes\AttributesExtension;
41
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
42
use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
43
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
44
use League\CommonMark\Extension\Mention\MentionExtension;
45
use League\CommonMark\Extension\Embed\EmbedExtension;
46
use League\CommonMark\Extension\Embed\Bridge\OscaroteroEmbedAdapter;
47
use League\CommonMark\Extension\FrontMatter\FrontMatterProviderInterface;
48
49
/**
50
 * Estrategia de renderizado de archivos Markdown (.md).
51
 */
52
class MarkdownRendererStrategy extends AbstractStrategy implements MarkdownRendererStrategyInterface
53
{
54
    /**
55
     * Instancia del convertidor de markdown.
56
     *
57
     * @var MarkdownConverter
58
     */
59
    private MarkdownConverter $markdown;
60
61
    /**
62
     * Configuración del ambiente de markdown.
63
     *
64
     * @var array
65
     */
66
    private array $config = [
67
        'extensions' => [
68
            CommonMarkCoreExtension::class,
69
            GithubFlavoredMarkdownExtension::class,
70
            TableOfContentsExtension::class,
71
            HeadingPermalinkExtension::class,
72
            FootnoteExtension::class,
73
            DescriptionListExtension::class,
74
            AttributesExtension::class,
75
            SmartPunctExtension::class,
76
            ExternalLinkExtension::class,
77
            FrontMatterExtension::class,
78
            MentionExtension::class,
79
            EmbedExtension::class,
80
        ],
81
        'environment' => [
82
            'table_of_contents' => [
83
                'min_heading_level' => 2,
84
                'max_heading_level' => 3,
85
                'normalize' => 'relative',
86
                'position' => 'placeholder',
87
                'placeholder' => '[TOC]',
88
            ],
89
            'heading_permalink' => [
90
                'html_class' => 'text-decoration-none small text-muted',
91
                'id_prefix' => 'content',
92
                'fragment_prefix' => 'content',
93
                'insert' => 'before',
94
                'title' => 'Permalink',
95
                'symbol' => '<i class="fa-solid fa-link"></i> ',
96
            ],
97
            'external_link' => [
98
                //'internal_hosts' => null, // Solo el Dominio (sin esquema HTTP).
99
                'open_in_new_window' => true,
100
                'html_class' => 'external-link',
101
                'nofollow' => 'external',
102
                'noopener' => 'external',
103
                'noreferrer' => 'external',
104
            ],
105
            'mentions' => [
106
                '@' => [
107
                    'prefix' => '@',
108
                    'pattern' => '[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)',
109
                    'generator' => 'https://github.com/%s',
110
                ],
111
                '#' => [
112
                    'prefix' => '#',
113
                    'pattern' => '\d+',
114
                    'generator' => "https://github.com/sowerphp/sowerphp-framework/issues/%d",
115
                ],
116
            ],
117
            'embed' => [
118
                //'adapter' => null, // new OscaroteroEmbedAdapter()
119
                'allowed_domains' => ['youtube.com'],
120
                'fallback' => 'link',
121
                'library' => [
122
                    'oembed:query_parameters' => [
123
                        'maxwidth' => 400,
124
                        'maxheight' => 300,
125
                    ],
126
                ],
127
            ],
128
        ],
129
    ];
130
131
    /**
132
     * Rutas donde están las plantillas.
133
     *
134
     * @var array
135
     */
136
    private array $paths;
137
138
    /**
139
     * Constructor de la estrategia.
140
     *
141
     * @param string|array $paths Rutas dónde se buscarán las plantillas.
142
     */
143
    public function __construct(string|array $paths = [])
144
    {
145
        $this->paths = is_string($paths) ? [$paths] : $paths;
0 ignored issues
show
introduced by
The condition is_string($paths) is always false.
Loading history...
146
    }
147
148
    /**
149
     * Renderiza una plantilla markdown y devuelve el resultado como una cadena.
150
     *
151
     * Además, si se ha solicitado, se entregará el contenido dentro de un
152
     * layout que se renderizará con PHP.
153
     *
154
     * @param string $template Plantilla markdown que se va a renderizar.
155
     * @param array $data Datos que se pasarán a la plantilla markdown para su
156
     * uso dentro de la vista.
157
     * @return string El contenido HTML generado por la plantilla markdown.
158
     */
159
    public function render(string $template, array &$data = []): string
160
    {
161
        // Cargar plantilla.
162
        $filepath = $this->resolveTemplate($template);
163
        $content = file_get_contents($filepath);
164
165
        // Obtener instancia del renderizador markdown (biblioteca externa).
166
        $config = $data['options']['config']['markdown'] ?? [];
167
        $markdown = $this->getMarkdown($config);
168
169
        // Reemplazar las variables del archivo Markdown con los datos.
170
        foreach ($data as $key => $value) {
171
            if (
172
                is_scalar($value)
173
                || (is_object($value) && method_exists($value, '__toString'))
174
            ) {
175
                $content = preg_replace(
176
                    '/\{\{\s*' . preg_quote($key, '/') . '\s*\}\}/',
177
                    $value,
178
                    $content
179
                );
180
            }
181
        }
182
183
        // Renderizar HTML a partir del contenido markdown.
184
        $result = $markdown->convert($content);
185
        $content = $result->getContent();
186
187
        // Reemplazos por diseño.
188
        $content = '<div class="markdown-body">' . $content . '</div>';
189
        $content = str_replace(
190
            [
191
                htmlspecialchars(
192
                    $this->config['environment']['heading_permalink']['symbol']
193
                ),
194
            ],
195
            [
196
                $this->config['environment']['heading_permalink']['symbol'],
197
            ],
198
            $content
199
        );
200
201
        // Acceder a los metadatos del Front Matter para agregar variables que
202
        // se hayan definido a $data y se puedan utilizar en el worker.
203
        if ($result instanceof FrontMatterProviderInterface) {
204
            $frontMatter = $result->getFrontMatter();
205
            $data = array_merge($data, $frontMatter);
0 ignored issues
show
Bug introduced by
It seems like $frontMatter can also be of type null; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

205
            $data = array_merge($data, /** @scrutinizer ignore-type */ $frontMatter);
Loading history...
206
        }
207
208
        // Entregar el contenido renderizado.
209
        return $content;
210
    }
211
212
    /**
213
     * Entrega la instancia del renderizador markdown.
214
     *
215
     * @param array $config
216
     * @return MarkdownConverter
217
     */
218
    private function getMarkdown(array $config): MarkdownConverter
219
    {
220
        if (!isset($this->markdown)) {
221
            // Cargar configuración del motor de renderizado.
222
            $this->config = $this->loadConfigurations($config);
223
224
            // Crear ambiente (entorno).
225
            $environment = new Environment($this->config['environment']);
226
227
            // Agregar extensiones.
228
            foreach ($this->config['extensions'] as $extension) {
229
                $environment->addExtension(new $extension());
230
            }
231
232
            // Crear instancia del convertidor de markdown.
233
            $this->markdown = new MarkdownConverter($environment);
234
        }
235
236
        return $this->markdown;
237
    }
238
239
    /**
240
     * Genera la configuración para el ambiente de conversión de markdown.
241
     *
242
     * @param array $config Configuración que se unirá a la por defecto.
243
     * @return array
244
     */
245
    private function loadConfigurations(array $config = []): array
246
    {
247
        // Configuración de 'external_link'.
248
        // Se hace antes del merge de abajo por si se desea sobrescribir
249
        // mediante la configuración de la aplicación (no debería).
250
        // $config['environment']['external_link']['internal_hosts'] = '';
251
252
        // Armar configuración usando la por defecto y la de la aplicación
253
        $config = Arr::mergeRecursiveDistinct(Arr::mergeRecursiveDistinct(
254
            $this->config,
255
            $this->getOptions()->all()
256
        ), $config);
257
258
        // Configuración de 'embed'.
259
        $embedLibrary = new Embed();
260
        $embedLibrary->setSettings($config['environment']['embed']['library']);
261
        $config['environment']['embed']['adapter'] =
262
            new OscaroteroEmbedAdapter($embedLibrary)
263
        ;
264
        unset($config['environment']['embed']['library']);
265
266
        // Entregar la configuración que se cargó.
267
        return $config;
268
    }
269
270
    /**
271
     * Resuelve la plantilla que se está solicitando.
272
     *
273
     * Se encarga de:
274
     *
275
     *   - Agregar la extensión a la plantilla.
276
     *   - Entregar la plantilla si es una ruta absoluta.
277
     *   - Buscar la plantilla en las rutas.
278
     *
279
     * @param string $template
280
     * @return string
281
     */
282
    private function resolveTemplate(string $template): string
283
    {
284
        // Agregar extensión.
285
        if (!str_ends_with($template, '.md')) {
286
            $template .= '.md';
287
        }
288
289
        // Si se pasó una ruta absoluta se entrega directamente.
290
        if ($template[0] === '/') {
291
            if (!file_exists($template)) {
292
                throw new TemplateException(sprintf(
293
                    'La plantilla %s no fue encontrada.',
294
                    $template
295
                ));
296
            }
297
298
            return $template;
299
        }
300
301
        // Buscar la plantilla en diferentes rutas.
302
        foreach ($this->paths as $path) {
303
            if (file_exists($path . '/' . $template)) {
304
                return $path . '/' . $template;
305
            }
306
        }
307
308
        // No se encontró la plantilla.
309
        throw new TemplateException(sprintf(
310
            'La plantilla %s no fue encontrada. Se buscó en los directorios: %s',
311
            $template,
312
            implode(' ', $this->paths)
313
        ));
314
    }
315
}
316