Completed
Push — master ( 617ba6...7f5c80 )
by Mikael
07:52
created

CTextFilter   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 539
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Test Coverage

Coverage 88.08%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
wmc 52
c 8
b 0
f 1
lcom 2
cbo 4
dl 0
loc 539
ccs 170
cts 193
cp 0.8808
rs 7.9487

18 Methods

Rating   Name   Duplication   Size   Complexity  
B doFilter() 0 31 4
A getFilters() 0 4 1
A hasFilter() 0 4 1
A addToFrontmatter() 0 13 3
C parseFactory() 0 49 10
A parse() 0 12 2
A extractFrontMatter() 0 21 3
A jsonFrontMatter() 0 17 3
A yamlFrontMatter() 0 18 4
A getTitleFromFirstH1() 0 11 2
A bbcode2html() 0 22 1
A makeClickable() 0 10 1
A purify() 0 10 1
A markdown() 0 4 1
A nl2br() 0 4 1
A shortCode() 0 22 2
A shortCodeInit() 0 18 3
C shortCodeFigure() 0 47 9

How to fix   Complexity   

Complex Class

Complex classes like CTextFilter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CTextFilter, and based on these observations, apply Extract Interface, too.

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
        "nl2br",
22
        "purify",
23
        "titlefromh1",
24
     ];
25
26
27
28
     /**
29
      * Current document parsed.
30
      */
31
    private $current;
32
33
34
35
    /**
36
     * Call each filter.
37
     *
38
     * @deprecated deprecated since version 1.2 in favour of parse().
39
     *
40
     * @param string       $text    the text to filter.
41
     * @param string|array $filters as comma separated list of filter,
42
     *                              or filters sent in as array.
43
     *
44
     * @return string the formatted text.
45
     */
46
    public function doFilter($text, $filters)
47
    {
48
        // Define all valid filters with their callback function.
49
        $callbacks = [
50
            'bbcode'    => 'bbcode2html',
51
            'clickable' => 'makeClickable',
52
            'shortcode' => 'shortCode',
53
            'markdown'  => 'markdown',
54
            'nl2br'     => 'nl2br',
55
            'purify'    => 'purify',
56
        ];
57
58
        // Make an array of the comma separated string $filters
59
        if (is_array($filters)) {
60
            $filter = $filters;
61
        } else {
62
            $filters = strtolower($filters);
63
            $filter = preg_replace('/\s/', '', explode(',', $filters));
64
        }
65
66
        // For each filter, call its function with the $text as parameter.
67
        foreach ($filter as $key) {
68
69
            if (!isset($callbacks[$key])) {
70
                throw new Exception("The filter '$filters' is not a valid filter string due to '$key'.");
71
            }
72
            $text = call_user_func_array([$this, $callbacks[$key]], [$text]);
73
        }
74
75
        return $text;
76
    }
77
78
79
80
    /**
81
     * Return an array of all filters supported.
82
     *
83
     * @return array with strings of filters supported.
84
     */
85 1
    public function getFilters()
86
    {
87 1
        return $this->filters;
88
    }
89
90
91
92
    /**
93
     * Check if filter is supported.
94
     *
95
     * @param string $filter to use.
96
     *
97
     * @throws mos/TextFilter/Exception  when filter does not exists.
98
     *
99
     * @return boolean true if filter exists, false othwerwise.
100
     */
101 2
    public function hasFilter($filter)
102
    {
103 2
        return in_array($filter, $this->filters);
104
    }
105
106
107
108
    /**
109
     * Add array items to frontmatter.
110
     *
111
     * @param array|null $matter key value array with items to add
112
     *                           or null if empty.
113
     *
114
     * @return $this
115
     */
116 3
    private function addToFrontmatter($matter)
117 1
    {
118 3
        if (empty($matter)) {
119 2
            return $this;
120
        }
121
122 2
        if (is_null($this->current->frontmatter)) {
123 2
            $this->current->frontmatter = [];
124 2
        }
125
126 2
        $this->current->frontmatter = array_merge_recursive($this->current->frontmatter, $matter);
127 2
        return $this;
128
    }
129
130
131
132
    /**
133
     * Call a specific filter and store its details.
134
     *
135
     * @param string $filter to use.
136
     *
137
     * @throws mos/TextFilter/Exception  when filter does not exists.
138
     *
139
     * @return string the formatted text.
140
     */
141 4
    private function parseFactory($filter)
142
    {
143
        // Define single tasks filter with a callback.
144
        $callbacks = [
145 4
            "bbcode"    => "bbcode2html",
146 4
            "clickable" => "makeClickable",
147 4
            "shortcode" => "shortCode",
148 4
            "markdown"  => "markdown",
149 4
            "nl2br"     => "nl2br",
150 4
            "purify"    => "purify",
151 4
        ];
152
153
        // Do the specific filter
154 4
        $text = $this->current->text;
155 1
        switch ($filter) {
156 4
            case "jsonfrontmatter":
157 2
                $res = $this->jsonFrontMatter($text);
158 3
                $this->current->text = $res["text"];
159 2
                $this->addToFrontmatter($res["frontmatter"]);
160 2
                break;
161
162 2
            case "yamlfrontmatter":
163
                $res = $this->yamlFrontMatter($text);
164
                $this->current->text = $res["text"];
165
                $this->addToFrontmatter($res["frontmatter"]);
166
                break;
167
168 2
            case "titlefromh1":
169 1
                $title = $this->getTitleFromFirstH1($text);
170 1
                $this->current->text = $text;
171 1
                $this->addToFrontmatter(["title" => $title]);
172 1
                break;
173
174 1
            case "bbcode":
175 1
            case "clickable":
176 1
            case "shortcode":
177 1
            case "markdown":
178 1
            case "nl2br":
179 1
            case "purify":
180 1
                $this->current->text = call_user_func_array(
181 1
                    [$this, $callbacks[$filter]],
182 1
                    [$text]
183 1
                );
184 1
                break;
185
186
            default:
187
                throw new Exception("The filter '$filter' is not a valid filter     string.");
188
        }
189 4
    }
190
191
192
193
    /**
194
     * Call each filter and return array with details of the formatted content.
195
     *
196
     * @param string $text   the text to filter.
197
     * @param array  $filter array of filters to use.
198
     *
199
     * @throws mos/TextFilter/Exception  when filterd does not exists.
200
     *
201
     * @return array with the formatted text and additional details.
202
     */
203 4
    public function parse($text, $filter)
204
    {
205 4
        $this->current = new \stdClass();
206 4
        $this->current->frontmatter = null;
207 4
        $this->current->text = $text;
208
209 4
        foreach ($filter as $key) {
210 4
            $this->parseFactory($key);
211 4
        }
212
213 4
        return $this->current;
214
    }
215
216
217
218
    /**
219
     * Extract front matter from text.
220
     *
221
     * @param string $text       the text to be parsed.
222
     * @param string $startToken the start token.
223
     * @param string $stopToken  the stop token.
224
     *
225
     * @return array with the formatted text and the front matter.
226
     */
227 2
    private function extractFrontMatter($text, $startToken, $stopToken)
228
    {
229 2
        $tokenLength = strlen($startToken);
230
231 2
        $start = strpos($text, $startToken);
232
        
233 2
        $frontmatter = null;
234 2
        if ($start !== false) {
235 2
            $stop = strpos($text, $stopToken, $tokenLength - 1);
236
237 2
            if ($stop !== false) {
238 1
                $length = $stop - ($start + $tokenLength);
239
240 1
                $frontmatter = substr($text, $start + $tokenLength, $length);
241 1
                $textStart = substr($text, 0, $start);
242 1
                $text = $textStart . substr($text, $stop + $tokenLength);
243 1
            }
244 2
        }
245
246 2
        return [$text, $frontmatter];
247
    }
248
249
250
251
    /**
252
     * Extract JSON front matter from text.
253
     *
254
     * @param string $text the text to be parsed.
255
     *
256
     * @return array with the formatted text and the front matter.
257
     */
258 2
    public function jsonFrontMatter($text)
259
    {
260 2
        list($text, $frontmatter) = $this->extractFrontMatter($text, "{{{\n", "}}}\n");
261
262 2
        if (!empty($frontmatter)) {
263 1
            $frontmatter = json_decode($frontmatter, true);
264
265 1
            if (is_null($frontmatter)) {
266
                throw new Exception("Failed parsing JSON frontmatter.");
267
            }
268 1
        }
269
270
        return [
271 2
            "text" => $text,
272
            "frontmatter" => $frontmatter
273 2
        ];
274
    }
275
276
277
278
    /**
279
     * Extract YAML front matter from text.
280
     *
281
     * @param string $text the text to be parsed.
282
     *
283
     * @return array with the formatted text and the front matter.
284
     */
285
    public function yamlFrontMatter($text)
286
    {
287
        $needle = "---\n";
288
        list($text, $frontmatter) = $this->extractFrontMatter($text, $needle, $needle);
289
290
        if (function_exists("yaml_parse") && !empty($frontmatter)) {
291
            $frontmatter = yaml_parse($needle . $frontmatter);
292
293
            if ($frontmatter === false) {
294
                throw new Exception("Failed parsing YAML frontmatter.");
295
            }
296
        }
297
298
        return [
299
            "text" => $text,
300
            "frontmatter" => $frontmatter
301
        ];
302
    }
303
304
305
306
    /**
307
     * Get the title from the first H1.
308
     *
309
     * @param string $text the text to be parsed.
310
     *
311
     * @return string|null with the title, if its found.
312
     */
313 1
    public function getTitleFromFirstH1($text)
314
    {
315 1
        $matches = [];
316 1
        $title = null;
317
318 1
        if (preg_match("#<h1.*?>(.*)</h1>#", $text, $matches)) {
319 1
            $title = strip_tags($matches[1]);
320 1
        }
321
322 1
        return $title;
323
    }
324
325
326
327
    /**
328
     * Helper, BBCode formatting converting to HTML.
329
     *
330
     * @param string $text The text to be converted.
331
     *
332
     * @return string the formatted text.
333
     *
334
     * @link http://dbwebb.se/coachen/reguljara-uttryck-i-php-ger-bbcode-formattering
335
     */
336 3
    public function bbcode2html($text)
337
    {
338
        $search = [
339 3
            '/\[b\](.*?)\[\/b\]/is',
340 3
            '/\[i\](.*?)\[\/i\]/is',
341 3
            '/\[u\](.*?)\[\/u\]/is',
342 3
            '/\[img\](https?.*?)\[\/img\]/is',
343 3
            '/\[url\](https?.*?)\[\/url\]/is',
344
            '/\[url=(https?.*?)\](.*?)\[\/url\]/is'
345 3
        ];
346
347
        $replace = [
348 3
            '<strong>$1</strong>',
349 3
            '<em>$1</em>',
350 3
            '<u>$1</u>',
351 3
            '<img src="$1" />',
352 3
            '<a href="$1">$1</a>',
353
            '<a href="$1">$2</a>'
354 3
        ];
355
356 3
        return preg_replace($search, $replace, $text);
357
    }
358
359
360
361
    /**
362
     * Make clickable links from URLs in text.
363
     *
364
     * @param string $text the text that should be formatted.
365
     *
366
     * @return string with formatted anchors.
367
     *
368
     * @link http://dbwebb.se/coachen/lat-php-funktion-make-clickable-automatiskt-skapa-klickbara-lankar
369
     */
370 1
    public function makeClickable($text)
371
    {
372 1
        return preg_replace_callback(
373 1
            '#\b(?<![href|src]=[\'"])https?://[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))#',
374
            function ($matches) {
375 1
                return "<a href='{$matches[0]}'>{$matches[0]}</a>";
376 1
            },
377
            $text
378 1
        );
379
    }
380
381
382
383
    /**
384
     * Format text according to HTML Purifier.
385
     *
386
     * @param string $text that should be formatted.
387
     *
388
     * @return string as the formatted html-text.
389
     */
390 1
    public function purify($text)
391
    {
392 1
        $config   = \HTMLPurifier_Config::createDefault();
393 1
        $config->set("Cache.DefinitionImpl", null);
394
        //$config->set('Cache.SerializerPath', '/home/user/absolute/path');
395
396 1
        $purifier = new \HTMLPurifier($config);
397
    
398 1
        return $purifier->purify($text);
399
    }
400
401
402
403
    /**
404
     * Format text according to Markdown syntax.
405
     *
406
     * @param string $text the text that should be formatted.
407
     *
408
     * @return string as the formatted html-text.
409
     */
410 6
    public function markdown($text)
411
    {
412 6
        return \Michelf\MarkdownExtra::defaultTransform($text);
413
    }
414
415
416
417
    /**
418
     * For convenience access to nl2br
419
     *
420
     * @param string $text text to be converted.
421
     *
422
     * @return string the formatted text.
423
     */
424 1
    public function nl2br($text)
425
    {
426 1
        return nl2br($text);
427
    }
428
429
430
431
    /**
432
     * Shortcode to to quicker format text as HTML.
433
     *
434
     * @param string $text text to be converted.
435
     *
436
     * @return string the formatted text.
437
     */
438 1
    public function shortCode($text)
439
    {
440
        $patterns = [
441 1
            '/\[(FIGURE)[\s+](.+)\]/',
442 1
        ];
443
444 1
        return preg_replace_callback(
445 1
            $patterns,
446 1
            function ($matches) {
447 1
                switch ($matches[1]) {
448
449 1
                    case 'FIGURE':
450 1
                        return self::ShortCodeFigure($matches[2]);
451
                        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...
452
453
                    default:
454
                        return "{$matches[1]} is unknown shortcode.";
455
                }
456 1
            },
457
            $text
458 1
        );
459
    }
460
461
462
463
    /**
464
     * Init shortcode handling by preparing the option list to an array, for those using arguments.
465
     *
466
     * @param string $options for the shortcode.
467
     *
468
     * @return array with all the options.
469
     */
470 1
    public static function shortCodeInit($options)
471
    {
472 1
        preg_match_all('/[a-zA-Z0-9]+="[^"]+"|\S+/', $options, $matches);
473
474 1
        $res = array();
475 1
        foreach ($matches[0] as $match) {
476 1
            $pos = strpos($match, '=');
477 1
            if ($pos === false) {
478
                $res[$match] = true;
479
            } else {
480 1
                $key = substr($match, 0, $pos);
481 1
                $val = trim(substr($match, $pos+1), '"');
482 1
                $res[$key] = $val;
483
            }
484 1
        }
485
486 1
        return $res;
487
    }
488
489
490
491
    /**
492
     * Shortcode for <figure>.
493
     *
494
     * Usage example: [FIGURE src="img/home/me.jpg" caption="Me" alt="Bild på mig" nolink="nolink"]
495
     *
496
     * @param string $options for the shortcode.
497
     *
498
     * @return array with all the options.
499
     */
500 1
    public static function shortCodeFigure($options)
501
    {
502
        // Merge incoming options with default and expose as variables
503 1
        $options= array_merge(
504
            [
505 1
                'id' => null,
506 1
                'class' => null,
507 1
                'src' => null,
508 1
                'title' => null,
509 1
                'alt' => null,
510 1
                'caption' => null,
511 1
                'href' => null,
512 1
                'nolink' => false,
513 1
            ],
514 1
            self::ShortCodeInit($options)
515 1
        );
516 1
        extract($options, EXTR_SKIP);
517
518 1
        $id = $id ? " id='$id'" : null;
519 1
        $class = $class ? " class='figure $class'" : " class='figure'";
520 1
        $title = $title ? " title='$title'" : null;
521
522 1
        if (!$alt && $caption) {
523 1
            $alt = $caption;
524 1
        }
525
526 1
        if (!$href) {
527 1
            $pos = strpos($src, '?');
528 1
            $href = $pos ? substr($src, 0, $pos) : $src;
529 1
        }
530
531 1
        $start = null;
532 1
        $end = null;
533 1
        if (!$nolink) {
534 1
            $start = "<a href='{$href}'>";
535 1
            $end = "</a>";
536 1
        }
537
538
        $html = <<<EOD
539 1
<figure{$id}{$class}>
540 1
{$start}<img src='{$src}' alt='{$alt}'{$title}/>{$end}
541 1
<figcaption markdown=1>{$caption}</figcaption>
542 1
</figure>
543 1
EOD;
544
545 1
        return $html;
546
    }
547
}
548