xref: /plugin/dwtimeline/syntax/renderpagetimeline.php (revision 545c554be65100e9bb5eff6d4aa54c829680442a)
1<?php
2
3/**
4 * DokuWiki Plugin dwtimeline (Syntax Component: renderpage timeline)
5 * Renders <dwtimeline page=ns:page /> by reusing the plugin's own markup.
6 *
7 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
8 * @author  saggi
9 */
10
11use dokuwiki\File\PageResolver;
12
13class syntax_plugin_dwtimeline_renderpagetimeline extends syntax_plugin_dwtimeline_dwtimeline
14{
15    public function getPType()
16    {
17        return 'block';
18    }
19
20    /**
21     * Recognize: <dwtimeline page=ns:page />
22     */
23    public function connectTo($mode)
24    {
25        $this->Lexer->addSpecialPattern(
26            '<dwtimeline\s+page\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s/>]+)\s*/>',
27            $mode,
28            'plugin_dwtimeline_renderpagetimeline'
29        );
30    }
31
32    /**
33     * Extract and resolve the target page id.
34     */
35    public function handle($match, $state, $pos, Doku_Handler $handler)
36    {
37        if (preg_match('/page\s*=\s*"([^"]*)"/i', $match, $m)) {
38            $raw = trim($m[1]);
39        } elseif (preg_match('/page\s*=\s*([^\s\/>]+)/i', $match, $m)) {
40            $raw = trim($m[1]);
41        } else {
42            $raw = '';
43        }
44
45        $id = cleanID((string)$raw);
46
47        global $ID;
48        $resolver = new PageResolver($ID);
49        $id       = $resolver->resolveId($id); // may resolve to a non-existing page (by design)
50
51        return [
52            'id'  => $id,
53            'pos' => $pos,
54        ];
55    }
56
57    /**
58     * Render metadata (references) and XHTML output.
59     */
60    public function render($mode, Doku_Renderer $renderer, $data)
61    {
62        // --- METADATA: persist backlink/reference only here ---
63        if ($mode === 'metadata') {
64            global $ID;
65            $target = $data['id'] ?? '';
66            if ($target && $target !== $ID && page_exists($target)) {
67                if (!isset($renderer->meta['relation']['references'])) {
68                    $renderer->meta['relation']['references'] = [];
69                }
70                $renderer->meta['relation']['references'][] = $target;
71            }
72            return true;
73        }
74
75        if ($mode !== 'xhtml') {
76            return false;
77        }
78
79        global $ID;
80
81        $target = $data['id'] ?? '';
82        if ($target === '') {
83            $renderer->doc .= $this->err('rp_missing_id');
84            return true;
85        }
86
87        // Permission first (avoid existence leaks)
88        if (auth_quickaclcheck($target) < AUTH_READ) {
89            $renderer->doc .= $this->err('rp_no_acl', [$target]);
90            return true;
91        }
92
93        // Existence check
94        if (!page_exists($target)) {
95            $renderer->doc .= $this->err('rp_not_found', [$target]);
96            return true;
97        }
98
99        // Self-include guard
100        if ($target === $ID) {
101            $renderer->doc .= $this->err('rp_same', [$target]);
102            return true;
103        }
104
105        // Cache dependency on the source page (so changes there invalidate this page)
106        $renderer->info['depends']['pages'][] = $target;
107
108        // Read source wikitext
109        $wikitext = rawWiki($target);
110        if ($wikitext === null) {
111            $renderer->doc .= $this->err('rp_not_found', [$target]);
112            return true;
113        }
114
115        // Parse instructions (headers provide [text, level, pos])
116        $instr = p_get_instructions($wikitext);
117
118        // Collect headers
119        $headers = [];
120        foreach ($instr as $idx => $ins) {
121            if ($ins[0] !== 'header') {
122                continue;
123            }
124            $text      = $ins[1][0] ?? '';
125            $level     = (int)($ins[1][1] ?? 0);
126            $pos       = (int)($ins[1][2] ?? -1); // may be -1 on older DW versions
127            $headers[] = ['idx' => $idx, 'text' => $text, 'level' => $level, 'pos' => $pos];
128        }
129
130        // Determine timeline title: prefer first H1, fallback to first header, then prettyId
131        $titleHdr = null;
132        foreach ($headers as $h) {
133            if ($h['level'] === 1) {
134                $titleHdr = $h;
135                break;
136            }
137        }
138        if ($titleHdr === null && !empty($headers)) {
139            $titleHdr = $headers[0];
140        }
141
142        $title      = $titleHdr ? trim($titleHdr['text']) : $this->prettyId($target);
143        $titleLevel = $titleHdr ? (int)$titleHdr['level'] : 1;
144        $milLevel   = $titleLevel + 1;
145
146        // Build synthetic <dwtimeline> markup using existing plugin tags
147        $align     = (string)$this->getConf('align');
148        $synthetic = $this->buildSyntheticTimeline($wikitext, $headers, $title, $milLevel, $align);
149
150        // Render the synthetic markup through DokuWiki (uses your existing syntax classes)
151        $info = [];
152        $html = p_render('xhtml', p_get_instructions($synthetic), $info);
153
154        // Output + small source link
155        $renderer->doc .= $html;
156
157        $info2         = [];
158        $renderer->doc .= p_render('xhtml', p_get_instructions('[[' . $target . ']]'), $info2);
159
160        return true;
161    }
162
163    /**
164     * Build the synthetic DokuWiki markup for the timeline using the plugin's own tags.
165     *
166     * @param string $wikitext Raw wikitext of the source page
167     * @param array  $headers  List of headers: ['text'=>string,'level'=>int,'pos'=>int]
168     * @param string $title    Timeline title (from H1 or fallback)
169     * @param int    $milLevel Milestone level (titleLevel + 1, typically H2)
170     * @param string $align    Alignment taken from plugin config
171     * @return string           Complete <dwtimeline ...> ... </dwtimeline> markup
172     */
173    public function buildSyntheticTimeline(
174        string $wikitext,
175        array $headers,
176        string $title,
177        int $milLevel,
178        string $align
179    ): string {
180        $len       = strlen($wikitext); // byte-based; header positions are byte offsets
181        $synthetic = '<dwtimeline align="' . $align . '" title=' . $this->quoteAttrForWiki($title) . '>' . DOKU_LF;
182
183        $count = count($headers);
184        for ($i = 0; $i < $count; $i++) {
185            $h = $headers[$i];
186            if (($h['level'] ?? null) !== $milLevel) {
187                continue;
188            }
189
190            // If positions are not available, we cannot safely cut body text; emit empty content
191            if (!isset($h['pos']) || $h['pos'] < 0) {
192                $synthetic .= '<milestone title=';
193                $synthetic .= $this->quoteAttrForWiki((string)$h['text']);
194                $synthetic .= ' data="' . $i . '">' . DOKU_LF;
195                $synthetic .= '</milestone>' . DOKU_LF;
196                continue;
197            }
198
199            // Start right after the milestone header line
200            $start = $this->lineEndAt($wikitext, (int)$h['pos'], $len);
201
202            // End at the line start of the next header with level <= $milLevel (or EOF)
203            $end = $len;
204            for ($j = $i + 1; $j < $count; $j++) {
205                $hn = $headers[$j];
206                if (($hn['level'] ?? PHP_INT_MAX) <= $milLevel) {
207                    $nextPos = (int)($hn['pos'] ?? -1);
208                    // If next header position is missing, fall back to EOF
209                    if ($nextPos >= 0) {
210                        $end = $this->lineStartAt($wikitext, $nextPos);
211                    }
212                    break;
213                }
214            }
215
216            $section = $this->cutSection($wikitext, $start, $end);
217
218            // Emit milestone with title attribute and body content
219            $synthetic .= '<milestone title=';
220            $synthetic .= $this->quoteAttrForWiki((string)$h['text']);
221            $synthetic .= ' data="' . $i . '">' . DOKU_LF;
222            $synthetic .= $section . DOKU_LF;
223            $synthetic .= '</milestone>' . DOKU_LF;
224        }
225
226        $synthetic .= '</dwtimeline>' . DOKU_LF;
227        return $synthetic;
228    }
229}
230