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