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($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 && $headers !== []) { 139 $titleHdr = $headers[0]; 140 } 141 142 $title = $titleHdr ? trim($titleHdr['text']) : $this->prettyId($target); 143 $titleLevel = $titleHdr ? $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