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?>