1<?php 2/** 3 * DokuWiki Plugin dokutranslate (Syntax Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Martin Doucha <next_ghost@quick.cz> 7 */ 8 9// must be run within Dokuwiki 10if (!defined('DOKU_INC')) die(); 11 12if (!defined('DOKU_LF')) define('DOKU_LF', "\n"); 13if (!defined('DOKU_TAB')) define('DOKU_TAB', "\t"); 14if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 15 16require_once DOKU_PLUGIN.'syntax.php'; 17 18# Nesting counter, patterns disabled when non-zero 19$DOKUTRANSLATE_NEST = 0; 20 21# Generate edit button for paragraph 22function parEditButton($parId) { 23 global $ID; 24 global $INFO; 25 26 $ret = ''; 27 28 $params = array( 29 'do' => 'edit', 30 'rev' => $INFO['lastmod'], 31 'parid' => $parId, 32 ); 33 34 $ret .= '<div class="secedit editbutton_par' . strval($parId) . '">'; 35 $ret .= html_btn('secedit', $ID, '', $params, 'post'); 36 $ret .= '</div>'; 37 return $ret; 38} 39 40function startEditForm(&$renderer, $erase = true) { 41 global $DOKUTRANSLATE_EDITFORM; 42 global $DOKUTRANSLATE_NEST; 43 global $ACT; 44 global $TEXT; 45 46 # Insert saved edit form 47 $renderer->doc .= '<div class="preview" id="scroll__here">'; 48 $renderer->doc .= $DOKUTRANSLATE_EDITFORM; 49 50 # Render preview from submitted text (the saved page may look different 51 # if dokutranslate markup is present in the text) 52 if ($ACT == 'preview') { 53 $renderer->doc .= p_locale_xhtml('preview'); 54 $DOKUTRANSLATE_NEST++; 55 $previewIns = p_get_instructions($TEXT); 56 $DOKUTRANSLATE_NEST--; 57 $renderer->nest($previewIns); 58 } 59 60 $renderer->doc .= '</div>'; 61 62 if ($erase) { 63 # Insert erasure start marker 64 $renderer->doc .= '<!-- DOKUTRANSLATE ERASE START -->'; 65 } 66} 67 68function endEditForm(&$renderer) { 69 # Insert erasure end marker 70 $renderer->doc .= '<!-- DOKUTRANSLATE ERASE STOP -->'; 71} 72 73function loadTranslationMeta($id) { 74 global $REV; 75 76 # Loading meta for current version is simple 77 if (empty($REV)) { 78 return unserialize(io_readFile(metaFN($id, '.translate'), false)); 79 } 80 81 # Old revision, do it the hard way... 82 $ret = array(); 83 $meta = unserialize(io_readFile(metaFN($id, '.translateHistory'), false)); 84 $oldrev = intval($REV); 85 86 for ($i = 0; $i < count($meta[$oldrev]); $i++) { 87 $tmp = empty($meta[$oldrev][$i]['changed']) ? $oldrev : $meta[$oldrev][$i]['changed']; 88 $ret[$i] = $meta[$tmp][$i]; 89 $ret[$i]['changed'] = $tmp; 90 } 91 92 return $ret; 93} 94 95function parReviewClass($meta, $parid) { 96 static $classes = array('mistrans', 'reph', 'incaccept', 'accept'); 97 98 # No reviews, no class 99 if (empty($meta[$parid]['reviews'])) { 100 return ''; 101 } 102 103 # Start with max possible value of $clsid 104 $clsid = count($classes) - 1; 105 106 # Find the worst review 107 foreach ($meta[$parid]['reviews'] as $line) { 108 $tmp = $line['quality']; 109 110 if ($tmp >= count($classes) - 2 && !$line['incomplete']) { 111 $tmp++; 112 } 113 114 $clsid = $tmp < $clsid ? $tmp : $clsid; 115 } 116 117 return empty($classes[$clsid]) ? '' : $classes[$clsid]; 118} 119 120class syntax_plugin_dokutranslate extends DokuWiki_Syntax_Plugin { 121 private $origIns = NULL; 122 private $meta = NULL; 123 private $parCounter = 0; 124 125 public function getType() { 126 return 'container'; 127 } 128 129 public function getPType() { 130 return 'stack'; 131 } 132 133 public function getSort() { 134 return 100; 135 } 136 137 public function getAllowedTypes() { 138 return array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); 139 } 140 141 public function isSingleton() { 142 return true; 143 } 144 145 public function connectTo($mode) { 146 global $DOKUTRANSLATE_NEST; 147 global $ID; 148 149 # Disable patterns when the page is not being translated or 150 # we're building instructions for original page 151 if (!@file_exists(metaFN($ID, '.translate')) || $DOKUTRANSLATE_NEST > 0) { 152 return; 153 } 154 155 $this->Lexer->addEntryPattern('~~DOKUTRANSLATE_START~~(?=.*~~DOKUTRANSLATE_END~~)',$mode,'plugin_dokutranslate'); 156 $this->Lexer->addSpecialPattern('~~DOKUTRANSLATE_PARAGRAPH~~','plugin_dokutranslate','plugin_dokutranslate'); 157 } 158 159 public function postConnect() { 160 global $DOKUTRANSLATE_NEST; 161 global $ID; 162 163 # Disable patterns when the page is not being translated or 164 # we're building instructions for original page 165 if (!@file_exists(metaFN($ID, '.translate')) || $DOKUTRANSLATE_NEST > 0) { 166 return; 167 } 168 169 $this->Lexer->addExitPattern('~~DOKUTRANSLATE_END~~','plugin_dokutranslate'); 170 } 171 172 public function handle($match, $state, $pos, Doku_Handler $handler){ 173 switch ($state) { 174 case DOKU_LEXER_ENTER: 175 case DOKU_LEXER_EXIT: 176 case DOKU_LEXER_SPECIAL: 177 return array($state, $pos, $pos + strlen($match)); 178 } 179 180 return array($state, $match); 181 } 182 183 public function render($mode, Doku_Renderer $renderer, $data) { 184 global $DOKUTRANSLATE_NEST; 185 global $ID; 186 global $ACT; 187 global $TEXT; 188 global $REV; 189 190 # No metadata rendering 191 if($mode == 'metadata') { 192 return false; 193 } 194 195 # Allow exporting the page 196 if (substr($ACT, 0, 7) == 'export_') { 197 # Ignore plugin-specific markup, just let text through 198 if ($data[0] != DOKU_LEXER_UNMATCHED) { 199 return true; 200 } 201 202 $renderer->cdata($data[1]); 203 return true; 204 # Not exporting, allow only XHTML 205 } else if ($mode != 'xhtml') { 206 return false; 207 } 208 209 # Load instructions for original text on first call 210 if (is_null($this->origIns)) { 211 $DOKUTRANSLATE_NEST++; 212 $this->origIns = getCleanInstructions(dataPath($ID) . '/orig.txt'); 213 $this->meta = loadTranslationMeta($ID); 214 $this->parCounter = 0; 215 $DOKUTRANSLATE_NEST--; 216 } 217 218 $parid = getParID(); 219 $edithere = (in_array($ACT, array('edit', 'preview')) && $parid == $this->parCounter); 220 221 switch ($data[0]) { 222 # Open the table 223 case DOKU_LEXER_ENTER: 224 $renderer->doc .= '<table width="100%" class="dokutranslate"><tbody><tr>'; 225 $cls = parReviewClass($this->meta, $this->parCounter); 226 227 # Start the cell with proper review class 228 if (empty($cls)) { 229 $renderer->doc .= '<td width="50%">'; 230 } else { 231 $renderer->doc .= '<td width="50%" class="' . $cls . '">'; 232 } 233 234 # Paragraph anchor (yes, empty named anchor is valid) 235 $renderer->doc .= "<a name=\"_par$this->parCounter\"></a>\n"; 236 237 # Insert edit form if we're editing the first paragraph 238 if ($edithere) { 239 startEditForm($renderer); 240 } 241 242 break; 243 244 # Dump original text and close the row 245 case DOKU_LEXER_SPECIAL: 246 # Generate edit button 247 if ($ACT == 'show') { 248 if (empty($REV)) { 249 $renderer->doc .= parEditButton($this->parCounter); 250 } 251 252 $renderer->doc .= $this->_renderReviews($ID, $this->meta, $this->parCounter); 253 # Finish erasure if we're editing this paragraph 254 } else if ($edithere) { 255 endEditForm($renderer); 256 $renderer->doc .= $this->_renderReviews($ID, $this->meta, $this->parCounter); 257 } 258 259 $renderer->doc .= "</td>\n"; 260 261 if (needsReview($ID, $this->meta, $this->parCounter) || $edithere) { 262 $renderer->doc .= '<td class="reviewme">'; 263 } else { 264 $renderer->doc .= '<td>'; 265 } 266 267 # If this condition fails, somebody's been messing 268 # with the data 269 if (current($this->origIns) !== FALSE) { 270 $renderer->nest(current($this->origIns)); 271 next($this->origIns); 272 } 273 274 $renderer->doc .= "</td></tr>\n<tr>"; 275 $this->parCounter++; 276 $cls = parReviewClass($this->meta, $this->parCounter); 277 278 # Start the cell with proper review class 279 if (empty($cls)) { 280 $renderer->doc .= '<td width="50%">'; 281 } else { 282 $renderer->doc .= '<td width="50%" class="' . $cls . '">'; 283 } 284 285 # Paragraph anchor (yes, empty named anchor is valid) 286 $renderer->doc .= "<a name=\"_par$this->parCounter\"></a>\n"; 287 288 # Insert edit form if we're editing this paragraph 289 if (in_array($ACT, array('edit', 'preview')) && getParID() == $this->parCounter) { 290 startEditForm($renderer); 291 } 292 293 break; 294 295 # Dump the rest of the original text and close the table 296 case DOKU_LEXER_EXIT: 297 # Generate edit button 298 if ($ACT == 'show') { 299 if (empty($REV)) { 300 $renderer->doc .= parEditButton($this->parCounter); 301 } 302 303 $renderer->doc .= $this->_renderReviews($ID, $this->meta, $this->parCounter); 304 # Finish erasure if we're editing the last paragraph 305 } else if (in_array($ACT, array('edit', 'preview'))) { 306 $parid = getParID(); 307 308 if ($parid == $this->parCounter) { 309 endEditForm($renderer); 310 $renderer->doc .= $this->_renderReviews($ID, $this->meta, $this->parCounter); 311 # Invalid paragraph ID, show form here 312 } else if ($parid > $this->parCounter) { 313 startEditForm($renderer, true); 314 } 315 } 316 317 $renderer->doc .= "</td>\n"; 318 319 if (needsReview($ID, $this->meta, $this->parCounter) || $edithere) { 320 $renderer->doc .= '<td class="reviewme">'; 321 } else { 322 $renderer->doc .= '<td>'; 323 } 324 325 # Loop to make sure all remaining text gets dumped 326 # (external edit safety) 327 while (current($this->origIns) !== FALSE) { 328 $renderer->nest(current($this->origIns)); 329 next($this->origIns); 330 } 331 332 $renderer->doc .= '</td></tr></tbody></table>'; 333 break; 334 335 # Just sanitize and dump the text 336 default: 337 $renderer->cdata($data[1]); 338 break; 339 } 340 341 return true; 342 } 343 344 function _renderReviews($id, $meta, $parid) { 345 # Check for permission to write reviews 346 $mod = canReview($id, $meta, $parid); 347 348 # No reviews and no moderator privileges => no review block 349 if (!$mod && empty($meta[$parid]['reviews'])) { 350 return ''; 351 } 352 353 $ret = "<div class=\"dokutranslate_review\">\n"; 354 $ret .= '<h5>' . $this->getLang('review_header') . "</h5>\n"; 355 $ret .= "<table>\n"; 356 357 $listbox = array( 358 array('0', $this->getLang('trans_wrong')), 359 array('1', $this->getLang('trans_rephrase')), 360 array('2', $this->getLang('trans_accepted')) 361 ); 362 363 # Prepare review form for current user 364 if ($mod) { 365 if (isset($meta[$parid]['reviews'][$_SERVER['REMOTE_USER']])) { 366 $myReview = $meta[$parid]['reviews'][$_SERVER['REMOTE_USER']]; 367 } else { 368 $myReview = array('message' => '', 'quality' => 0, 'incomplete' => false); 369 } 370 371 $form = new Doku_Form(array()); 372 $form->addHidden('parid', strval($parid)); 373 $form->addHidden('do', 'dokutranslate_review'); 374 $form->addElement(form_makeTextField('review', $myReview['message'], $this->getLang('trans_message'), '', 'nowrap', array('size' => '50'))); 375 $form->addElement(form_makeMenuField('quality', $listbox, strval($myReview['quality']), $this->getLang('trans_quality'), '', 'nowrap')); 376 $args = array(); 377 378 if ($myReview['incomplete']) { 379 $args['checked'] = 'checked'; 380 } 381 382 $form->addElement(form_makeCheckboxField('incomplete', '1', $this->getLang('trans_incomplete'), '', 'nowrap', $args)); 383 $form->addElement(form_makeButton('submit', '', $this->getLang('add_review'))); 384 } 385 386 # Display all reviews for this paragraph 387 while (list($key, $value) = each($meta[$parid]['reviews'])) { 388 $ret .= '<tr><td>' . hsc($key) . '</td><td>'; 389 390 # Moderators can modify their own review 391 if ($mod && $key == $_SERVER['REMOTE_USER']) { 392 $ret .= $form->getForm(); 393 } else { 394 $ret .= '(' . $listbox[$value['quality']][1]; 395 396 if ($value['incomplete']) { 397 $ret .= ', ' . $this->getLang('rend_incomplete'); 398 } 399 400 $ret .= ') '; 401 $ret .= hsc($value['message']); 402 } 403 404 $ret .= "</td></tr>\n"; 405 } 406 407 # Current user is a moderator who didn't write a review yet, 408 # display the review form at the end 409 if ($mod && !isset($meta[$parid]['reviews'][$_SERVER['REMOTE_USER']])) { 410 if (empty($meta[$parid]['reviews'])) { 411 $ret .= '<tr><td>'; 412 } else { 413 $ret .= '<tr><td colspan="2">'; 414 } 415 416 $ret .= $form->getForm(); 417 $ret .= "</td></tr>\n"; 418 } 419 420 $ret .= "</table></div>\n"; 421 return $ret; 422 } 423} 424 425// vim:ts=4:sw=4:et: 426