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