Completed
Push — master ( e2a82f...9067e5 )
by Mikael
02:02
created

CTextFilter::nl2br()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 2
1
<?php
2
3
namespace Mos\TextFilter;
4
5
/**
6
 * Filter and format content.
7
 *
8
 */
9
class CTextFilter
10
{
11
    /**
12
     * Supported filters.
13
     */
14
    private $filters = [
15
        "jsonfrontmatter",
16
        "yamlfrontmatter",
17
        "bbcode",
18
        "clickable",
19
        "shortcode",
20 10
        "markdown",
21
        "nl2br",
22
        "purify",
23
     ];
24 10
25 10
26 10
27 10
     /**
28 10
      * Current document parsed.
29 10
      */
30
    private $current;
31
32 10
33 2
34 2
    /**
35 8
     * Call each filter.
36 8
     *
37
     * @deprecated deprecated since version 1.2 in favour of parse().
38
     *
39
     * @param string       $text    the text to filter.
40 10
     * @param string|array $filters as comma separated list of filter,
41
     *                              or filters sent in as array.
42 10
     *
43 1
     * @return string the formatted text.
44
     */
45 9
    public function doFilter($text, $filters)
46 9
    {
47
        // Define all valid filters with their callback function.
48 9
        $callbacks = [
49
            'bbcode'    => 'bbcode2html',
50
            'clickable' => 'makeClickable',
51
            'shortcode' => 'shortCode',
52
            'markdown'  => 'markdown',
53
            'nl2br'     => 'nl2br',
54
            'purify'    => 'purify',
55
        ];
56
57
        // Make an array of the comma separated string $filters
58
        if (is_array($filters)) {
59
            $filter = $filters;
60
        } else {
61
            $filters = strtolower($filters);
62 3
            $filter = preg_replace('/\s/', '', explode(',', $filters));
63
        }
64
65 3
        // For each filter, call its function with the $text as parameter.
66 3
        foreach ($filter as $key) {
67 3
68 3
            if (!isset($callbacks[$key])) {
69 3
                throw new Exception("The filter '$filters' is not a valid filter string due to '$key'.");
70
            }
71 3
            $text = call_user_func_array([$this, $callbacks[$key]], [$text]);
72
        }
73
74 3
        return $text;
75 3
    }
76 3
77 3
78 3
79
    /**
80 3
     * Return an array of all filters supported.
81
     *
82 3
     * @return array with strings of filters supported.
83
     */
84
    public function getFilters()
85
    {
86
        return $this->filters;
87
    }
88
89
90
91
    /**
92
     * Check if filter is supported.
93
     *
94
     * @param string $filter to use.
95
     *
96 1
     * @throws mos/TextFilter/Exception  when filter does not exists.
97
     *
98 1
     * @return boolean true if filter exists, false othwerwise.
99 1
     */
100
    public function hasFilter($filter)
101 1
    {
102 1
        return in_array($filter, $this->filters);
103
    }
104 1
105
106
107
    /**
108
     * Call a specific filter and store its details.
109
     *
110
     * @param string $filter to use.
111
     *
112
     * @throws mos/TextFilter/Exception  when filter does not exists.
113
     *
114
     * @return string the formatted text.
115
     */
116
    private function parseFactory($filter)
117
    {
118 5
        // Define single tasks filter with a callback.
119
        $callbacks = [
120 5
            "bbcode"    => "bbcode2html",
121
            "clickable" => "makeClickable",
122
            "shortcode" => "shortCode",
123
            "markdown"  => "markdown",
124
            "nl2br"     => "nl2br",
125
            "purify"    => "purify",
126
        ];
127
128
        // Do the specific filter
129
        $text = $this->current->text;
130
        switch ($filter) {
131
            case "jsonfrontmatter":
132 1
                $res = $this->jsonFrontMatter($text);
133
                $this->current->text        = $res["text"];
134 1
                $this->current->frontmatter = $res["frontmatter"];
135
                break;
136
137
            case "yamlfrontmatter":
138
                $res = $this->yamlFrontMatter($text);
139
                $this->current->text        = $res["text"];
140
                $this->current->frontmatter = $res["frontmatter"];
141
                break;
142
143
            case "bbcode":
144
            case "clickable":
145
            case "shortcode":
146 1
            case "markdown":
147
            case "nl2br":
148
            case "purify":
149 1
                $this->current->text = call_user_func_array(
150 1
                    [$this, $callbacks[$filter]],
151
                    [$text]
152 1
                );
153 1
                break;
154 1
155 1
            default:
156
                throw new Exception("The filter '$filter' is not a valid filter     string.");
157 1
        }
158 1
    }
159
160
161
162
    /**
163
     * Call each filter and return array with details of the formatted content.
164 1
     *
165
     * @param string $text   the text to filter.
166 1
     * @param array  $filter array of filters to use.
167
     *
168
     * @throws mos/TextFilter/Exception  when filterd does not exists.
169
     *
170
     * @return array with the formatted text and additional details.
171
     */
172
    public function parse($text, $filter)
173
    {
174
        $this->current = new \stdClass();
175
        $this->current->frontmatter = null;
176
        $this->current->text = $text;
177
178 1
        foreach ($filter as $key) {
179
            $this->parseFactory($key);
180 1
        }
181
182 1
        return $this->current;
183 1
    }
184 1
185 1
186
187
    /**
188 1
     * Extract front matter from text.
189 1
     *
190 1
     * @param string $text       the text to be parsed.
191
     * @param string $startToken the start token.
192 1
     * @param string $stopToken  the stop token.
193
     *
194 1
     * @return array with the formatted text and the front matter.
195
     */
196
    private function extractFrontMatter($text, $startToken, $stopToken)
197
    {
198
        $tokenLength = strlen($startToken);
199
200
        $start = strpos($text, $startToken);
201
        
202
        $frontmatter = null;
203
        if ($start !== false) {
204
            $stop = strpos($text, $stopToken, $tokenLength - 1);
205
206
            if ($stop !== false) {
207
                $length = $stop - ($start + $tokenLength);
208 1
209
                $frontmatter = substr($text, $start + $tokenLength, $length);
210
                $textStart = substr($text, 0, $start);
211 1
                $text = $textStart . substr($text, $stop + $tokenLength);
212
            }
213 1
        }
214 1
215 1
        return [$text, $frontmatter
216 1
        ];
217 1
    }
218 1
219 1
220 1
221 1
    /**
222 1
     * Extract JSON front matter from text.
223 1
     *
224 1
     * @param string $text the text to be parsed.
225
     *
226 1
     * @return array with the formatted text and the front matter.
227 1
     */
228 1
    public function jsonFrontMatter($text)
229
    {
230 1
        list($text, $frontmatter) = $this->extractFrontMatter($text, "{{{\n", "}}}\n");
231 1
232 1
        if (!empty($frontmatter)) {
233
            $frontmatter = json_decode($frontmatter, true);
234 1
235 1
            if (is_null($frontmatter)) {
236 1
                throw new Exception("Failed parsing JSON frontmatter.");
237 1
            }
238
        }
239 1
240 1
        return [
241 1
            "text" => $text,
242 1
            "frontmatter" => $frontmatter
243 1
        ];
244 1
    }
245
246
247 1
248 1
    /**
249 1
     * Extract YAML front matter from text.
250 1
     *
251 1
     * @param string $text the text to be parsed.
252
     *
253 1
     * @return array with the formatted text and the front matter.
254
     */
255
    public function yamlFrontMatter($text)
256
    {
257
        $needle = "---\n";
258
        list($text, $frontmatter) = $this->extractFrontMatter($text, $needle, $needle);
259
260
        if (function_exists("yaml_parse") && !empty($frontmatter)) {
261
            $frontmatter = yaml_parse($needle . $frontmatter);
262
263
            if ($frontmatter === false) {
264
                throw new Exception("Failed parsing YAML frontmatter.");
265
            }
266
        }
267
268
        return [
269
            "text" => $text,
270
            "frontmatter" => $frontmatter
271
        ];
272
    }
273
274
275
276
    /**
277
     * Helper, BBCode formatting converting to HTML.
278
     *
279
     * @param string $text The text to be converted.
280
     *
281
     * @return string the formatted text.
282
     *
283
     * @link http://dbwebb.se/coachen/reguljara-uttryck-i-php-ger-bbcode-formattering
284
     */
285
    public function bbcode2html($text)
286
    {
287
        $search = [
288
            '/\[b\](.*?)\[\/b\]/is',
289
            '/\[i\](.*?)\[\/i\]/is',
290
            '/\[u\](.*?)\[\/u\]/is',
291
            '/\[img\](https?.*?)\[\/img\]/is',
292
            '/\[url\](https?.*?)\[\/url\]/is',
293
            '/\[url=(https?.*?)\](.*?)\[\/url\]/is'
294
        ];
295
296
        $replace = [
297
            '<strong>$1</strong>',
298
            '<em>$1</em>',
299
            '<u>$1</u>',
300
            '<img src="$1" />',
301
            '<a href="$1">$1</a>',
302
            '<a href="$1">$2</a>'
303
        ];
304
305
        return preg_replace($search, $replace, $text);
306
    }
307
308
309
310
    /**
311
     * Make clickable links from URLs in text.
312
     *
313
     * @param string $text the text that should be formatted.
314
     *
315
     * @return string with formatted anchors.
316
     *
317
     * @link http://dbwebb.se/coachen/lat-php-funktion-make-clickable-automatiskt-skapa-klickbara-lankar
318
     */
319
    public function makeClickable($text)
320
    {
321
        return preg_replace_callback(
322
            '#\b(?<![href|src]=[\'"])https?://[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))#',
323
            function ($matches) {
324
                return "<a href='{$matches[0]}'>{$matches[0]}</a>";
325
            },
326
            $text
327
        );
328
    }
329
330
331
332
    /**
333
     * Format text according to HTML Purifier.
334
     *
335
     * @param string $text that should be formatted.
336
     *
337
     * @return string as the formatted html-text.
338
     */
339
    public function purify($text)
340
    {
341
        $config   = \HTMLPurifier_Config::createDefault();
342
        $config->set("Cache.DefinitionImpl", null);
343
        //$config->set('Cache.SerializerPath', '/home/user/absolute/path');
344
345
        $purifier = new \HTMLPurifier($config);
346
    
347
        return $purifier->purify($text);
348
    }
349
350
351
352
    /**
353
     * Format text according to Markdown syntax.
354
     *
355
     * @param string $text the text that should be formatted.
356
     *
357
     * @return string as the formatted html-text.
358
     */
359
    public function markdown($text)
360
    {
361
        return \Michelf\MarkdownExtra::defaultTransform($text);
362
    }
363
364
365
366
    /**
367
     * For convenience access to nl2br
368
     *
369
     * @param string $text text to be converted.
370
     *
371
     * @return string the formatted text.
372
     */
373
    public function nl2br($text)
374
    {
375
        return nl2br($text);
376
    }
377
378
379
380
    /**
381
     * Shortcode to to quicker format text as HTML.
382
     *
383
     * @param string $text text to be converted.
384
     *
385
     * @return string the formatted text.
386
     */
387
    public function shortCode($text)
388
    {
389
        $patterns = [
390
            '/\[(FIGURE)[\s+](.+)\]/',
391
        ];
392
393
        return preg_replace_callback(
394
            $patterns,
395
            function ($matches) {
396
                switch ($matches[1]) {
397
398
                    case 'FIGURE':
399
                        return self::ShortCodeFigure($matches[2]);
400
                        break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
401
402
                    default:
403
                        return "{$matches[1]} is unknown shortcode.";
404
                }
405
            },
406
            $text
407
        );
408
    }
409
410
411
412
    /**
413
     * Init shortcode handling by preparing the option list to an array, for those using arguments.
414
     *
415
     * @param string $options for the shortcode.
416
     *
417
     * @return array with all the options.
418
     */
419
    public static function shortCodeInit($options)
420
    {
421
        preg_match_all('/[a-zA-Z0-9]+="[^"]+"|\S+/', $options, $matches);
422
423
        $res = array();
424
        foreach ($matches[0] as $match) {
425
            $pos = strpos($match, '=');
426
            if ($pos === false) {
427
                $res[$match] = true;
428
            } else {
429
                $key = substr($match, 0, $pos);
430
                $val = trim(substr($match, $pos+1), '"');
431
                $res[$key] = $val;
432
            }
433
        }
434
435
        return $res;
436
    }
437
438
439
440
    /**
441
     * Shortcode for <figure>.
442
     *
443
     * Usage example: [FIGURE src="img/home/me.jpg" caption="Me" alt="Bild på mig" nolink="nolink"]
444
     *
445
     * @param string $options for the shortcode.
446
     *
447
     * @return array with all the options.
448
     */
449
    public static function shortCodeFigure($options)
450
    {
451
        // Merge incoming options with default and expose as variables
452
        $options= array_merge(
453
            [
454
                'id' => null,
455
                'class' => null,
456
                'src' => null,
457
                'title' => null,
458
                'alt' => null,
459
                'caption' => null,
460
                'href' => null,
461
                'nolink' => false,
462
            ],
463
            self::ShortCodeInit($options)
464
        );
465
        extract($options, EXTR_SKIP);
466
467
        $id = $id ? " id='$id'" : null;
468
        $class = $class ? " class='figure $class'" : " class='figure'";
469
        $title = $title ? " title='$title'" : null;
470
471
        if (!$alt && $caption) {
472
            $alt = $caption;
473
        }
474
475
        if (!$href) {
476
            $pos = strpos($src, '?');
477
            $href = $pos ? substr($src, 0, $pos) : $src;
478
        }
479
480
        $start = null;
481
        $end = null;
482
        if (!$nolink) {
483
            $start = "<a href='{$href}'>";
484
            $end = "</a>";
485
        }
486
487
        $html = <<<EOD
488
<figure{$id}{$class}>
489
{$start}<img src='{$src}' alt='{$alt}'{$title}/>{$end}
490
<figcaption markdown=1>{$caption}</figcaption>
491
</figure>
492
EOD;
493
494
        return $html;
495
    }
496
}
497