xref: /plugin/combo/syntax/related.php (revision 1fa8c418ed5809db58049141be41b7738471dd32)
1<?php
2/**
3 * DokuWiki Syntax Plugin Related.
4 *
5 */
6
7use ComboStrap\LinkUtility;
8use ComboStrap\Page;
9use ComboStrap\PluginUtility;
10use ComboStrap\TagAttributes;
11
12
13require_once(DOKU_INC . 'inc/parserutils.php');
14
15/**
16 * All DokuWiki plugins to extend the parser/rendering mechanism
17 * need to inherit from this class
18 *
19 * The name of the class must follow a pattern (don't change it)
20 *
21 * The index and the metadata key for backlinks is  called 'relation_references'
22 * It's the key value that you need to pass in the {@link lookupKey} of the {@link \dokuwiki\Search\Indexer}
23 *
24 * Type of conf[index]/index:
25 *   * page.idx (id of the page is the element number)
26 *   * title
27 *   * relation_references_w.idx - _w for words
28 *   * relation_references_w.idx - _i for lines (index by lines)
29 *
30 * The index is a associative map by key
31 *
32 *
33 */
34class syntax_plugin_combo_related extends DokuWiki_Syntax_Plugin
35{
36
37
38    // Conf property key
39    const MAX_LINKS_CONF = 'maxLinks';
40    // For when you come from another plugin (such as backlinks) and that you don't want to change the pattern on each page
41    const EXTRA_PATTERN_CONF = 'extra_pattern';
42
43    // This is a fake page ID that is added
44    // to the related page array when the number of backlinks is bigger than the max
45    // Poisoning object strategy
46    const MORE_PAGE_ID = 'related_more';
47
48    // The array key of an array of related page
49    const RELATED_PAGE_ID_PROP = 'id';
50    const RELATED_BACKLINKS_COUNT_PROP = 'backlinks';
51
52
53    /**
54     * Syntax Type.
55     *
56     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
57     * @see DokuWiki_Syntax_Plugin::getType()
58     */
59    function getType()
60    {
61        return 'substition';
62    }
63
64    /**
65     * @see DokuWiki_Syntax_Plugin::getPType()
66     */
67    function getPType()
68    {
69        return 'block';
70    }
71
72    /**
73     * @see Doku_Parser_Mode::getSort()
74     */
75    function getSort()
76    {
77        return 100;
78    }
79
80    /**
81     * Create a pattern that will called this plugin
82     *
83     * @param string $mode
84     * @see Doku_Parser_Mode::connectTo()
85     */
86    function connectTo($mode)
87    {
88        // The basic
89        $this->Lexer->addSpecialPattern('<' . self::getTag() . '[^>]*>', $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
90
91        // To replace backlinks, you may add it in the configuration
92        $extraPattern = $this->getConf(self::EXTRA_PATTERN_CONF);
93        if ($extraPattern != "") {
94            $this->Lexer->addSpecialPattern($extraPattern, $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
95        }
96
97    }
98
99    /**
100     *
101     * The handle function goal is to parse the matched syntax through the pattern function
102     * and to return the result for use in the renderer
103     * This result is always cached until the page is modified.
104     * @param string $match
105     * @param int $state
106     * @param int $pos
107     * @param Doku_Handler $handler
108     * @return array|bool
109     * @see DokuWiki_Syntax_Plugin::handle()
110     *
111     */
112    function handle($match, $state, $pos, Doku_Handler $handler)
113    {
114
115        switch ($state) {
116
117            // As there is only one call to connect to in order to a add a pattern,
118            // there is only one state entering the function
119            // but I leave it for better understanding of the process flow
120            case DOKU_LEXER_SPECIAL :
121
122                // Parse the parameters
123                $match = substr($match, strlen(self::getTag()), -1);
124                $parameters = array();
125
126                // /i not case sensitive
127                $attributePattern = "\\s*(\w+)\\s*=\\s*[\'\"]{1}([^\`\"]*)[\'\"]{1}\\s*";
128                $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches);
129                if ($result != 0) {
130                    foreach ($matches[1] as $key => $parameterKey) {
131                        $parameter = strtolower($parameterKey);
132                        $value = $matches[2][$key];
133                        $parameters[$parameter] = $value;
134                    }
135                }
136                // Cache the values
137                return array($state, $parameters);
138
139        }
140
141        // Cache the values
142        return array($state);
143    }
144
145    /**
146     * Render the output
147     * @param string $format
148     * @param Doku_Renderer $renderer
149     * @param array $data - what the function handle() return'ed
150     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
151     * @see DokuWiki_Syntax_Plugin::render()
152     *
153     *
154     */
155    function render($format, Doku_Renderer $renderer, $data)
156    {
157        global $lang;
158        global $INFO;
159        global $ID;
160
161        $id = $ID;
162        // If it's a sidebar, get the original id.
163        if (isset($INFO)) {
164            $id = $INFO['id'];
165        }
166
167        if ($format == 'xhtml') {
168
169            $relatedPages = $this->related($id);
170            $tagAttributes = TagAttributes::createEmpty(self::getTag());
171            $tagAttributes->addClassName("d-print-none");
172            $renderer->doc .= $tagAttributes->toHtmlEnterTag("div");
173
174            if (empty($relatedPages)) {
175
176                // Dokuwiki debug
177                dbglog("No Backlinks", "Related plugins: all backlinks for page: $id");
178                $renderer->doc .= "<strong>Plugin " . PluginUtility::PLUGIN_BASE_NAME . " - Component " . self::getTag() . ": " . $lang['nothingfound'] . "</strong>" . DOKU_LF;
179
180            } else {
181
182                // Dokuwiki debug
183
184                $renderer->doc .= '<ul>' . DOKU_LF;
185
186                foreach ($relatedPages as $backlink) {
187                    $backlinkId = $backlink[self::RELATED_PAGE_ID_PROP];
188                    $renderer->doc .= '<li>';
189                    if ($backlinkId != self::MORE_PAGE_ID) {
190                        $linkUtility = LinkUtility::createFromPageId($backlinkId);
191                        $renderer->doc .= $linkUtility->renderOpenTag($renderer);
192                        $renderer->doc .= ucfirst($linkUtility->getName());
193                        $renderer->doc .= $linkUtility->renderClosingTag();
194                    } else {
195                        $renderer->doc .=
196                            tpl_link(
197                                wl($id) . '?do=backlink',
198                                "More ...",
199                                'class="" rel="nofollow" title="More..."',
200                                $return = true
201                            );
202                    }
203                    $renderer->doc .= '</li>' . DOKU_LF;
204                }
205
206                $renderer->doc .= '</ul>' . DOKU_LF;
207
208            }
209
210            $renderer->doc .= '</div>' . DOKU_LF;
211
212            return true;
213        }
214        return false;
215    }
216
217    /**
218     * @param $id
219     * @param $max
220     * @return array
221     */
222    public function related($id, $max = NULL): array
223    {
224        if ($max == NULL) {
225            $max = $this->getConf(self::MAX_LINKS_CONF);
226        }
227        // Call the dokuwiki backlinks function
228        // @require_once(DOKU_INC . 'inc/fulltext.php');
229        // Backlinks called the indexer, for more info
230        // See: https://www.dokuwiki.org/devel:metadata#metadata_index
231        $backlinks = ft_backlinks($id, $ignore_perms = false);
232
233        // To minimize the pressure on the index
234        // as we asks then the backlinks of the backlinks on the next step
235        if (sizeof($backlinks) > 50) {
236            $backlinks = array_slice($backlinks, 0, 50);
237        }
238
239        $related = array();
240        foreach ($backlinks as $backlink) {
241            $page = array();
242            $page[self::RELATED_PAGE_ID_PROP] = $backlink;
243            $page[self::RELATED_BACKLINKS_COUNT_PROP] = sizeof(ft_backlinks($backlink, $ignore_perms = false));
244            $related[] = $page;
245        }
246
247        usort($related, function ($a, $b) {
248            return $b[self::RELATED_BACKLINKS_COUNT_PROP] - $a[self::RELATED_BACKLINKS_COUNT_PROP];
249        });
250
251        if (sizeof($related) > $max) {
252            $related = array_slice($related, 0, $max);
253            $page = array();
254            $page[self::RELATED_PAGE_ID_PROP] = self::MORE_PAGE_ID;
255            $related[] = $page;
256        }
257
258        return $related;
259
260    }
261
262    public static function getTag(): string
263    {
264        return "related";
265    }
266
267
268}
269