Completed
Push — master ( 4dba6c...bdbb24 )
by Mikael
02:08
created

CTextFilter   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 533
Duplicated Lines 6.38 %

Coupling/Cohesion

Components 3
Dependencies 7

Test Coverage

Coverage 86.05%

Importance

Changes 19
Bugs 0 Features 4
Metric Value
wmc 50
c 19
b 0
f 4
lcom 3
cbo 7
dl 34
loc 533
ccs 148
cts 172
cp 0.8605
rs 8.6206

18 Methods

Rating   Name   Duplication   Size   Complexity  
B doFilter() 0 31 4
A setMeta() 0 4 1
A getFilters() 0 4 1
A hasFilter() 0 4 1
A addToFrontmatter() 0 13 3
C parseFactory() 0 55 13
A parse() 0 14 2
A addExcerpt() 0 6 1
C extractFrontMatter() 0 27 7
A jsonFrontMatter() 17 17 3
A yamlFrontMatter() 17 17 4
A getTitleFromFirstH1() 0 11 2
A bbcode2html() 0 22 1
A makeClickable() 0 10 1
A syntaxHighlightGeSHi() 0 17 3
A purify() 0 10 1
A markdown() 0 4 1
A nl2br() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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
    use TTextUtilities,
12
        TShortcode;
13
14
15
16
    /**
17
     * Supported filters.
18
     */
19
    private $filters = [
20
        "jsonfrontmatter",
21
        "yamlfrontmatter",
22
        "bbcode",
23
        "clickable",
24
        "shortcode",
25
        "markdown",
26
        "geshi",
27
        "nl2br",
28
        "purify",
29
        "titlefromh1",
30
        "anchor4Header",
31
     ];
32
33
34
35
     /**
36
      * Current document parsed.
37
      */
38
    private $current;
39
40
41
42
    /**
43
     * Hold meta information for filters to use.
44
     */
45
    private $meta = [];
46
47
48
49
    /**
50
     * Call each filter.
51
     *
52
     * @deprecated deprecated since version 1.2 in favour of parse().
53
     *
54
     * @param string       $text    the text to filter.
55
     * @param string|array $filters as comma separated list of filter,
56
     *                              or filters sent in as array.
57
     *
58
     * @return string the formatted text.
59
     */
60
    public function doFilter($text, $filters)
61
    {
62
        // Define all valid filters with their callback function.
63
        $callbacks = [
64
            'bbcode'    => 'bbcode2html',
65
            'clickable' => 'makeClickable',
66
            'shortcode' => 'shortCode',
67
            'markdown'  => 'markdown',
68
            'nl2br'     => 'nl2br',
69
            'purify'    => 'purify',
70
        ];
71
72
        // Make an array of the comma separated string $filters
73
        if (is_array($filters)) {
74
            $filter = $filters;
75
        } else {
76
            $filters = strtolower($filters);
77
            $filter = preg_replace('/\s/', '', explode(',', $filters));
78
        }
79
80
        // For each filter, call its function with the $text as parameter.
81
        foreach ($filter as $key) {
82
83
            if (!isset($callbacks[$key])) {
84
                throw new Exception("The filter '$filters' is not a valid filter string due to '$key'.");
85
            }
86
            $text = call_user_func_array([$this, $callbacks[$key]], [$text]);
87
        }
88
89
        return $text;
90
    }
91 1
92
93 1
94
    /**
95
     * Set meta information that some filters can use.
96
     *
97
     * @param array $meta values for filters to use.
98
     *
99
     * @return void
100
     */
101
    public function setMeta($meta)
102
    {
103
        return $this->meta = $meta;
104
    }
105
106
107 2
108
    /**
109 2
     * Return an array of all filters supported.
110
     *
111
     * @return array with strings of filters supported.
112
     */
113
    public function getFilters()
114
    {
115
        return $this->filters;
116
    }
117
118
119
120
    /**
121
     * Check if filter is supported.
122 3
     *
123
     * @param string $filter to use.
124 3
     *
125 2
     * @throws mos/TextFilter/Exception  when filter does not exists.
126
     *
127
     * @return boolean true if filter exists, false othwerwise.
128 2
     */
129 2
    public function hasFilter($filter)
130 2
    {
131
        return in_array($filter, $this->filters);
132 2
    }
133 2
134
135
136
    /**
137
     * Add array items to frontmatter.
138
     *
139
     * @param array|null $matter key value array with items to add
140
     *                           or null if empty.
141
     *
142
     * @return $this
143
     */
144
    private function addToFrontmatter($matter)
145
    {
146
        if (empty($matter)) {
147 6
            return $this;
148
        }
149
150
        if (is_null($this->current->frontmatter)) {
151 6
            $this->current->frontmatter = [];
152 6
        }
153 6
154 6
        $this->current->frontmatter = array_merge($this->current->frontmatter, $matter);
155 6
        return $this;
156 6
    }
157 6
158 6
159 6
160
    /**
161
     * Call a specific filter and store its details.
162 6
     *
163
     * @param string $filter to use.
164 6
     *
165 3
     * @throws mos/TextFilter/Exception when filter does not exists.
166 3
     *
167 3
     * @return string the formatted text.
168 3
     */
169
    private function parseFactory($filter)
170 4
    {
171
        // Define single tasks filter with a callback.
172
        $callbacks = [
173
            "bbcode"    => "bbcode2html",
174 1
            "clickable" => "makeClickable",
175
            "shortcode" => "shortCode",
176 4
            "markdown"  => "markdown",
177 2
            "geshi"     => "syntaxHighlightGeSHi",
178 1
            "nl2br"     => "nl2br",
179 1
            "purify"    => "purify",
180 1
            'anchor4Header' => 'createAnchor4Header',
181 1
        ];
182 1
183
        // Do the specific filter
184 4
        $text = $this->current->text;
185 4
        switch ($filter) {
186 4
            case "jsonfrontmatter":
187 4
                $res = $this->jsonFrontMatter($text);
188 4
                $this->current->text = $res["text"];
189 4
                $this->addToFrontmatter($res["frontmatter"]);
190 4
                break;
191 4
192 4
            case "yamlfrontmatter":
193 4
                $res = $this->yamlFrontMatter($text);
194 4
                $this->current->text = $res["text"];
195 4
                $this->addToFrontmatter($res["frontmatter"]);
196 4
                break;
197
198
            case "titlefromh1":
199
                $title = $this->getTitleFromFirstH1($text);
200
                $this->current->text = $text;
201 6
                if (!isset($this->current->frontmatter["title"])) {
202
                    $this->addToFrontmatter(["title" => $title]);
203
                }
204
                break;
205
206
            case "bbcode":
207
            case "clickable":
208
            case "shortcode":
209
            case "markdown":
210
            case "geshi":
211
            case "nl2br":
212
            case "purify":
213
            case "anchor4Header":
214
                $this->current->text = call_user_func_array(
215 8
                    [$this, $callbacks[$filter]],
216
                    [$text]
217 8
                );
218 8
                break;
219 8
220
            default:
221 8
                throw new Exception("The filter '$filter' is not a valid filter     string.");
222 6
        }
223 8
    }
224
225 8
226 8
227 8
    /**
228 8
     * Call each filter and return array with details of the formatted content.
229
     *
230 8
     * @param string $text   the text to filter.
231
     * @param array  $filter array of filters to use.
232
     *
233
     * @throws mos/TextFilter/Exception  when filterd does not exists.
234
     *
235
     * @return array with the formatted text and additional details.
236
     */
237
    public function parse($text, $filter)
238
    {
239
        $this->current = new \stdClass();
240
        $this->current->frontmatter = null;
241
        $this->current->text = $text;
242
243
        foreach ($filter as $key) {
244 3
            $this->parseFactory($key);
245
        }
246 3
247
        $this->current->text = $this->getUntilStop($this->current->text);
248 3
249
        return $this->current;
250 3
    }
251
252
253
254
    /**
255
     * Add excerpt as short version of text if available.
256 3
     *
257 3
     * @param object &$current same structure as returned by parse().
258 3
     *
259
     * @return void.
0 ignored issues
show
Documentation introduced by
The doc-type void. could not be parsed: Unknown type name "void." at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
260 3
     */
261 2
    public function addExcerpt($current)
262
    {
263 2
        list($excerpt, $hasMore) = $this->getUntilMore($current->text);
264 2
        $current->excerpt = $excerpt;
265 2
        $current->hasMore = $hasMore;
266 2
    }
267 3
268
269 3
270
    /**
271
     * Extract front matter from text.
272
     *
273
     * @param string $text       the text to be parsed.
274
     * @param string $startToken the start token.
275
     * @param string $stopToken  the stop token.
276
     *
277
     * @return array with the formatted text and the front matter.
278
     */
279
    private function extractFrontMatter($text, $startToken, $stopToken)
280
    {
281 3
        $tokenLength = strlen($startToken);
282
283 3
        $start = strpos($text, $startToken);
284
        // Is a valid start?
285 3
        if ($start !== false && $start !== 0) {
286 2
            if ($text[$start - 1] !== "\n") {
287
                $start = false;
288 2
            }
289
        }
290
291 2
        $frontmatter = null;
292
        if ($start !== false) {
293
            $stop = strpos($text, $stopToken, $tokenLength - 1);
294 3
295
            if ($stop !== false && $text[$stop - 1] === "\n") {
296 3
                $length = $stop - ($start + $tokenLength);
297
298
                $frontmatter = substr($text, $start + $tokenLength, $length);
299
                $textStart = substr($text, 0, $start);
300
                $text = $textStart . substr($text, $stop + $tokenLength);
301
            }
302
        }
303
304
        return [$text, $frontmatter];
305
    }
306
307
308
309
    /**
310
     * Extract JSON front matter from text.
311
     *
312
     * @param string $text the text to be parsed.
313
     *
314
     * @return array with the formatted text and the front matter.
315
     */
316 View Code Duplication
    public function jsonFrontMatter($text)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317
    {
318
        list($text, $frontmatter) = $this->extractFrontMatter($text, "{{{\n", "}}}\n");
319
320
        if (!empty($frontmatter)) {
321
            $frontmatter = json_decode($frontmatter, true);
322
323
            if (is_null($frontmatter)) {
324
                throw new Exception("Failed parsing JSON frontmatter.");
325
            }
326
        }
327
328
        return [
329
            "text" => $text,
330
            "frontmatter" => $frontmatter
331
        ];
332
    }
333
334
335
336 1
    /**
337
     * Extract YAML front matter from text.
338 1
     *
339 1
     * @param string $text the text to be parsed.
340
     *
341 1
     * @return array with the formatted text and the front matter.
342 1
     */
343 1 View Code Duplication
    public function yamlFrontMatter($text)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
344
    {
345 1
        list($text, $frontmatter) = $this->extractFrontMatter($text, "---\n", "...\n");
346
347
        if (function_exists("yaml_parse") && !empty($frontmatter)) {
348
            $frontmatter = yaml_parse("---\n$frontmatter...\n");
349
350
            if ($frontmatter === false) {
351
                throw new Exception("Failed parsing YAML frontmatter.");
352
            }
353
        }
354
355
        return [
356
            "text" => $text,
357
            "frontmatter" => $frontmatter
358
        ];
359 3
    }
360
361
362 3
363 3
    /**
364 3
     * Get the title from the first H1.
365 3
     *
366 3
     * @param string $text the text to be parsed.
367
     *
368 3
     * @return string|null with the title, if its found.
369
     */
370
    public function getTitleFromFirstH1($text)
371 3
    {
372 3
        $matches = [];
373 3
        $title = null;
374 3
375 3
        if (preg_match("#<h1.*?>(.*)</h1>#", $text, $matches)) {
376
            $title = strip_tags($matches[1]);
377 3
        }
378
379 3
        return $title;
380
    }
381
382
383
384
    /**
385
     * Helper, BBCode formatting converting to HTML.
386
     *
387
     * @param string $text The text to be converted.
388
     *
389
     * @return string the formatted text.
390
     *
391
     * @link http://dbwebb.se/coachen/reguljara-uttryck-i-php-ger-bbcode-formattering
392
     */
393 1
    public function bbcode2html($text)
394
    {
395 1
        $search = [
396 1
            '/\[b\](.*?)\[\/b\]/is',
397
            '/\[i\](.*?)\[\/i\]/is',
398 1
            '/\[u\](.*?)\[\/u\]/is',
399 1
            '/\[img\](https?.*?)\[\/img\]/is',
400
            '/\[url\](https?.*?)\[\/url\]/is',
401 1
            '/\[url=(https?.*?)\](.*?)\[\/url\]/is'
402
        ];
403
404
        $replace = [
405
            '<strong>$1</strong>',
406
            '<em>$1</em>',
407
            '<u>$1</u>',
408
            '<img src="$1" />',
409
            '<a href="$1">$1</a>',
410
            '<a href="$1">$2</a>'
411
        ];
412
413
        return preg_replace($search, $replace, $text);
414 2
    }
415
416 2
417 2
418 2
    /**
419 2
     * Make clickable links from URLs in text.
420 2
     *
421
     * @param string $text the text that should be formatted.
422
     *
423
     * @return string with formatted anchors.
424
     *
425 2
     * @link http://dbwebb.se/coachen/lat-php-funktion-make-clickable-automatiskt-skapa-klickbara-lankar
426
     */
427
    public function makeClickable($text)
428
    {
429
        return preg_replace_callback(
430
            '#\b(?<![href|src]=[\'"])https?://[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))#',
431
            function ($matches) {
432
                return "<a href='{$matches[0]}'>{$matches[0]}</a>";
433
            },
434
            $text
435
        );
436
    }
437 1
438
439 1
440 1
    /**
441
     * Syntax highlighter using GeSHi http://qbnz.com/highlighter/.
442
     *
443 1
     * @param string $text     text to be converted.
444
     * @param string $language which language to use for highlighting syntax.
445 1
     *
446
     * @return string the formatted text.
447
     */
448
    public function syntaxHighlightGeSHi($text, $language = "text")
449
    {
450
        $language = $language ?: "text";
451
        $language = ($language === 'html') ? 'html4strict' : $language;
452
        $geshi = new \GeSHi($text, $language);
453
        $geshi->set_overall_class('geshi');
454
        $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...
455
        //$geshi->set_header_type(GESHI_HEADER_PRE_VALID);
456
        //$geshi->enable_line_numbers(GESHI_NORMAL_LINE_NUMBERS);
457 7
        //echo "<pre>", $geshi->get_stylesheet(false) , "</pre>"; exit;
458
459 7
        $code = $geshi->parse_code();
460
461
        // Replace last &nbsp;</pre>, -strlen("&nbsp;</pre>") == 12
462
        $code = substr_replace($code, "</pre>", -12);
463
        return $code;
464
    }
465
466
467
468
    /**
469
     * Format text according to HTML Purifier.
470
     *
471 1
     * @param string $text that should be formatted.
472
     *
473 1
     * @return string as the formatted html-text.
474
     */
475
    public function purify($text)
476
    {
477
        $config   = \HTMLPurifier_Config::createDefault();
478
        $config->set("Cache.DefinitionImpl", null);
479
        //$config->set('Cache.SerializerPath', '/home/user/absolute/path');
480
481
        $purifier = new \HTMLPurifier($config);
482
    
483
        return $purifier->purify($text);
484
    }
485 2
486
487
488
    /**
489
     * Format text according to Markdown syntax.
490
     *
491
     * @param string $text the text that should be formatted.
492
     *
493
     * @return string as the formatted html-text.
494
     */
495
    public function markdown($text)
496
    {
497
        return \Michelf\MarkdownExtra::defaultTransform($text);
498
    }
499
500
501 2
502 2
    /**
503 2
     * For convenience access to nl2br
504
     *
505 2
     * @param string $text text to be converted.
506 2
     *
507 2
     * @return string the formatted text.
508 2
     */
509
    public function nl2br($text)
510 2
    {
511 1
        return nl2br($text);
512
    }
513
514 1
515 1
516
    /**
517
     * Support SmartyPants for better typography.
518
     *
519
     * @param string text text to be converted.
520
     * @return string the formatted text.
521 2
     */
522
/*     public static function SmartyPants($text) {   
523 2
      require_once(__DIR__.'/php_smartypants_1.5.1e/smartypants.php');
524
      return SmartyPants($text);
525
    }
526
*/
527
528
529
    /**
530
     * Support enhanced SmartyPants/Typographer for better typography.
531
     *
532
     * @param string text text to be converted.
533
     * @return string the formatted text.
534
     */
535 1
/*     public static function Typographer($text) {   
536
      require_once(__DIR__.'/php_smartypants_typographer_1.0/smartypants.php');
537 1
      $ret = SmartyPants($text);
538
      return $ret;
539 1
    }
540 1
*/
541
}
542