Passed
Push — master ( 2275f4...6b55ec )
by Andreas
25:05
created

net_nemein_wiki_parser::render_link()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 8
nop 2
dl 0
loc 30
ccs 0
cts 20
cp 0
crap 42
rs 9.0111
c 0
b 0
f 0
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
17
{
18
    use midcom_baseclasses_components_base;
0 ignored issues
show
introduced by
The trait midcom_baseclasses_components_base requires some properties which are not provided by net_nemein_wiki_parser: $i18n, $head
Loading history...
19
20
    private net_nemein_wiki_wikipage $_page;
21
22 8
    public function __construct(net_nemein_wiki_wikipage $page)
23
    {
24 8
        $this->_component = 'net.nemein.wiki';
25 8
        $this->_page = $page;
26
    }
27
28
    public function get_html() : string
29
    {
30
        return MarkdownExtra::defaultTransform($this->get_markdown($this->_page->content));
31
    }
32
33 4
    public function get_markdown(string $input) : string
34
    {
35 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

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