xref: /plugin/combo/syntax/related.php (revision c3437056399326d621a01da73b649707fbb0ae69)
1007225e5Sgerardnico<?php
2007225e5Sgerardnico/**
3007225e5Sgerardnico * DokuWiki Syntax Plugin Related.
4007225e5Sgerardnico *
5007225e5Sgerardnico */
6007225e5Sgerardnico
71fa8c418SNickeauuse ComboStrap\LinkUtility;
8ef295d81Sgerardnicouse ComboStrap\Page;
9007225e5Sgerardnicouse ComboStrap\PluginUtility;
101fa8c418SNickeauuse ComboStrap\TagAttributes;
11007225e5Sgerardnico
12007225e5Sgerardnico
13007225e5Sgerardnicorequire_once(DOKU_INC . 'inc/parserutils.php');
14007225e5Sgerardnico
15007225e5Sgerardnico/**
16007225e5Sgerardnico * All DokuWiki plugins to extend the parser/rendering mechanism
17007225e5Sgerardnico * need to inherit from this class
18007225e5Sgerardnico *
19007225e5Sgerardnico * The name of the class must follow a pattern (don't change it)
20007225e5Sgerardnico *
21007225e5Sgerardnico * The index and the metadata key for backlinks is  called 'relation_references'
22007225e5Sgerardnico * It's the key value that you need to pass in the {@link lookupKey} of the {@link \dokuwiki\Search\Indexer}
23007225e5Sgerardnico *
24007225e5Sgerardnico * Type of conf[index]/index:
25007225e5Sgerardnico *   * page.idx (id of the page is the element number)
26007225e5Sgerardnico *   * title
27007225e5Sgerardnico *   * relation_references_w.idx - _w for words
28007225e5Sgerardnico *   * relation_references_w.idx - _i for lines (index by lines)
29007225e5Sgerardnico *
30007225e5Sgerardnico * The index is a associative map by key
31007225e5Sgerardnico *
32007225e5Sgerardnico *
33007225e5Sgerardnico */
34007225e5Sgerardnicoclass syntax_plugin_combo_related extends DokuWiki_Syntax_Plugin
35007225e5Sgerardnico{
36007225e5Sgerardnico
37007225e5Sgerardnico
38007225e5Sgerardnico    // Conf property key
39007225e5Sgerardnico    const MAX_LINKS_CONF = 'maxLinks';
40*c3437056SNickeau    const MAX_LINKS_CONF_DEFAULT = 10;
41007225e5Sgerardnico    // For when you come from another plugin (such as backlinks) and that you don't want to change the pattern on each page
42007225e5Sgerardnico    const EXTRA_PATTERN_CONF = 'extra_pattern';
43007225e5Sgerardnico
44007225e5Sgerardnico    // This is a fake page ID that is added
45007225e5Sgerardnico    // to the related page array when the number of backlinks is bigger than the max
46007225e5Sgerardnico    // Poisoning object strategy
47007225e5Sgerardnico    const MORE_PAGE_ID = 'related_more';
48007225e5Sgerardnico
49007225e5Sgerardnico    // The array key of an array of related page
50007225e5Sgerardnico    const RELATED_PAGE_ID_PROP = 'id';
51007225e5Sgerardnico    const RELATED_BACKLINKS_COUNT_PROP = 'backlinks';
52007225e5Sgerardnico
53007225e5Sgerardnico
54007225e5Sgerardnico    /**
55*c3437056SNickeau     * @param Page $page
56*c3437056SNickeau     * @param int|null $max
57*c3437056SNickeau     * @param null $renderer
58*c3437056SNickeau     * @return string
59*c3437056SNickeau     */
60*c3437056SNickeau    public static function getHtmlRelated(Page $page, ?int $max = null, $renderer = null): string
61*c3437056SNickeau    {
62*c3437056SNickeau        global $lang;
63*c3437056SNickeau
64*c3437056SNickeau        $tagAttributes = TagAttributes::createEmpty(self::getTag());
65*c3437056SNickeau        $tagAttributes->addClassName("d-print-none");
66*c3437056SNickeau        $html = $tagAttributes->toHtmlEnterTag("div");
67*c3437056SNickeau
68*c3437056SNickeau        $relatedPages = self::getRelatedPagesOrderedByBacklinkCount($page, $max);
69*c3437056SNickeau        if (empty($relatedPages)) {
70*c3437056SNickeau
71*c3437056SNickeau            $html .= "<strong>Plugin " . PluginUtility::PLUGIN_BASE_NAME . " - Component " . self::getTag() . ": " . $lang['nothingfound'] . "</strong>" . DOKU_LF;
72*c3437056SNickeau
73*c3437056SNickeau        } else {
74*c3437056SNickeau
75*c3437056SNickeau            // Dokuwiki debug
76*c3437056SNickeau
77*c3437056SNickeau            $html .= '<ul>' . DOKU_LF;
78*c3437056SNickeau
79*c3437056SNickeau            foreach ($relatedPages as $backlink) {
80*c3437056SNickeau                $backlinkId = $backlink[self::RELATED_PAGE_ID_PROP];
81*c3437056SNickeau                $html .= '<li>';
82*c3437056SNickeau                if ($backlinkId != self::MORE_PAGE_ID) {
83*c3437056SNickeau                    $linkUtility = LinkUtility::createFromPageId($backlinkId);
84*c3437056SNickeau                    $html .= $linkUtility->renderOpenTag($renderer);
85*c3437056SNickeau                    $html .= ucfirst($linkUtility->getName());
86*c3437056SNickeau                    $html .= $linkUtility->renderClosingTag();
87*c3437056SNickeau                } else {
88*c3437056SNickeau                    $html .=
89*c3437056SNickeau                        tpl_link(
90*c3437056SNickeau                            wl($page->getDokuwikiId()) . '?do=backlink',
91*c3437056SNickeau                            "More ...",
92*c3437056SNickeau                            'class="" rel="nofollow" title="More..."',
93*c3437056SNickeau                            true
94*c3437056SNickeau                        );
95*c3437056SNickeau                }
96*c3437056SNickeau                $html .= '</li>' . DOKU_LF;
97*c3437056SNickeau            }
98*c3437056SNickeau
99*c3437056SNickeau            $html .= '</ul>' . DOKU_LF;
100*c3437056SNickeau
101*c3437056SNickeau        }
102*c3437056SNickeau
103*c3437056SNickeau        return $html . '</div>' . DOKU_LF;
104*c3437056SNickeau    }
105*c3437056SNickeau
106*c3437056SNickeau
107*c3437056SNickeau    /**
108007225e5Sgerardnico     * Syntax Type.
109007225e5Sgerardnico     *
110007225e5Sgerardnico     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
111007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::getType()
112007225e5Sgerardnico     */
113007225e5Sgerardnico    function getType()
114007225e5Sgerardnico    {
115007225e5Sgerardnico        return 'substition';
116007225e5Sgerardnico    }
117007225e5Sgerardnico
118007225e5Sgerardnico    /**
119007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::getPType()
120007225e5Sgerardnico     */
121007225e5Sgerardnico    function getPType()
122007225e5Sgerardnico    {
123007225e5Sgerardnico        return 'block';
124007225e5Sgerardnico    }
125007225e5Sgerardnico
126007225e5Sgerardnico    /**
127007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
128007225e5Sgerardnico     */
129007225e5Sgerardnico    function getSort()
130007225e5Sgerardnico    {
131007225e5Sgerardnico        return 100;
132007225e5Sgerardnico    }
133007225e5Sgerardnico
134007225e5Sgerardnico    /**
135007225e5Sgerardnico     * Create a pattern that will called this plugin
136007225e5Sgerardnico     *
137007225e5Sgerardnico     * @param string $mode
138007225e5Sgerardnico     * @see Doku_Parser_Mode::connectTo()
139007225e5Sgerardnico     */
140007225e5Sgerardnico    function connectTo($mode)
141007225e5Sgerardnico    {
142007225e5Sgerardnico        // The basic
143*c3437056SNickeau        $this->Lexer->addSpecialPattern(PluginUtility::getVoidElementTagPattern(self::getTag()), $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
144007225e5Sgerardnico
145007225e5Sgerardnico        // To replace backlinks, you may add it in the configuration
146007225e5Sgerardnico        $extraPattern = $this->getConf(self::EXTRA_PATTERN_CONF);
147007225e5Sgerardnico        if ($extraPattern != "") {
148007225e5Sgerardnico            $this->Lexer->addSpecialPattern($extraPattern, $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
149007225e5Sgerardnico        }
150007225e5Sgerardnico
151007225e5Sgerardnico    }
152007225e5Sgerardnico
153007225e5Sgerardnico    /**
154007225e5Sgerardnico     *
155007225e5Sgerardnico     * The handle function goal is to parse the matched syntax through the pattern function
156007225e5Sgerardnico     * and to return the result for use in the renderer
157007225e5Sgerardnico     * This result is always cached until the page is modified.
158007225e5Sgerardnico     * @param string $match
159007225e5Sgerardnico     * @param int $state
160007225e5Sgerardnico     * @param int $pos
161007225e5Sgerardnico     * @param Doku_Handler $handler
162007225e5Sgerardnico     * @return array|bool
163007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::handle()
164007225e5Sgerardnico     *
165007225e5Sgerardnico     */
166007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
167007225e5Sgerardnico    {
168007225e5Sgerardnico
169007225e5Sgerardnico        switch ($state) {
170007225e5Sgerardnico
171007225e5Sgerardnico            // As there is only one call to connect to in order to a add a pattern,
172007225e5Sgerardnico            // there is only one state entering the function
173007225e5Sgerardnico            // but I leave it for better understanding of the process flow
174007225e5Sgerardnico            case DOKU_LEXER_SPECIAL :
175007225e5Sgerardnico
176*c3437056SNickeau                $qualifiedMach = trim($match);
177*c3437056SNickeau                $attributes = [];
178*c3437056SNickeau                if ($qualifiedMach[0] === "<") {
179*c3437056SNickeau                    // not an extra pattern
180*c3437056SNickeau                    $tagAttributes = TagAttributes::createFromTagMatch($match);
181*c3437056SNickeau                    $attributes = $tagAttributes->toCallStackArray();
182007225e5Sgerardnico                }
183*c3437056SNickeau                return array(
184*c3437056SNickeau                    PluginUtility::STATE => $state,
185*c3437056SNickeau                    PluginUtility::ATTRIBUTES => $attributes
186*c3437056SNickeau                );
187007225e5Sgerardnico
188007225e5Sgerardnico        }
189007225e5Sgerardnico
190007225e5Sgerardnico        // Cache the values
191007225e5Sgerardnico        return array($state);
192007225e5Sgerardnico    }
193007225e5Sgerardnico
194007225e5Sgerardnico    /**
195007225e5Sgerardnico     * Render the output
196007225e5Sgerardnico     * @param string $format
197007225e5Sgerardnico     * @param Doku_Renderer $renderer
198007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
199007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
200007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
201007225e5Sgerardnico     *
202007225e5Sgerardnico     *
203007225e5Sgerardnico     */
204007225e5Sgerardnico    function render($format, Doku_Renderer $renderer, $data)
205007225e5Sgerardnico    {
206007225e5Sgerardnico
207007225e5Sgerardnico
208007225e5Sgerardnico        if ($format == 'xhtml') {
209007225e5Sgerardnico
210*c3437056SNickeau            $page = Page::createPageFromRequestedPage();
211*c3437056SNickeau            $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
212*c3437056SNickeau            $max = $tagAttributes->getValue(self::MAX_LINKS_CONF);
213*c3437056SNickeau            if ($max === NULL) {
214*c3437056SNickeau                $max = PluginUtility::getConfValue(self::MAX_LINKS_CONF, self::MAX_LINKS_CONF_DEFAULT);
215007225e5Sgerardnico            }
216*c3437056SNickeau            $renderer->doc .= self::getHtmlRelated($page, $max, $renderer);
217007225e5Sgerardnico            return true;
218007225e5Sgerardnico        }
219007225e5Sgerardnico        return false;
220007225e5Sgerardnico    }
221007225e5Sgerardnico
222007225e5Sgerardnico    /**
223*c3437056SNickeau     * @param Page $page
224*c3437056SNickeau     * @param int|null $max
225007225e5Sgerardnico     * @return array
226007225e5Sgerardnico     */
227*c3437056SNickeau    public static function getRelatedPagesOrderedByBacklinkCount(Page $page, ?int $max = null): array
228007225e5Sgerardnico    {
229*c3437056SNickeau
230007225e5Sgerardnico        // Call the dokuwiki backlinks function
231007225e5Sgerardnico        // @require_once(DOKU_INC . 'inc/fulltext.php');
232007225e5Sgerardnico        // Backlinks called the indexer, for more info
233007225e5Sgerardnico        // See: https://www.dokuwiki.org/devel:metadata#metadata_index
234*c3437056SNickeau        $backlinks = ft_backlinks($page->getDokuwikiId(), $ignore_perms = false);
235007225e5Sgerardnico
236007225e5Sgerardnico        $related = array();
237007225e5Sgerardnico        foreach ($backlinks as $backlink) {
238007225e5Sgerardnico            $page = array();
239007225e5Sgerardnico            $page[self::RELATED_PAGE_ID_PROP] = $backlink;
240007225e5Sgerardnico            $page[self::RELATED_BACKLINKS_COUNT_PROP] = sizeof(ft_backlinks($backlink, $ignore_perms = false));
241007225e5Sgerardnico            $related[] = $page;
242007225e5Sgerardnico        }
243007225e5Sgerardnico
244007225e5Sgerardnico        usort($related, function ($a, $b) {
245007225e5Sgerardnico            return $b[self::RELATED_BACKLINKS_COUNT_PROP] - $a[self::RELATED_BACKLINKS_COUNT_PROP];
246007225e5Sgerardnico        });
247007225e5Sgerardnico
248*c3437056SNickeau        if ($max !== null) {
249007225e5Sgerardnico            if (sizeof($related) > $max) {
250007225e5Sgerardnico                $related = array_slice($related, 0, $max);
251007225e5Sgerardnico                $page = array();
252007225e5Sgerardnico                $page[self::RELATED_PAGE_ID_PROP] = self::MORE_PAGE_ID;
253007225e5Sgerardnico                $related[] = $page;
254007225e5Sgerardnico            }
255*c3437056SNickeau        }
256007225e5Sgerardnico
257007225e5Sgerardnico        return $related;
258007225e5Sgerardnico
259007225e5Sgerardnico    }
260007225e5Sgerardnico
2611fa8c418SNickeau    public static function getTag(): string
262007225e5Sgerardnico    {
263007225e5Sgerardnico        return "related";
264007225e5Sgerardnico    }
265007225e5Sgerardnico
266007225e5Sgerardnico
267007225e5Sgerardnico}
268