Completed
Push — master ( 9d5144...4f67af )
by Mikael
03:33
created

CTextFilter::getFilters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
409
        //$geshi->set_header_type(GESHI_HEADER_PRE_VALID);
410
        //$geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
411
        //echo "<pre>", $geshi->get_stylesheet(false) , "</pre>"; exit;
412
413 2
        return $geshi->parse_code();
414
    }
415
416
417
418
    /**
419
     * Format text according to HTML Purifier.
420
     *
421
     * @param string $text that should be formatted.
422
     *
423
     * @return string as the formatted html-text.
424
     */
425 1
    public function purify($text)
426
    {
427 1
        $config   = \HTMLPurifier_Config::createDefault();
428 1
        $config->set("Cache.DefinitionImpl", null);
429
        //$config->set('Cache.SerializerPath', '/home/user/absolute/path');
430
431 1
        $purifier = new \HTMLPurifier($config);
432
    
433 1
        return $purifier->purify($text);
434
    }
435
436
437
438
    /**
439
     * Format text according to Markdown syntax.
440
     *
441
     * @param string $text the text that should be formatted.
442
     *
443
     * @return string as the formatted html-text.
444
     */
445 7
    public function markdown($text)
446
    {
447 7
        return \Michelf\MarkdownExtra::defaultTransform($text);
448
    }
449
450
451
452
    /**
453
     * For convenience access to nl2br
454
     *
455
     * @param string $text text to be converted.
456
     *
457
     * @return string the formatted text.
458
     */
459 1
    public function nl2br($text)
460
    {
461 1
        return nl2br($text);
462
    }
463
464
465
466
    /**
467
     * Shortcode to to quicker format text as HTML.
468
     *
469
     * @param string $text text to be converted.
470
     *
471
     * @return string the formatted text.
472
     */
473 2
    public function shortCode($text)
474
    {
475
        /* Needs PHP 7
476
        $patternsAndCallbacks = [
477
            "/\[(FIGURE)[\s+](.+)\]/" => function ($match) {
478
                return self::ShortCodeFigure($matches[2]);
479
            },
480
            "/(```([\w]*))\n([^`]*)```[\n]{1}/s" => function ($match) {
481
                return $this->syntaxHighlightGeSHi($matches[3], $matches[2]);
482
            },
483
        ];
484
485
        return preg_replace_callback_array($patternsAndCallbacks, $text);
486
        */
487
488
        $patterns = [
489 2
            "/\[(FIGURE)[\s+](.+)\]/",
490 2
            "/(```)([\w]*)\n([^`]*)```[\n]{1}/s",
491 2
        ];
492
493 2
        return preg_replace_callback(
494 2
            $patterns,
495 2
            function ($matches) {
496 2
                switch ($matches[1]) {
497
498 2
                    case "FIGURE":
499 1
                        return self::ShortCodeFigure($matches[2]);
500
                    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...
501
502 1
                    case "```":
503 1
                        return $this->syntaxHighlightGeSHi($matches[3], $matches[2]);
504
                    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...
505
506
                    default:
507
                        return "{$matches[1]} is unknown shortcode.";
508
                }
509 2
            },
510
            $text
511 2
        );
512
    }
513
514
515
516
    /**
517
     * Init shortcode handling by preparing the option list to an array, for those using arguments.
518
     *
519
     * @param string $options for the shortcode.
520
     *
521
     * @return array with all the options.
522
     */
523 1
    public static function shortCodeInit($options)
524
    {
525 1
        preg_match_all('/[a-zA-Z0-9]+="[^"]+"|\S+/', $options, $matches);
526
527 1
        $res = array();
528 1
        foreach ($matches[0] as $match) {
529 1
            $pos = strpos($match, '=');
530 1
            if ($pos === false) {
531
                $res[$match] = true;
532
            } else {
533 1
                $key = substr($match, 0, $pos);
534 1
                $val = trim(substr($match, $pos+1), '"');
535 1
                $res[$key] = $val;
536
            }
537 1
        }
538
539 1
        return $res;
540
    }
541
542
543
544
    /**
545
     * Shortcode for <figure>.
546
     *
547
     * Usage example: [FIGURE src="img/home/me.jpg" caption="Me" alt="Bild på mig" nolink="nolink"]
548
     *
549
     * @param string $options for the shortcode.
550
     *
551
     * @return array with all the options.
552
     */
553 1
    public static function shortCodeFigure($options)
554
    {
555
        // Merge incoming options with default and expose as variables
556 1
        $options= array_merge(
557
            [
558 1
                'id' => null,
559 1
                'class' => null,
560 1
                'src' => null,
561 1
                'title' => null,
562 1
                'alt' => null,
563 1
                'caption' => null,
564 1
                'href' => null,
565 1
                'nolink' => false,
566 1
            ],
567 1
            self::ShortCodeInit($options)
568 1
        );
569 1
        extract($options, EXTR_SKIP);
570
571 1
        $id = $id ? " id='$id'" : null;
572 1
        $class = $class ? " class='figure $class'" : " class='figure'";
573 1
        $title = $title ? " title='$title'" : null;
574
575 1
        if (!$alt && $caption) {
576 1
            $alt = $caption;
577 1
        }
578
579 1
        if (!$href) {
580 1
            $pos = strpos($src, '?');
581 1
            $href = $pos ? substr($src, 0, $pos) : $src;
582 1
        }
583
584 1
        $start = null;
585 1
        $end = null;
586 1
        if (!$nolink) {
587 1
            $start = "<a href='{$href}'>";
588 1
            $end = "</a>";
589 1
        }
590
591
        $html = <<<EOD
592 1
<figure{$id}{$class}>
593 1
{$start}<img src='{$src}' alt='{$alt}'{$title}/>{$end}
594 1
<figcaption markdown=1>{$caption}</figcaption>
595 1
</figure>
596 1
EOD;
597
598 1
        return $html;
599
    }
600
}
601