Completed
Push — master ( cde53c...fe9df2 )
by Andreas
27:57 queued 42s
created

net_nemein_wiki_parser   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 286
Duplicated Lines 0 %

Test Coverage

Coverage 29.13%

Importance

Changes 0
Metric Value
eloc 118
dl 0
loc 286
ccs 37
cts 127
cp 0.2913
rs 8.72
c 0
b 0
f 0
wmc 46

16 Methods

Rating   Name   Duplication   Size   Complexity  
A _run_macro_note() 0 3 1
A _run__classed_div() 0 4 1
A __construct() 0 4 1
A get_html() 0 3 1
A _run_macro_warning() 0 3 1
A _run_macro_tip() 0 3 1
A get_markdown() 0 3 1
A _run_macro_photo() 0 16 3
A _run_macro_wiki() 0 9 2
A _run_macro_abbr() 0 7 2
A _run_macro_nodetoc() 0 21 3
A _run_macro_tagged() 0 32 5
B render_link() 0 35 8
B replace_wikiwords() 0 33 7
A _code_sort_by_title() 0 3 1
B find_links_in_content() 0 28 8

How to fix   Complexity   

Complex Class

Complex classes like net_nemein_wiki_parser 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.

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 net_nemein_wiki_parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package net.nemein.wiki
4
 * @author CONTENT CONTROL http://www.contentcontrol-berlin.de/
5
 * @copyright CONTENT CONTROL http://www.contentcontrol-berlin.de/
6
 * @license http://www.gnu.org/licenses/gpl.html GNU General Public License
7
 */
8
9
use Michelf\MarkdownExtra;
10
11
/**
12
 * Wiki markup parser
13
 *
14
 * @package net.nemein.wiki
15
 */
16
class net_nemein_wiki_parser extends midcom_baseclasses_components_purecode
17
{
18
    /**
19
     * The page we're working on
20
     *
21
     * @var net_nemein_wiki_wikipage
22
     */
23
    private $_page;
24
25 8
    public function __construct(net_nemein_wiki_wikipage $page)
26
    {
27 8
        $this->_page = $page;
28 8
        parent::__construct();
29 8
    }
30
31
    public function get_html() : string
32
    {
33
        return MarkdownExtra::defaultTransform($this->get_markdown($this->_page->content));
34
    }
35
36 4
    public function get_markdown($input) : string
37
    {
38 4
        return preg_replace_callback($this->_config->get('wikilink_regexp'), [$this, 'replace_wikiwords'], $input);
0 ignored issues
show
Bug introduced by
It seems like $this->_config->get('wikilink_regexp') can also be of type false; however, parameter $pattern of preg_replace_callback() does only seem to accept string|string[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

38
        return preg_replace_callback(/** @scrutinizer ignore-type */ $this->_config->get('wikilink_regexp'), [$this, 'replace_wikiwords'], $input);
Loading history...
39
    }
40
41
    /**
42
     * Abbreviation support [abbr: Abbreviation - Explanation]
43
     */
44
    private function _run_macro_abbr(string $macro_content, string $fulltag, string $after) : string
45
    {
46
        if (preg_match("/^(.*?) \- (.*)/", $macro_content, $parts)) {
47
            return "<abbr title=\"{$parts[2]}\">{$parts[1]}</abbr>{$after}";
48
        }
49
        // Could not figure it out, return the tag as is
50
        return $fulltag;
51
    }
52
53
    /**
54
     * Photo inclusion support [photo: GUID]
55
     */
56
    private function _run_macro_photo(string $macro_content, string $fulltag, string $after) : string
57
    {
58
        $guid = trim($macro_content);
59
        if (!mgd_is_guid($guid)) {
60
            // value is not guid
61
            return $fulltag;
62
        }
63
64
        try {
65
            $attachment = new midcom_db_attachment($guid);
66
        } catch (midcom_error $e) {
67
            $e->log();
68
            return "<span class=\"missing_photo\" title=\"{$guid}\">{$fulltag}</span>{$after}";
69
        }
70
71
        return '<img src="' . midcom_db_attachment::get_url($attachment) . '" class="wiki_photo">' . $after;
72
    }
73
74
    /**
75
     * WikiPedia term search [wiki: search terms]
76
     *
77
     * @todo Switch to InterWiki format instead
78
     */
79 4
    private function _run_macro_wiki(string $macro_content, string $fulltag, string $after) : string
80
    {
81 4
        $text = trim($macro_content);
82 4
        if (empty($text)) {
83
            return $fulltag;
84
        }
85 4
        $target = ucfirst(strtolower(preg_replace('/[\s\-,.\']+/', "_", $text)));
86 4
        $url = "http://en.wikipedia.org/wiki/{$target}";
87 4
        return "<a href=\"{$url}\" class=\"wikipedia\">{$text}</a>{$after}";
88
    }
89
90
    /**
91
     * A notice macro (will display a classed DIV)
92
     */
93
    private function _run_macro_note(string $macro_content, string $fulltag, string $after) : string
94
    {
95
        return $this->_run__classed_div('note', $macro_content, $fulltag, $after);
96
    }
97
98
    /**
99
     * A tip macro (will display a classed DIV)
100
     */
101
    private function _run_macro_tip(string $macro_content, string $fulltag, string $after) : string
102
    {
103
        return $this->_run__classed_div('tip', $macro_content, $fulltag, $after);
104
    }
105
106
    /**
107
     * A warning macro (will display a classed DIV)
108
     */
109
    private function _run_macro_warning(string $macro_content, string $fulltag, string $after) : string
110
    {
111
        return $this->_run__classed_div('warning', $macro_content, $fulltag, $after);
112
    }
113
114
    /**
115
     * Creates a div with given CSS class(es)
116
     *
117
     * Used by the note, tip and warning macros
118
     */
119
    private function _run__classed_div(string $css_class, string $macro_content, string $fulltag, string $after) : string
120
    {
121
        $text = trim($macro_content);
122
        return "\n<div class=\"{$css_class}\">\n{$text}\n</div>\n{$after}";
123
    }
124
125
    /**
126
     * table of contents for the current pages node
127
     */
128
    private function _run_macro_nodetoc(string $macro_content, string $fulltag, string $after) : string
129
    {
130
        $text = trim($macro_content);
131
        $qb = net_nemein_wiki_wikipage::new_query_builder();
132
        $qb->add_constraint('topic', '=', $this->_page->topic);
133
        $qb->add_constraint('name', '<>', $this->_page->name);
134
        $qb->add_order('title', 'ASC');
135
136
        $ret = '';
137
        if (!empty($text)) {
138
            $ret .= "\n<h3 class=\"node-toc-headline\">{$text}</h3>\n";
139
        }
140
        $nap = new midcom_helper_nav();
141
        $node = $nap->get_node($this->_page->topic);
142
        $ret .= "\n<ul class=\"node-toc\">\n";
143
        foreach ($qb->execute() as $page) {
144
            $url = $node[MIDCOM_NAV_ABSOLUTEURL] . "{$page->name}/";
145
            $ret .= "    <li class=\"page\"><a href=\"{$url}\">{$page->title}</a></li>\n";
146
        }
147
        $ret .= "</ul>\n";
148
        return $ret . $after;
149
    }
150
151
    /**
152
     * Links to other wiki pages tagged with arbitrary tags
153
     */
154
    private function _run_macro_tagged(string $macro_content, string $fulltag, string $after) : string
155
    {
156
        $tags_exploded = explode(',', $macro_content);
157
        $tags = [];
158
        foreach (array_filter($tags_exploded) as $tagname) {
159
            $tags[] = net_nemein_tag_handler::resolve_tagname(trim($tagname));
160
        }
161
        $classes = [
162
            net_nemein_wiki_wikipage::class,
163
            midcom_db_article::class,
164
            'midgard_article',
165
        ];
166
        $pages = net_nemein_tag_handler::get_objects_with_tags($tags, $classes, 'OR');
167
        if (!is_array($pages)) {
0 ignored issues
show
introduced by
The condition is_array($pages) is always true.
Loading history...
168
            // Failure in tag library
169
            return $fulltag;
170
        }
171
        $nap = new midcom_helper_nav();
172
        $ret = "\n<ul class=\"tagged\">\n";
173
174
        usort($pages, [$this, '_code_sort_by_title']);
175
        foreach ($pages as $page) {
176
            $node = $nap->get_node($page->topic);
177
            if ($node[MIDCOM_NAV_COMPONENT] !== 'net.nemein.wiki') {
178
                // We only wish to link to wiki pages
179
                continue;
180
            }
181
            $url = $node[MIDCOM_NAV_ABSOLUTEURL] . "{$page->name}/";
182
            $ret .= "    <li class=\"page\"><a href=\"{$url}\">{$page->title}</a></li>\n";
183
        }
184
        $ret .= "</ul>\n";
185
        return $ret . $after;
186
    }
187
188
    /**
189
     * Code to sort array by values (which is object) property 'title'
190
     *
191
     * Used by $this->_run_macro_tagged()
192
     */
193
    private function _code_sort_by_title($a, $b)
194
    {
195
        return strnatcmp($a->title, $b->title);
196
    }
197
198
    /**
199
     * Replace wiki syntax in the document with HTML for display purposes
200
     */
201 4
    private function replace_wikiwords(array $match) : string
202
    {
203
        // Refactored using code from the WordPress SimpleLink plugin
204
        // http://warpedvisions.org/projects/simplelink
205
        // Since then refactored again...
206 4
        $fulltext = $match[1];
207 4
        $after = $match[2] ?: '';
208
209
        // See what kind of tag we have hit
210
        switch (true) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/^(.*?)\|(.*.../i', $fulltext, $parts) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/^\!\!(.*)/', $fulltext, $parts) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/[\(:\[]/', $after) of type integer to the boolean true. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
211
            // Ignore markdown tags
212 4
            case (preg_match("/[\(:\[]/", $after)):
213
                // TODO: should by str match (array) instead
214
                return $match[0];
215
216
            // Escaped tag [!!text]
217 4
            case (preg_match("/^\!\!(.*)/", $fulltext, $parts)):
218 4
                return "[{$parts[1]}]{$after}";
219
220
            // MediaWiki-style link [wikipage|label]
221 4
            case (preg_match("/^(.*?)\|(.*?)$/i", $fulltext, $parts)):
222
                return $this->render_link($parts[1], $parts[2]) . $after;
223
224
            // Macro [something: <data>] (for example [abbr: BOFH - Bastard Operator From Hell] or [photo: <GUID>])
225 4
            case (   preg_match('/^(.*?): (.*)/', $fulltext, $macro_parts)
226 4
                  && method_exists($this, "_run_macro_{$macro_parts[1]}")):
227 4
                $method = "_run_macro_{$macro_parts[1]}";
228 4
                return $this->$method($macro_parts[2], $match[0], $after);
229
230
            // MediaWiki-style link [wikipage] (no text)
231
            // TODO: it is possible that this wasn't originally intended to be default, but the if/elseif tree was complex and this ended up being resolved as the last else
232
            default:
233
                return $this->render_link($fulltext, $fulltext) . $after;
234
        }
235
    }
236
237
    public function render_link(string $wikilink, $text = null) : string
238
    {
239
        if (null === $text) {
240
            $text = $wikilink;
241
        }
242
        // Don't choke on links with anchors
243
        $parts = explode('#', $wikilink, 2);
244
        $page_anchor = null;
245
        if (count($parts) == 2) {
246
            $wikilink = $parts[0];
247
            $page_anchor = "#{$parts[1]}";
248
        }
249
        $resolver = new net_nemein_wiki_resolver($this->_page->topic);
250
        $wikipage_match = $resolver->path_to_wikipage($wikilink);
251
        if ($wikipage_match['wikipage'] === null) {
252
            // No page matched, link to creation
253
            $folder = $wikipage_match['folder'];
254
            if ($wikipage_match['folder'] === null) {
255
                $folder = $wikipage_match['latest_parent'];
256
            }
257
258
            if (   isset($folder[MIDCOM_NAV_OBJECT])
259
                && $folder[MIDCOM_NAV_OBJECT]->can_do('midgard:create')) {
260
                $workflow = $this->get_workflow('datamanager');
261
                return "<a href=\"{$folder[MIDCOM_NAV_ABSOLUTEURL]}create/?wikiword={$wikipage_match['remaining_path']}\" " . $workflow->render_attributes() . " class=\"wiki_missing\" title=\"" . $this->_l10n->get('click to create') . "\">{$text}</a>";
262
            }
263
            return "<span class=\"wiki_missing_nouser\" title=\"" . $this->_l10n->get('login to create') . "\">{$text}</span>";
264
        }
265
266
        $url_name = $resolver->generate_page_url($wikipage_match['wikipage']);
267
268
        $type = $wikipage_match['wikipage']->get_parameter('midcom.helper.datamanager2', 'schema_name');
269
        $type .= ($wikipage_match['wikipage']->can_do('midgard:read')) ? '' : ' access-denied';
270
271
        return "<a href=\"{$url_name}{$page_anchor}\" class=\"wikipage {$type}\" title=\"{$wikilink}\">{$text}</a>";
272
    }
273
274 4
    public function find_links_in_content() : array
275
    {
276
        // Seek wiki page links inside page content
277 4
        $matches = [];
278 4
        $links = [];
279 4
        preg_match_all($this->_config->get('wikilink_regexp'), $this->_page->content, $matches);
280 4
        foreach ($matches[1] as $match_key => $match) {
281 2
            $fulltext = $match;
282 2
            $after = $matches[2][$match_key] ?: '';
283
284
            // See what kind of tag we have hit
285
            // NOTE: This logic must be kept consistent with $this->replace_wikiwords()
286
            // Ignore markdown tags and escaped tag [!!text]
287 2
            if (!preg_match("/[\(:\[]/", $after) && !preg_match("/^\!\!(.*)/", $fulltext)) {
288
                // MediaWiki-style link [wikipage|label]
289 2
                if (preg_match("/^(.*?)\|(.*?)$/i", $fulltext, $parts)) {
290 1
                    $links[$parts[1]] = $parts[2];
291
                }
292
                // Ignore macros [something: <data>] (for example [abbr: BOFH - Bastard Operator From Hell] or [photo: <GUID>])
293 1
                elseif (   !preg_match('/^(.*?): (.*)/', $fulltext, $macro_parts)
294 1
                        || !method_exists($this, "_run_macro_{$macro_parts[1]}")) {
295
                    // MediaWiki-style link [wikipage] (no text)
296 1
                    $links[$fulltext] = $fulltext;
297
                }
298
            }
299
        }
300
301 4
        return $links;
302
    }
303
}
304