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