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