Completed
Push — master ( d6728d...9d5144 )
by Mikael
02:29
created

CTextFilter::extractFrontMatter()   C

Complexity

Conditions 7
Paths 9

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 7.457

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 27
ccs 15
cts 19
cp 0.7895
rs 6.7272
cc 7
eloc 15
nc 9
nop 3
crap 7.457
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($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 3
                $res = $this->jsonFrontMatter($text);
158 4
                $this->current->text = $res["text"];
159 3
                $this->addToFrontmatter($res["frontmatter"]);
160 3
                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
                if (!isset($this->current->frontmatter["title"])) {
172 1
                    $this->addToFrontmatter(["title" => $title]);
173 1
                }
174 2
                break;
175
176 2
            case "bbcode":
177 2
            case "clickable":
178 2
            case "shortcode":
179 2
            case "markdown":
180 2
            case "nl2br":
181 2
            case "purify":
182 2
                $this->current->text = call_user_func_array(
183 2
                    [$this, $callbacks[$filter]],
184 2
                    [$text]
185 2
                );
186 2
                break;
187
188
            default:
189
                throw new Exception("The filter '$filter' is not a valid filter     string.");
190
        }
191 4
    }
192
193
194
195
    /**
196
     * Call each filter and return array with details of the formatted content.
197
     *
198
     * @param string $text   the text to filter.
199
     * @param array  $filter array of filters to use.
200
     *
201
     * @throws mos/TextFilter/Exception  when filterd does not exists.
202
     *
203
     * @return array with the formatted text and additional details.
204
     */
205 4
    public function parse($text, $filter)
206
    {
207 4
        $this->current = new \stdClass();
208 4
        $this->current->frontmatter = null;
209 4
        $this->current->text = $text;
210
211 4
        foreach ($filter as $key) {
212 4
            $this->parseFactory($key);
213 4
        }
214
215 4
        return $this->current;
216
    }
217
218
219
220
    /**
221
     * Extract front matter from text.
222
     *
223
     * @param string $text       the text to be parsed.
224
     * @param string $startToken the start token.
225
     * @param string $stopToken  the stop token.
226
     *
227
     * @return array with the formatted text and the front matter.
228
     */
229 3
    private function extractFrontMatter($text, $startToken, $stopToken)
230
    {
231 3
        $tokenLength = strlen($startToken);
232
233 3
        $start = strpos($text, $startToken);
234
        // Is a valid start?
235 3
        if ($start !== false && $start !== 0) {
236
            if ($text[$start - 1] !== "\n") {
237
                $start = false;
238
            }
239
        }
240
241 3
        $frontmatter = null;
242 3
        if ($start !== false) {
243 3
            $stop = strpos($text, $stopToken, $tokenLength - 1);
244
245 3
            if ($stop !== false && $text[$stop - 1] === "\n") {
246 2
                $length = $stop - ($start + $tokenLength);
247
248 2
                $frontmatter = substr($text, $start + $tokenLength, $length);
249 2
                $textStart = substr($text, 0, $start);
250 2
                $text = $textStart . substr($text, $stop + $tokenLength);
251 2
            }
252 3
        }
253
254 3
        return [$text, $frontmatter];
255
    }
256
257
258
259
    /**
260
     * Extract JSON front matter from text.
261
     *
262
     * @param string $text the text to be parsed.
263
     *
264
     * @return array with the formatted text and the front matter.
265
     */
266 3
    public function jsonFrontMatter($text)
267
    {
268 3
        list($text, $frontmatter) = $this->extractFrontMatter($text, "{{{\n", "}}}\n");
269
270 3
        if (!empty($frontmatter)) {
271 2
            $frontmatter = json_decode($frontmatter, true);
272
273 2
            if (is_null($frontmatter)) {
274
                throw new Exception("Failed parsing JSON frontmatter.");
275
            }
276 2
        }
277
278
        return [
279 3
            "text" => $text,
280
            "frontmatter" => $frontmatter
281 3
        ];
282
    }
283
284
285
286
    /**
287
     * Extract YAML front matter from text.
288
     *
289
     * @param string $text the text to be parsed.
290
     *
291
     * @return array with the formatted text and the front matter.
292
     */
293
    public function yamlFrontMatter($text)
294
    {
295
        $needle = "---\n";
296
        list($text, $frontmatter) = $this->extractFrontMatter($text, $needle, $needle);
297
298
        if (function_exists("yaml_parse") && !empty($frontmatter)) {
299
            $frontmatter = yaml_parse($needle . $frontmatter);
300
301
            if ($frontmatter === false) {
302
                throw new Exception("Failed parsing YAML frontmatter.");
303
            }
304
        }
305
306
        return [
307
            "text" => $text,
308
            "frontmatter" => $frontmatter
309
        ];
310
    }
311
312
313
314
    /**
315
     * Get the title from the first H1.
316
     *
317
     * @param string $text the text to be parsed.
318
     *
319
     * @return string|null with the title, if its found.
320
     */
321 1
    public function getTitleFromFirstH1($text)
322
    {
323 1
        $matches = [];
324 1
        $title = null;
325
326 1
        if (preg_match("#<h1.*?>(.*)</h1>#", $text, $matches)) {
327 1
            $title = strip_tags($matches[1]);
328 1
        }
329
330 1
        return $title;
331
    }
332
333
334
335
    /**
336
     * Helper, BBCode formatting converting to HTML.
337
     *
338
     * @param string $text The text to be converted.
339
     *
340
     * @return string the formatted text.
341
     *
342
     * @link http://dbwebb.se/coachen/reguljara-uttryck-i-php-ger-bbcode-formattering
343
     */
344 3
    public function bbcode2html($text)
345
    {
346
        $search = [
347 3
            '/\[b\](.*?)\[\/b\]/is',
348 3
            '/\[i\](.*?)\[\/i\]/is',
349 3
            '/\[u\](.*?)\[\/u\]/is',
350 3
            '/\[img\](https?.*?)\[\/img\]/is',
351 3
            '/\[url\](https?.*?)\[\/url\]/is',
352
            '/\[url=(https?.*?)\](.*?)\[\/url\]/is'
353 3
        ];
354
355
        $replace = [
356 3
            '<strong>$1</strong>',
357 3
            '<em>$1</em>',
358 3
            '<u>$1</u>',
359 3
            '<img src="$1" />',
360 3
            '<a href="$1">$1</a>',
361
            '<a href="$1">$2</a>'
362 3
        ];
363
364 3
        return preg_replace($search, $replace, $text);
365
    }
366
367
368
369
    /**
370
     * Make clickable links from URLs in text.
371
     *
372
     * @param string $text the text that should be formatted.
373
     *
374
     * @return string with formatted anchors.
375
     *
376
     * @link http://dbwebb.se/coachen/lat-php-funktion-make-clickable-automatiskt-skapa-klickbara-lankar
377
     */
378 1
    public function makeClickable($text)
379
    {
380 1
        return preg_replace_callback(
381 1
            '#\b(?<![href|src]=[\'"])https?://[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))#',
382
            function ($matches) {
383 1
                return "<a href='{$matches[0]}'>{$matches[0]}</a>";
384 1
            },
385
            $text
386 1
        );
387
    }
388
389
390
391
    /**
392
     * Format text according to HTML Purifier.
393
     *
394
     * @param string $text that should be formatted.
395
     *
396
     * @return string as the formatted html-text.
397
     */
398 1
    public function purify($text)
399
    {
400 1
        $config   = \HTMLPurifier_Config::createDefault();
401 1
        $config->set("Cache.DefinitionImpl", null);
402
        //$config->set('Cache.SerializerPath', '/home/user/absolute/path');
403
404 1
        $purifier = new \HTMLPurifier($config);
405
    
406 1
        return $purifier->purify($text);
407
    }
408
409
410
411
    /**
412
     * Format text according to Markdown syntax.
413
     *
414
     * @param string $text the text that should be formatted.
415
     *
416
     * @return string as the formatted html-text.
417
     */
418 7
    public function markdown($text)
419
    {
420 7
        return \Michelf\MarkdownExtra::defaultTransform($text);
421
    }
422
423
424
425
    /**
426
     * For convenience access to nl2br
427
     *
428
     * @param string $text text to be converted.
429
     *
430
     * @return string the formatted text.
431
     */
432 1
    public function nl2br($text)
433
    {
434 1
        return nl2br($text);
435
    }
436
437
438
439
    /**
440
     * Shortcode to to quicker format text as HTML.
441
     *
442
     * @param string $text text to be converted.
443
     *
444
     * @return string the formatted text.
445
     */
446 1
    public function shortCode($text)
447
    {
448
        $patterns = [
449 1
            '/\[(FIGURE)[\s+](.+)\]/',
450 1
        ];
451
452 1
        return preg_replace_callback(
453 1
            $patterns,
454 1
            function ($matches) {
455 1
                switch ($matches[1]) {
456
457 1
                    case 'FIGURE':
458 1
                        return self::ShortCodeFigure($matches[2]);
459
                        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...
460
461
                    default:
462
                        return "{$matches[1]} is unknown shortcode.";
463
                }
464 1
            },
465
            $text
466 1
        );
467
    }
468
469
470
471
    /**
472
     * Init shortcode handling by preparing the option list to an array, for those using arguments.
473
     *
474
     * @param string $options for the shortcode.
475
     *
476
     * @return array with all the options.
477
     */
478 1
    public static function shortCodeInit($options)
479
    {
480 1
        preg_match_all('/[a-zA-Z0-9]+="[^"]+"|\S+/', $options, $matches);
481
482 1
        $res = array();
483 1
        foreach ($matches[0] as $match) {
484 1
            $pos = strpos($match, '=');
485 1
            if ($pos === false) {
486
                $res[$match] = true;
487
            } else {
488 1
                $key = substr($match, 0, $pos);
489 1
                $val = trim(substr($match, $pos+1), '"');
490 1
                $res[$key] = $val;
491
            }
492 1
        }
493
494 1
        return $res;
495
    }
496
497
498
499
    /**
500
     * Shortcode for <figure>.
501
     *
502
     * Usage example: [FIGURE src="img/home/me.jpg" caption="Me" alt="Bild på mig" nolink="nolink"]
503
     *
504
     * @param string $options for the shortcode.
505
     *
506
     * @return array with all the options.
507
     */
508 1
    public static function shortCodeFigure($options)
509
    {
510
        // Merge incoming options with default and expose as variables
511 1
        $options= array_merge(
512
            [
513 1
                'id' => null,
514 1
                'class' => null,
515 1
                'src' => null,
516 1
                'title' => null,
517 1
                'alt' => null,
518 1
                'caption' => null,
519 1
                'href' => null,
520 1
                'nolink' => false,
521 1
            ],
522 1
            self::ShortCodeInit($options)
523 1
        );
524 1
        extract($options, EXTR_SKIP);
525
526 1
        $id = $id ? " id='$id'" : null;
527 1
        $class = $class ? " class='figure $class'" : " class='figure'";
528 1
        $title = $title ? " title='$title'" : null;
529
530 1
        if (!$alt && $caption) {
531 1
            $alt = $caption;
532 1
        }
533
534 1
        if (!$href) {
535 1
            $pos = strpos($src, '?');
536 1
            $href = $pos ? substr($src, 0, $pos) : $src;
537 1
        }
538
539 1
        $start = null;
540 1
        $end = null;
541 1
        if (!$nolink) {
542 1
            $start = "<a href='{$href}'>";
543 1
            $end = "</a>";
544 1
        }
545
546
        $html = <<<EOD
547 1
<figure{$id}{$class}>
548 1
{$start}<img src='{$src}' alt='{$alt}'{$title}/>{$end}
549 1
<figcaption markdown=1>{$caption}</figcaption>
550 1
</figure>
551 1
EOD;
552
553 1
        return $html;
554
    }
555
}
556