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