xref: /plugin/dwtimeline/syntax/renderpagetimeline.php (revision adfdabd7bf1d07e505f4aef1b04dc0290dd26249)
1545c554bSsaggi-dw<?php
2545c554bSsaggi-dw
3545c554bSsaggi-dw/**
4545c554bSsaggi-dw * DokuWiki Plugin dwtimeline (Syntax Component: renderpage timeline)
5545c554bSsaggi-dw * Renders <dwtimeline page=ns:page /> by reusing the plugin's own markup.
6545c554bSsaggi-dw *
7545c554bSsaggi-dw * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
8545c554bSsaggi-dw * @author  saggi
9545c554bSsaggi-dw */
10545c554bSsaggi-dw
11545c554bSsaggi-dwuse dokuwiki\File\PageResolver;
12545c554bSsaggi-dw
13545c554bSsaggi-dwclass syntax_plugin_dwtimeline_renderpagetimeline extends syntax_plugin_dwtimeline_dwtimeline
14545c554bSsaggi-dw{
15545c554bSsaggi-dw    public function getPType()
16545c554bSsaggi-dw    {
17545c554bSsaggi-dw        return 'block';
18545c554bSsaggi-dw    }
19545c554bSsaggi-dw
20545c554bSsaggi-dw    /**
21545c554bSsaggi-dw     * Recognize: <dwtimeline page=ns:page />
22545c554bSsaggi-dw     */
23545c554bSsaggi-dw    public function connectTo($mode)
24545c554bSsaggi-dw    {
25545c554bSsaggi-dw        $this->Lexer->addSpecialPattern(
26545c554bSsaggi-dw            '<dwtimeline\s+page\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s/>]+)\s*/>',
27545c554bSsaggi-dw            $mode,
28545c554bSsaggi-dw            'plugin_dwtimeline_renderpagetimeline'
29545c554bSsaggi-dw        );
30545c554bSsaggi-dw    }
31545c554bSsaggi-dw
32545c554bSsaggi-dw    /**
33545c554bSsaggi-dw     * Extract and resolve the target page id.
34545c554bSsaggi-dw     */
35545c554bSsaggi-dw    public function handle($match, $state, $pos, Doku_Handler $handler)
36545c554bSsaggi-dw    {
37545c554bSsaggi-dw        if (preg_match('/page\s*=\s*"([^"]*)"/i', $match, $m)) {
38545c554bSsaggi-dw            $raw = trim($m[1]);
39545c554bSsaggi-dw        } elseif (preg_match('/page\s*=\s*([^\s\/>]+)/i', $match, $m)) {
40545c554bSsaggi-dw            $raw = trim($m[1]);
41545c554bSsaggi-dw        } else {
42545c554bSsaggi-dw            $raw = '';
43545c554bSsaggi-dw        }
44545c554bSsaggi-dw
45*adfdabd7Ssaggi-dw        $id = cleanID($raw);
46545c554bSsaggi-dw
47545c554bSsaggi-dw        global $ID;
48545c554bSsaggi-dw        $resolver = new PageResolver($ID);
49545c554bSsaggi-dw        $id       = $resolver->resolveId($id); // may resolve to a non-existing page (by design)
50545c554bSsaggi-dw
51545c554bSsaggi-dw        return [
52545c554bSsaggi-dw            'id'  => $id,
53545c554bSsaggi-dw            'pos' => $pos,
54545c554bSsaggi-dw        ];
55545c554bSsaggi-dw    }
56545c554bSsaggi-dw
57545c554bSsaggi-dw    /**
58545c554bSsaggi-dw     * Render metadata (references) and XHTML output.
59545c554bSsaggi-dw     */
60545c554bSsaggi-dw    public function render($mode, Doku_Renderer $renderer, $data)
61545c554bSsaggi-dw    {
62545c554bSsaggi-dw        // --- METADATA: persist backlink/reference only here ---
63545c554bSsaggi-dw        if ($mode === 'metadata') {
64545c554bSsaggi-dw            global $ID;
65545c554bSsaggi-dw            $target = $data['id'] ?? '';
66545c554bSsaggi-dw            if ($target && $target !== $ID && page_exists($target)) {
67545c554bSsaggi-dw                if (!isset($renderer->meta['relation']['references'])) {
68545c554bSsaggi-dw                    $renderer->meta['relation']['references'] = [];
69545c554bSsaggi-dw                }
70545c554bSsaggi-dw                $renderer->meta['relation']['references'][] = $target;
71545c554bSsaggi-dw            }
72545c554bSsaggi-dw            return true;
73545c554bSsaggi-dw        }
74545c554bSsaggi-dw
75545c554bSsaggi-dw        if ($mode !== 'xhtml') {
76545c554bSsaggi-dw            return false;
77545c554bSsaggi-dw        }
78545c554bSsaggi-dw
79545c554bSsaggi-dw        global $ID;
80545c554bSsaggi-dw
81545c554bSsaggi-dw        $target = $data['id'] ?? '';
82545c554bSsaggi-dw        if ($target === '') {
83545c554bSsaggi-dw            $renderer->doc .= $this->err('rp_missing_id');
84545c554bSsaggi-dw            return true;
85545c554bSsaggi-dw        }
86545c554bSsaggi-dw
87545c554bSsaggi-dw        // Permission first (avoid existence leaks)
88545c554bSsaggi-dw        if (auth_quickaclcheck($target) < AUTH_READ) {
89545c554bSsaggi-dw            $renderer->doc .= $this->err('rp_no_acl', [$target]);
90545c554bSsaggi-dw            return true;
91545c554bSsaggi-dw        }
92545c554bSsaggi-dw
93545c554bSsaggi-dw        // Existence check
94545c554bSsaggi-dw        if (!page_exists($target)) {
95545c554bSsaggi-dw            $renderer->doc .= $this->err('rp_not_found', [$target]);
96545c554bSsaggi-dw            return true;
97545c554bSsaggi-dw        }
98545c554bSsaggi-dw
99545c554bSsaggi-dw        // Self-include guard
100545c554bSsaggi-dw        if ($target === $ID) {
101545c554bSsaggi-dw            $renderer->doc .= $this->err('rp_same', [$target]);
102545c554bSsaggi-dw            return true;
103545c554bSsaggi-dw        }
104545c554bSsaggi-dw
105545c554bSsaggi-dw        // Cache dependency on the source page (so changes there invalidate this page)
106545c554bSsaggi-dw        $renderer->info['depends']['pages'][] = $target;
107545c554bSsaggi-dw
108545c554bSsaggi-dw        // Read source wikitext
109545c554bSsaggi-dw        $wikitext = rawWiki($target);
110545c554bSsaggi-dw        if ($wikitext === null) {
111545c554bSsaggi-dw            $renderer->doc .= $this->err('rp_not_found', [$target]);
112545c554bSsaggi-dw            return true;
113545c554bSsaggi-dw        }
114545c554bSsaggi-dw
115545c554bSsaggi-dw        // Parse instructions (headers provide [text, level, pos])
116545c554bSsaggi-dw        $instr = p_get_instructions($wikitext);
117545c554bSsaggi-dw
118545c554bSsaggi-dw        // Collect headers
119545c554bSsaggi-dw        $headers = [];
120545c554bSsaggi-dw        foreach ($instr as $idx => $ins) {
121545c554bSsaggi-dw            if ($ins[0] !== 'header') {
122545c554bSsaggi-dw                continue;
123545c554bSsaggi-dw            }
124545c554bSsaggi-dw            $text      = $ins[1][0] ?? '';
125545c554bSsaggi-dw            $level     = (int)($ins[1][1] ?? 0);
126545c554bSsaggi-dw            $pos       = (int)($ins[1][2] ?? -1); // may be -1 on older DW versions
127545c554bSsaggi-dw            $headers[] = ['idx' => $idx, 'text' => $text, 'level' => $level, 'pos' => $pos];
128545c554bSsaggi-dw        }
129545c554bSsaggi-dw
130545c554bSsaggi-dw        // Determine timeline title: prefer first H1, fallback to first header, then prettyId
131545c554bSsaggi-dw        $titleHdr = null;
132545c554bSsaggi-dw        foreach ($headers as $h) {
133545c554bSsaggi-dw            if ($h['level'] === 1) {
134545c554bSsaggi-dw                $titleHdr = $h;
135545c554bSsaggi-dw                break;
136545c554bSsaggi-dw            }
137545c554bSsaggi-dw        }
138*adfdabd7Ssaggi-dw        if ($titleHdr === null && $headers !== []) {
139545c554bSsaggi-dw            $titleHdr = $headers[0];
140545c554bSsaggi-dw        }
141545c554bSsaggi-dw
142545c554bSsaggi-dw        $title      = $titleHdr ? trim($titleHdr['text']) : $this->prettyId($target);
143*adfdabd7Ssaggi-dw        $titleLevel = $titleHdr ? $titleHdr['level'] : 1;
144545c554bSsaggi-dw        $milLevel   = $titleLevel + 1;
145545c554bSsaggi-dw
146545c554bSsaggi-dw        // Build synthetic <dwtimeline> markup using existing plugin tags
147545c554bSsaggi-dw        $align     = (string)$this->getConf('align');
148545c554bSsaggi-dw        $synthetic = $this->buildSyntheticTimeline($wikitext, $headers, $title, $milLevel, $align);
149545c554bSsaggi-dw
150545c554bSsaggi-dw        // Render the synthetic markup through DokuWiki (uses your existing syntax classes)
151545c554bSsaggi-dw        $info = [];
152545c554bSsaggi-dw        $html = p_render('xhtml', p_get_instructions($synthetic), $info);
153545c554bSsaggi-dw
154545c554bSsaggi-dw        // Output + small source link
155545c554bSsaggi-dw        $renderer->doc .= $html;
156545c554bSsaggi-dw
157545c554bSsaggi-dw        $info2         = [];
158545c554bSsaggi-dw        $renderer->doc .= p_render('xhtml', p_get_instructions('[[' . $target . ']]'), $info2);
159545c554bSsaggi-dw
160545c554bSsaggi-dw        return true;
161545c554bSsaggi-dw    }
162545c554bSsaggi-dw
163545c554bSsaggi-dw    /**
164545c554bSsaggi-dw     * Build the synthetic DokuWiki markup for the timeline using the plugin's own tags.
165545c554bSsaggi-dw     *
166545c554bSsaggi-dw     * @param string $wikitext Raw wikitext of the source page
167545c554bSsaggi-dw     * @param array  $headers  List of headers: ['text'=>string,'level'=>int,'pos'=>int]
168545c554bSsaggi-dw     * @param string $title    Timeline title (from H1 or fallback)
169545c554bSsaggi-dw     * @param int    $milLevel Milestone level (titleLevel + 1, typically H2)
170545c554bSsaggi-dw     * @param string $align    Alignment taken from plugin config
171545c554bSsaggi-dw     * @return string           Complete <dwtimeline ...> ... </dwtimeline> markup
172545c554bSsaggi-dw     */
173545c554bSsaggi-dw    public function buildSyntheticTimeline(
174545c554bSsaggi-dw        string $wikitext,
175545c554bSsaggi-dw        array $headers,
176545c554bSsaggi-dw        string $title,
177545c554bSsaggi-dw        int $milLevel,
178545c554bSsaggi-dw        string $align
179545c554bSsaggi-dw    ): string {
180545c554bSsaggi-dw        $len       = strlen($wikitext); // byte-based; header positions are byte offsets
181545c554bSsaggi-dw        $synthetic = '<dwtimeline align="' . $align . '" title=' . $this->quoteAttrForWiki($title) . '>' . DOKU_LF;
182545c554bSsaggi-dw
183545c554bSsaggi-dw        $count = count($headers);
184545c554bSsaggi-dw        for ($i = 0; $i < $count; $i++) {
185545c554bSsaggi-dw            $h = $headers[$i];
186545c554bSsaggi-dw            if (($h['level'] ?? null) !== $milLevel) {
187545c554bSsaggi-dw                continue;
188545c554bSsaggi-dw            }
189545c554bSsaggi-dw
190545c554bSsaggi-dw            // If positions are not available, we cannot safely cut body text; emit empty content
191545c554bSsaggi-dw            if (!isset($h['pos']) || $h['pos'] < 0) {
192545c554bSsaggi-dw                $synthetic .= '<milestone title=';
193545c554bSsaggi-dw                $synthetic .= $this->quoteAttrForWiki((string)$h['text']);
194545c554bSsaggi-dw                $synthetic .= ' data="' . $i . '">' . DOKU_LF;
195545c554bSsaggi-dw                $synthetic .= '</milestone>' . DOKU_LF;
196545c554bSsaggi-dw                continue;
197545c554bSsaggi-dw            }
198545c554bSsaggi-dw
199545c554bSsaggi-dw            // Start right after the milestone header line
200545c554bSsaggi-dw            $start = $this->lineEndAt($wikitext, (int)$h['pos'], $len);
201545c554bSsaggi-dw
202545c554bSsaggi-dw            // End at the line start of the next header with level <= $milLevel (or EOF)
203545c554bSsaggi-dw            $end = $len;
204545c554bSsaggi-dw            for ($j = $i + 1; $j < $count; $j++) {
205545c554bSsaggi-dw                $hn = $headers[$j];
206545c554bSsaggi-dw                if (($hn['level'] ?? PHP_INT_MAX) <= $milLevel) {
207545c554bSsaggi-dw                    $nextPos = (int)($hn['pos'] ?? -1);
208545c554bSsaggi-dw                    // If next header position is missing, fall back to EOF
209545c554bSsaggi-dw                    if ($nextPos >= 0) {
210545c554bSsaggi-dw                        $end = $this->lineStartAt($wikitext, $nextPos);
211545c554bSsaggi-dw                    }
212545c554bSsaggi-dw                    break;
213545c554bSsaggi-dw                }
214545c554bSsaggi-dw            }
215545c554bSsaggi-dw
216545c554bSsaggi-dw            $section = $this->cutSection($wikitext, $start, $end);
217545c554bSsaggi-dw
218545c554bSsaggi-dw            // Emit milestone with title attribute and body content
219545c554bSsaggi-dw            $synthetic .= '<milestone title=';
220545c554bSsaggi-dw            $synthetic .= $this->quoteAttrForWiki((string)$h['text']);
221545c554bSsaggi-dw            $synthetic .= ' data="' . $i . '">' . DOKU_LF;
222545c554bSsaggi-dw            $synthetic .= $section . DOKU_LF;
223545c554bSsaggi-dw            $synthetic .= '</milestone>' . DOKU_LF;
224545c554bSsaggi-dw        }
225545c554bSsaggi-dw
226545c554bSsaggi-dw        $synthetic .= '</dwtimeline>' . DOKU_LF;
227545c554bSsaggi-dw        return $synthetic;
228545c554bSsaggi-dw    }
229545c554bSsaggi-dw}
230