xref: /plugin/combo/syntax/related.php (revision 4cadd4f8c541149bdda95f080e38a6d4e3a640ca) !
1007225e5Sgerardnico<?php
2007225e5Sgerardnico/**
3007225e5Sgerardnico * DokuWiki Syntax Plugin Related.
4007225e5Sgerardnico *
5007225e5Sgerardnico */
6007225e5Sgerardnico
7*4cadd4f8SNickeauuse ComboStrap\ExceptionCombo;
8*4cadd4f8SNickeauuse ComboStrap\LogUtility;
9*4cadd4f8SNickeauuse ComboStrap\MarkupRef;
10ef295d81Sgerardnicouse ComboStrap\Page;
11007225e5Sgerardnicouse ComboStrap\PluginUtility;
121fa8c418SNickeauuse ComboStrap\TagAttributes;
13007225e5Sgerardnico
14007225e5Sgerardnico
15007225e5Sgerardnicorequire_once(DOKU_INC . 'inc/parserutils.php');
16007225e5Sgerardnico
17007225e5Sgerardnico/**
18007225e5Sgerardnico * All DokuWiki plugins to extend the parser/rendering mechanism
19007225e5Sgerardnico * need to inherit from this class
20007225e5Sgerardnico *
21007225e5Sgerardnico * The name of the class must follow a pattern (don't change it)
22007225e5Sgerardnico *
23007225e5Sgerardnico * The index and the metadata key for backlinks is  called 'relation_references'
24007225e5Sgerardnico * It's the key value that you need to pass in the {@link lookupKey} of the {@link \dokuwiki\Search\Indexer}
25007225e5Sgerardnico *
26007225e5Sgerardnico * Type of conf[index]/index:
27007225e5Sgerardnico *   * page.idx (id of the page is the element number)
28007225e5Sgerardnico *   * title
29007225e5Sgerardnico *   * relation_references_w.idx - _w for words
30007225e5Sgerardnico *   * relation_references_w.idx - _i for lines (index by lines)
31007225e5Sgerardnico *
32007225e5Sgerardnico * The index is a associative map by key
33007225e5Sgerardnico *
34007225e5Sgerardnico *
35007225e5Sgerardnico */
36007225e5Sgerardnicoclass syntax_plugin_combo_related extends DokuWiki_Syntax_Plugin
37007225e5Sgerardnico{
38007225e5Sgerardnico
39007225e5Sgerardnico
40007225e5Sgerardnico    // Conf property key
41007225e5Sgerardnico    const MAX_LINKS_CONF = 'maxLinks';
42c3437056SNickeau    const MAX_LINKS_CONF_DEFAULT = 10;
43007225e5Sgerardnico    // For when you come from another plugin (such as backlinks) and that you don't want to change the pattern on each page
44007225e5Sgerardnico    const EXTRA_PATTERN_CONF = 'extra_pattern';
45007225e5Sgerardnico
46007225e5Sgerardnico    // This is a fake page ID that is added
47007225e5Sgerardnico    // to the related page array when the number of backlinks is bigger than the max
48007225e5Sgerardnico    // Poisoning object strategy
49007225e5Sgerardnico    const MORE_PAGE_ID = 'related_more';
50007225e5Sgerardnico
51007225e5Sgerardnico    // The array key of an array of related page
52007225e5Sgerardnico    const RELATED_PAGE_ID_PROP = 'id';
53007225e5Sgerardnico    const RELATED_BACKLINKS_COUNT_PROP = 'backlinks';
54*4cadd4f8SNickeau    const TAG = "related";
55007225e5Sgerardnico
56007225e5Sgerardnico
57007225e5Sgerardnico    /**
58c3437056SNickeau     * @param Page $page
59c3437056SNickeau     * @param int|null $max
60c3437056SNickeau     * @return string
61c3437056SNickeau     */
62*4cadd4f8SNickeau    public static function getHtmlRelated(Page $page, ?int $max = null): string
63c3437056SNickeau    {
64c3437056SNickeau        global $lang;
65c3437056SNickeau
66c3437056SNickeau        $tagAttributes = TagAttributes::createEmpty(self::getTag());
67c3437056SNickeau        $tagAttributes->addClassName("d-print-none");
68c3437056SNickeau        $html = $tagAttributes->toHtmlEnterTag("div");
69c3437056SNickeau
70c3437056SNickeau        $relatedPages = self::getRelatedPagesOrderedByBacklinkCount($page, $max);
71c3437056SNickeau        if (empty($relatedPages)) {
72c3437056SNickeau
73c3437056SNickeau            $html .= "<strong>Plugin " . PluginUtility::PLUGIN_BASE_NAME . " - Component " . self::getTag() . ": " . $lang['nothingfound'] . "</strong>" . DOKU_LF;
74c3437056SNickeau
75c3437056SNickeau        } else {
76c3437056SNickeau
77c3437056SNickeau            // Dokuwiki debug
78c3437056SNickeau
79c3437056SNickeau            $html .= '<ul>' . DOKU_LF;
80c3437056SNickeau
81c3437056SNickeau            foreach ($relatedPages as $backlink) {
82c3437056SNickeau                $backlinkId = $backlink[self::RELATED_PAGE_ID_PROP];
83c3437056SNickeau                $html .= '<li>';
84c3437056SNickeau                if ($backlinkId != self::MORE_PAGE_ID) {
85*4cadd4f8SNickeau                    $linkUtility = MarkupRef::createFromPageId($backlinkId);
86*4cadd4f8SNickeau                    try {
87*4cadd4f8SNickeau                        $html .= $linkUtility->toAttributes(self::TAG)->toHtmlEnterTag("a");
88*4cadd4f8SNickeau                        $html .= $linkUtility->getLabel();
89*4cadd4f8SNickeau                        $html .= "</a>";
90*4cadd4f8SNickeau                    } catch (ExceptionCombo $e) {
91*4cadd4f8SNickeau                        $html = "Error while trying to create the link for the page ($backlinkId). Error: {$e->getMessage()}";
92*4cadd4f8SNickeau                        LogUtility::msg($html);
93*4cadd4f8SNickeau                    }
94*4cadd4f8SNickeau
95c3437056SNickeau                } else {
96c3437056SNickeau                    $html .=
97c3437056SNickeau                        tpl_link(
98c3437056SNickeau                            wl($page->getDokuwikiId()) . '?do=backlink',
99c3437056SNickeau                            "More ...",
100c3437056SNickeau                            'class="" rel="nofollow" title="More..."',
101c3437056SNickeau                            true
102c3437056SNickeau                        );
103c3437056SNickeau                }
104c3437056SNickeau                $html .= '</li>' . DOKU_LF;
105c3437056SNickeau            }
106c3437056SNickeau
107c3437056SNickeau            $html .= '</ul>' . DOKU_LF;
108c3437056SNickeau
109c3437056SNickeau        }
110c3437056SNickeau
111c3437056SNickeau        return $html . '</div>' . DOKU_LF;
112c3437056SNickeau    }
113c3437056SNickeau
114c3437056SNickeau
115c3437056SNickeau    /**
116007225e5Sgerardnico     * Syntax Type.
117007225e5Sgerardnico     *
118007225e5Sgerardnico     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
119007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::getType()
120007225e5Sgerardnico     */
121007225e5Sgerardnico    function getType()
122007225e5Sgerardnico    {
123007225e5Sgerardnico        return 'substition';
124007225e5Sgerardnico    }
125007225e5Sgerardnico
126007225e5Sgerardnico    /**
127007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::getPType()
128007225e5Sgerardnico     */
129007225e5Sgerardnico    function getPType()
130007225e5Sgerardnico    {
131007225e5Sgerardnico        return 'block';
132007225e5Sgerardnico    }
133007225e5Sgerardnico
134007225e5Sgerardnico    /**
135007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
136007225e5Sgerardnico     */
137007225e5Sgerardnico    function getSort()
138007225e5Sgerardnico    {
139007225e5Sgerardnico        return 100;
140007225e5Sgerardnico    }
141007225e5Sgerardnico
142007225e5Sgerardnico    /**
143007225e5Sgerardnico     * Create a pattern that will called this plugin
144007225e5Sgerardnico     *
145007225e5Sgerardnico     * @param string $mode
146007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
147007225e5Sgerardnico     */
148007225e5Sgerardnico    function connectTo($mode)
149007225e5Sgerardnico    {
150007225e5Sgerardnico        // The basic
151c3437056SNickeau        $this->Lexer->addSpecialPattern(PluginUtility::getVoidElementTagPattern(self::getTag()), $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
152007225e5Sgerardnico
153007225e5Sgerardnico        // To replace backlinks, you may add it in the configuration
154007225e5Sgerardnico        $extraPattern = $this->getConf(self::EXTRA_PATTERN_CONF);
155007225e5Sgerardnico        if ($extraPattern != "") {
156007225e5Sgerardnico            $this->Lexer->addSpecialPattern($extraPattern, $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
157007225e5Sgerardnico        }
158007225e5Sgerardnico
159007225e5Sgerardnico    }
160007225e5Sgerardnico
161007225e5Sgerardnico    /**
162007225e5Sgerardnico     *
163007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
164007225e5Sgerardnico     * and to return the result for use in the renderer
165007225e5Sgerardnico     * This result is always cached until the page is modified.
166007225e5Sgerardnico     * @param string $match
167007225e5Sgerardnico     * @param int $state
168007225e5Sgerardnico     * @param int $pos
169007225e5Sgerardnico     * @param Doku_Handler $handler
170007225e5Sgerardnico     * @return array|bool
171007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
172007225e5Sgerardnico     *
173007225e5Sgerardnico     */
174007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
175007225e5Sgerardnico    {
176007225e5Sgerardnico
177007225e5Sgerardnico        switch ($state) {
178007225e5Sgerardnico
179007225e5Sgerardnico            // As there is only one call to connect to in order to a add a pattern,
180007225e5Sgerardnico            // there is only one state entering the function
181007225e5Sgerardnico            // but I leave it for better understanding of the process flow
182007225e5Sgerardnico            case DOKU_LEXER_SPECIAL :
183007225e5Sgerardnico
184c3437056SNickeau                $qualifiedMach = trim($match);
185c3437056SNickeau                $attributes = [];
186c3437056SNickeau                if ($qualifiedMach[0] === "<") {
187c3437056SNickeau                    // not an extra pattern
188c3437056SNickeau                    $tagAttributes = TagAttributes::createFromTagMatch($match);
189c3437056SNickeau                    $attributes = $tagAttributes->toCallStackArray();
190007225e5Sgerardnico                }
191c3437056SNickeau                return array(
192c3437056SNickeau                    PluginUtility::STATE => $state,
193c3437056SNickeau                    PluginUtility::ATTRIBUTES => $attributes
194c3437056SNickeau                );
195007225e5Sgerardnico
196007225e5Sgerardnico        }
197007225e5Sgerardnico
198007225e5Sgerardnico        // Cache the values
199007225e5Sgerardnico        return array($state);
200007225e5Sgerardnico    }
201007225e5Sgerardnico
202007225e5Sgerardnico    /**
203007225e5Sgerardnico     * Render the output
204007225e5Sgerardnico     * @param string $format
205007225e5Sgerardnico     * @param Doku_Renderer $renderer
206007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
207007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
208007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
209007225e5Sgerardnico     *
210007225e5Sgerardnico     *
211007225e5Sgerardnico     */
212007225e5Sgerardnico    function render($format, Doku_Renderer $renderer, $data)
213007225e5Sgerardnico    {
214007225e5Sgerardnico
215007225e5Sgerardnico
216007225e5Sgerardnico        if ($format == 'xhtml') {
217007225e5Sgerardnico
218c3437056SNickeau            $page = Page::createPageFromRequestedPage();
219c3437056SNickeau            $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
220c3437056SNickeau            $max = $tagAttributes->getValue(self::MAX_LINKS_CONF);
221c3437056SNickeau            if ($max === NULL) {
222c3437056SNickeau                $max = PluginUtility::getConfValue(self::MAX_LINKS_CONF, self::MAX_LINKS_CONF_DEFAULT);
223007225e5Sgerardnico            }
224*4cadd4f8SNickeau            $renderer->doc .= self::getHtmlRelated($page, $max);
225007225e5Sgerardnico            return true;
226007225e5Sgerardnico        }
227007225e5Sgerardnico        return false;
228007225e5Sgerardnico    }
229007225e5Sgerardnico
230007225e5Sgerardnico    /**
231c3437056SNickeau     * @param Page $page
232c3437056SNickeau     * @param int|null $max
233007225e5Sgerardnico     * @return array
234007225e5Sgerardnico     */
235c3437056SNickeau    public static function getRelatedPagesOrderedByBacklinkCount(Page $page, ?int $max = null): array
236007225e5Sgerardnico    {
237c3437056SNickeau
238007225e5Sgerardnico        // Call the dokuwiki backlinks function
239007225e5Sgerardnico        // @require_once(DOKU_INC . 'inc/fulltext.php');
240007225e5Sgerardnico        // Backlinks called the indexer, for more info
241007225e5Sgerardnico        // See: https://www.dokuwiki.org/devel:metadata#metadata_index
242c3437056SNickeau        $backlinks = ft_backlinks($page->getDokuwikiId(), $ignore_perms = false);
243007225e5Sgerardnico
244007225e5Sgerardnico        $related = array();
245007225e5Sgerardnico        foreach ($backlinks as $backlink) {
246007225e5Sgerardnico            $page = array();
247007225e5Sgerardnico            $page[self::RELATED_PAGE_ID_PROP] = $backlink;
248007225e5Sgerardnico            $page[self::RELATED_BACKLINKS_COUNT_PROP] = sizeof(ft_backlinks($backlink, $ignore_perms = false));
249007225e5Sgerardnico            $related[] = $page;
250007225e5Sgerardnico        }
251007225e5Sgerardnico
252007225e5Sgerardnico        usort($related, function ($a, $b) {
253007225e5Sgerardnico            return $b[self::RELATED_BACKLINKS_COUNT_PROP] - $a[self::RELATED_BACKLINKS_COUNT_PROP];
254007225e5Sgerardnico        });
255007225e5Sgerardnico
256c3437056SNickeau        if ($max !== null) {
257007225e5Sgerardnico            if (sizeof($related) > $max) {
258007225e5Sgerardnico                $related = array_slice($related, 0, $max);
259007225e5Sgerardnico                $page = array();
260007225e5Sgerardnico                $page[self::RELATED_PAGE_ID_PROP] = self::MORE_PAGE_ID;
261007225e5Sgerardnico                $related[] = $page;
262007225e5Sgerardnico            }
263c3437056SNickeau        }
264007225e5Sgerardnico
265007225e5Sgerardnico        return $related;
266007225e5Sgerardnico
267007225e5Sgerardnico    }
268007225e5Sgerardnico
2691fa8c418SNickeau    public static function getTag(): string
270007225e5Sgerardnico    {
271*4cadd4f8SNickeau        return self::TAG;
272007225e5Sgerardnico    }
273007225e5Sgerardnico
274007225e5Sgerardnico
275007225e5Sgerardnico}
276