1<?php
2/**
3 * Automatically converts tab-delimted tables into dokuwiki tables.
4 *
5 * @license		GPLv3 (http://www.gnu.org/licenses/gpl.html)
6 * @link		http://www.dokuwiki.org/plugin:TabTables
7 * @author		Mike "Pomax" Kamermans <pomax@nihongoresources.com>
8 */
9
10if(!defined('DOKU_INC')) die();
11if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
12require_once(DOKU_PLUGIN.'action.php');
13
14class action_plugin_tabtables extends DokuWiki_Action_Plugin {
15
16	/**
17	 * Set this flag to true for timing information. Note that when this is turned on,
18	 * the plugin may generate output before dokuwiki's sent its own headers information,
19	 * which may cause problems for other plugins or even base configuration functionality.
20	 */
21	var $echo_timing = false;
22
23	/**
24	 * offsets-at-character-position. recorded as tuples {charpos,offset}, where charpos is the
25	 * position in the MODIFIED data, not the position in the original data, and offset is the
26	 * CUMULATIVE offset at that position, not the offset relative to the previous location.
27	 */
28	var $offsets = array();
29
30	/**
31	 * During the run, contains the original wiki data.
32	 */
33	var $original;
34
35	/**
36	 * During the run, contains the modified wiki data.
37	 */
38	var $wikified;
39
40	/**
41	 * Required function, used by dokuwiki on the plugins configuration page.
42	 */
43	function getInfo() {
44	  return array(
45		'author' => 'Mike "Pomax" Kamermans',
46		'email'  => 'pomax@nihongoresources.com',
47		'date'   => '2010-10-04',
48		'name'   => 'TabTables',
49		'desc'   => 'Turns tab delimited table data into dokuwiki tables',
50		'url'	=> 'http://www.dokuwiki.org/plugin:tabtables');
51	}
52
53	/**
54	 * Preprocesses the user's written data, by hooking into the text parser at the preprocessing point
55	 */
56	function register(&$controller) {
57		$controller->register_hook('PARSER_WIKITEXT_PREPROCESS', 'BEFORE', $this, '_tablify');
58		$controller->register_hook('PARSER_HANDLER_DONE','BEFORE', $this, '_fixsecedit');
59	}
60
61	/**
62	 * Tablify - runs through the base text, and replaces tab delimited table data
63	 * with proper docuwiki table syntax
64	 */
65	function _tablify(&$event, $param)
66	{
67		$start = $this->microtime_float();
68		if($this->echo_timing)  { echo "\n<!-- tabtables plugin output -->\n"; }
69
70		$this->original = explode("\n",$event->data);
71		$this->wikified = $this->original;
72
73		// tabling administration
74		$table_position = -1;
75		$in_block = false;
76		$_table_data = array();
77		$_empty_count = 0;
78
79		// iterate through the wiki data, line by line
80		$code_blocked = false;
81		$file_blocked = false;
82		$nowiki_blocked = false;
83		for($l=0; $l<count($this->original); $l++) {
84			$line = $this->original[$l];
85
86			// blocking?
87			if(strpos($line,"<code")!==false)		{ $code_blocked = true;		}
88			if(strpos($line,"<file")!==false)		{ $file_blocked = true;		}
89			if(strpos($line,"<nowiki")!==false)		{ $nowiki_blocked = true;	}
90			// block clearing?
91			if(strpos($line,"</code>")!==false)		{ $code_blocked = false;	}
92			if(strpos($line,"</file>")!==false)		{ $file_blocked = false;		}
93			if(strpos($line,"</nowiki>")!==false)	{ $nowiki_blocked = false;	}
94			// if blocked, immediately continue on to the next line
95			if($code_blocked || $file_blocked  || $nowiki_blocked) { continue; }
96
97			// aggregate tabling lines (a tabling line either contains tabs, or is either empty after trimming)
98			if(strpos($line,"\t")!==false || ($in_block && $line=="")) {
99				// set up table block if not aggregating yet
100				if(!$in_block) {
101					$in_block=true;
102					$table_position=$l;
103					$_table_data = array(); }
104
105				// if empty line, is this the first or second consecutive empty line?
106				// If the second, we need to finalise this table block
107				if($line=="" && count($_table_data)>1) { $in_block = $this->_finalise($_table_data, $table_position); }
108
109				// if we didn't just finalise, aggregate the data
110				if($in_block) { $_table_data[]=$line; }}
111
112			// last option: this was not a tabling line, but we have a filled table block that needs processing
113			elseif($in_block) { $in_block = $this->_finalise($_table_data, $table_position); }
114		}
115
116		// In case the table was the last thing on the page, we still have a table block to process
117		if($in_block) { $this->_finalise($_table_data, $table_position); }
118
119		if($this->echo_timing) { echo "<!-- initial parse took ".($this->microtime_float()-$start)."μs -->\n"; }
120		$start = $this->microtime_float();
121
122		// then, some administration so that we can perform section start/end
123		// marker correction after parsing is done (next event)
124		$char_pos = 0;
125		$text_offset = 0;
126		for($l=0; $l<count($this->wikified); $l++) {
127			// record offsets at the start of this line
128			$this->offsets[] = array('pos'=>$char_pos,'offset'=>$text_offset);
129			// pos/offset for next line will be:
130			$char_pos += strlen($this->wikified[$l]) + 1;	// +1 for the missing newline
131			$text_offset += strlen($this->wikified[$l]) - strlen($this->original[$l]); }
132
133		if($this->echo_timing) { echo "<!-- second parse took ".($this->microtime_float()-$start)."μs -->\n"; }
134
135		// and we're done...
136		$event->data = implode("\n",$this->wikified);
137
138		// but just for good measure, unset the original/wikified variables
139		unset($this->original);
140		unset($this->wikified);
141	}
142
143	/**
144	 * Gets the table data rewritten, then updates the modified container. returns false, so that
145	 * the iteration knows we're no longer in table data aggregation mode.
146	 */
147	function _finalise(&$_table_data, $table_position)
148	{
149		$wikified = $this->_replace($_table_data);
150		for($r=0; $r<count($wikified); $r++) { $this->wikified[$table_position + $r] = $wikified[$r]; }
151		return false;
152	}
153
154	/**
155	 * this function does the actual syntax replacement
156	 */
157	function _replace($original)
158	{
159		$new_table_block = array();
160
161		// preprocess check: will this use row headers? ie, is the first table "cell" an empty tab?
162		$_has_row_headers = (strpos($original[0],"\t")==0) ? true : false;
163
164		// how many cells are we actually dealing with?
165		$cells = count(split("\t",$original[0]));
166		$empty_line = "| " . str_repeat("|",$cells);
167
168		// replace the tabulation with wiki table syntax
169		for($r=0; $r<count($original); $r++) {
170			$row = $original[$r];
171			$new_table_block[$r] = (trim($row)=="") ?
172				$empty_line : "| " . str_replace("\t"," | ",$row) . " |"; }
173
174		// is the last line for this table empty? if so, clear it so that it doesn't become an empty table line.
175		if($new_table_block[count($new_table_block)-1]==$empty_line) {
176			unset($new_table_block[count($new_table_block)-1]); }
177
178		// is the second line for this table empty? and not the last line?
179		// If so, the first line contains headers rather than table data
180		if(count($new_table_block)>2 && $new_table_block[1]==$empty_line) {
181			$new_table_block[0] = str_replace("|","^",$new_table_block[0]);
182			// make sure to clear the header/content separator line
183			for($r=1; $r<count($new_table_block)-1; $r++) { $new_table_block[$r]=$new_table_block[$r+1]; }
184			$new_table_block[count($new_table_block)-1]="";
185		}
186
187		// does the table have row headers? if so, we need both row and column header styling
188		if($_has_row_headers) {
189			$new_table_block[0] = str_replace("|","^",$new_table_block[0]);
190			$new_table_block[0] = preg_replace("/^\^ /","| ",$new_table_block[0]);
191			for($r=1; $r<count($new_table_block); $r++) {
192					$new_table_block[$r] = preg_replace("/^\| /","^ ",$new_table_block[$r]); }}
193
194		// done
195		return $new_table_block;
196	}
197
198	/**
199	 * modifying the raw data has as side effect that the sectioning is based on the
200	 * modified data, not the original. This means that after processing, we need to
201	 * adjust the section start/end markers so that they point to start/end positions
202	 * in the original data, not the modified data.
203	 *
204	 * This function is based on the correction functions in the linebreak plugin,
205	 * by Christopher Smith (see http://www.dokuwiki.org/plugin:linebreak)
206	 */
207	function _fixsecedit(&$event, $param)
208	{
209		$start = $this->microtime_float();
210		$calls = &$event->data->calls;
211		$count = count($calls);
212
213		if($this->echo_timing) { echo "<!-- offset correction: running through ".$count." instructions -->\n"; }
214
215		// iterate through the instruction list and set the file offset values
216		// back to the values they would be if no tabling syntax ahd been added by this plugin
217
218		for ($i=0; $i < $count; $i++) {
219			if ($calls[$i][0] == 'section_edit') {
220				$calls[$i][1][0] = $this->_convert($calls[$i][1][0]);
221				$calls[$i][1][1] = $this->_convert($calls[$i][1][1]);
222				$calls[$i][2] = $this->_convert($calls[$i][2]); }}
223
224		if($this->echo_timing) { echo "<!-- offset correction took ".($this->microtime_float()-$start)."μs -->\n\n"; }
225	}
226
227	/**
228	 * Convert modified raw wiki offset value ($pos) back to the unmodified value
229	 */
230	function _convert($pos)
231	{
232		// find the offset that applies to this character position
233		$offset=0;
234		foreach($this->offsets as $tuple) {
235			if($pos>=$tuple['pos']) { $offset = $tuple['offset']; }
236			else { break; }}
237
238		// return offset-corrected position
239		return $pos - $offset;
240	}
241
242	/**
243	 * debugging helper function - gives us the microsecond
244	 * timestamp (in actual microseconds, not seconds)
245	 */
246	function microtime_float()
247	{
248		list($usec, $sec) = explode(" ", microtime());
249		return 1000000*((float)$usec + (float)$sec);
250	}
251}
252?>