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