1<?php 2 3// must be run within Dokuwiki 4if(!defined('DOKU_INC')) die(); 5 6define( 'WEIQI_ERROR_BAD_ATTRIBUTES', 0); 7define( 'WEIQI_ERROR_ONE_CAPTION', 1); 8define( 'WEIQI_ERROR_RECTANGULAR', 2); 9 10class weiqi_parser { 11 /** 12 * Some config items in this array. Keys are explained below. 13 * Provide the array in the constructor or the setter. 14 * 15 * attributes_key 16 * how to know a line is for the attributes 17 * caption_key 18 * how to know a line is for the caption 19 * img_path_web 20 * web img path (with trailing slash) 21 * img_path_fs 22 * path to the img directory no the server for the parser to check 23 * if requested img files are installed (absolute or relative) 24 * letter_sequence 25 * letter sequence (no 'i' on a goban) 26 * plain_text_coeff 27 * when displaying plain text instead of images, 28 * if font-size:(image_size)px; it's too big, hence we have to reduce it 29 * allowed_attribute_keys 30 * what can the user change in his/her code 31 * default_attributes 32 * what attributes will be used if nothing is provided by the user 33 * here is the detail of the keys of this array: 34 * demo : boolean, whether to display the source for demo purpose 35 * goban : integer, number of the 'wood' file to use 36 * grid_size : integer (px), size of the square img files to use 37 * edges_width : integer (px) 38 * coords : boolean, whether to display the co-ordinates 39 * reverse_numbers : boolean 40 * reverse_letters : boolean 41 * start_number : integer 42 * start_letter : letter in the letter sequence 43 * advanced : boolean, whether to use advanced code 44 */ 45 var $conf = array(); 46 /** 47 * Setted to true if the parse() function has to complain. 48 */ 49 var $error = false; 50 /** 51 * Among the constants defined above. 52 */ 53 var $error_code = ''; 54 /** 55 * Some errors need to pass some info about the error, 56 * especially WEIQI_ERROR_BAD_ATTRIBUTES that fills this array with 57 * array($key, $val) arrays when $key is not allowed ($val is '') or 58 * when $key is allowed but not its $val value. 59 */ 60 var $error_data = array(); 61 62 /** 63 * Constructor, with a possible config array 64 */ 65 function weiqi_parser($conf = array()) { 66 $this->set_conf($conf); 67 } 68 69 /** 70 * Setter for the conf array 71 */ 72 function set_conf($conf) { 73 $this->conf = $conf; 74 } 75 76 /** 77 * Makes the parser to parse the code. 78 * When no complaint is made ($this->error is then set to true), 79 * this function returns the $goban_data array with: 80 * $attributes, $caption, $board, $width, $height, $source 81 * for the render() function to work. 82 * Returns nothing if an error has occured. 83 */ 84 function parse($str) { 85 $lines = explode("\n", $str); 86 // init of attributes, caption and bi-dimensional array of chars 87 $attributes_str = ''; 88 $caption = ''; 89 $board = array(); 90 91 // first pass for some checks, caption and attributes 92 foreach ($lines as $line) { 93 if ($line!='') { 94 // check if this is the attributes line 95 // maybe preg_match is better 96 if (substr($line, 0, strlen($this->conf['attributes_key'])) == $this->conf['attributes_key']) { 97 $attributes_str.= ' '.substr($line, strlen($this->conf['attributes_key'])); 98 // check if this is the caption line 99 } elseif (substr($line, 0, strlen($this->conf['caption_key'])) == $this->conf['caption_key']) { 100 if ($caption!='') { 101 $this->error = true; 102 $this->error_code = WEIQI_ERROR_ONE_CAPTION; 103 return; 104 } 105 $caption = trim(substr($line, strlen($this->conf['caption_key']))); 106 } else { 107 // check length of our lines 108 // first store the one of the first line 109 if (empty($line_length)) $line_length = strlen($line); 110 if (strlen($line) != $line_length) { 111 $this->error = true; 112 $this->error_code = WEIQI_ERROR_RECTANGULAR; 113 return; 114 } 115 } 116 } 117 } 118 $attributes = $this->parse_goban_attributes($attributes_str);//echo 'att:<';print_r($attributes);echo '>'; 119 if ($this->error) return; 120 121 // second pass for parsing the board code 122 foreach ($lines as $line) { 123 if ($line!='') { 124 // check if this is the attributes or caption line 125 $att = substr($line, 0, strlen($this->conf['attributes_key'])) == $this->conf['attributes_key']; 126 $cap = substr($line, 0, strlen($this->conf['caption_key'])) == $this->conf['caption_key']; 127 if ( !$att AND !$cap) { 128 $tmp_array_line = array(); 129 // map the string to an array 130 // one by one for standard code, 131 // two by two for advanced code 132 if (!array_key_exists('advanced', $attributes)) { 133 for($i=0;$i<strlen($line);$i++) 134 $tmp_array_line[] = substr($line, $i, 1); 135 } else { 136 for($i=0;$i<strlen($line)/2;$i++) 137 $tmp_array_line[] = substr($line, 2*$i, 2); 138 } 139 // append this line to the main bi-dim array of chars 140 $board[] = $tmp_array_line; 141 } 142 } 143 } 144 $width = $attributes['advanced']?($line_length/2):$line_length; 145 $height = count($board); 146 return array($attributes, $caption, $board, $width, $height, $str); 147 } 148 149 /** 150 * Returns the html table. 151 * this function needs an array with these informations 152 * $attributes, $caption, $board, $width, $height, $source 153 */ 154 function render($goban_data) { 155 list($attributes, $caption, $board, $width, $height, $source) = $goban_data; 156 // import default attributes 157 foreach ($this->conf['default_attributes'] as $key => $val) $$key = $val; 158 // import user-defined attributes 159 // quite dangerous, but controlled by $allowed_keys 160 // in the parse_goban_attributes() function 161 foreach ($attributes as $key => $val) $$key = $val; 162 // some adjustments 163 $goban = 'images/wood'.$goban; 164 $w1 = $grid_size; 165 $w2 = $edges_width; 166 167 $wout = ''; // weiqi output, appended to $renderer->doc at the end 168 169 // should we output the source too (demo purpose) 170 if ($demo) { 171 $wout.= '<pre class="w-source">'; 172 $wout.= $source; 173 $wout.= '</pre>'; 174 } 175 176 // hardcoded style (I mean not CSSed) to be able to change the goban color 177 $wout.= '<table style="background-image: url(\''.$this->conf['img_path_web'].$goban.'.gif\');" border="0" cellspacing="0" cellpadding="0">'; 178 if ($caption!='') $wout .= '<caption>'.$caption.'</caption>'; 179 // top coordinates 180 if ($coords) { 181 $wout .= '<tr>'; 182 $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w1).'</td>';// blank for left coordinates 183 $wout .= '<td>'.$this->img('images/blank',' ',$w2,$w1).'</td>';// blank for left edge 184 for($x=1;$x<=$width;$x++) { 185 if ($reverse_letters) $row_letter = $this->letter($width - ($x - ($start_letter - 1)) + 1); 186 else $row_letter = $this->letter($x + ($start_letter - 1)); 187 $wout .= '<td>'.$this->img($w1.'/c'.$row_letter, $row_letter).'</td>';// top coordinates 188 } 189 $wout .= '<td>'.$this->img('images/blank',' ',$w2,$w1).'</td>';// blank for right edge 190 $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w1).'</td>';// blank for right coordinates 191 $wout .= '</tr>'; 192 } 193 // top edge 194 $wout .= '<tr>'; 195 if ($coords) $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w2).'</td>';// blank for left coordinates 196 $wout .= '<td>'.$this->img($goban.'_ul', ' ').'</td>';// top left goban corner 197 $wout .= '<td colspan="'.$width.'">'.$this->img($goban.'_u', ' ', $w1*$width, $w2).'</td>';// top corner 198 $wout .= '<td>'.$this->img($goban.'_ur', ' ').'</td>';// top right goban corner 199 if ($coords) $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w2).'</td>';// blank for right coordinates 200 $wout .= '</tr>'; 201 // body 202 $y=0; 203 foreach ($board as $line) { 204 $y++; 205 $x=0; 206 $wout .= '<tr>'; 207 if ($reverse_numbers) $line_number = $y + ($start_number - 1); 208 else $line_number = $height - $y + 1 + ($start_number - 1); 209 210 if ($coords) $wout .= '<td>'.$this->img($w1.'/c'.$line_number, $line_number).'</td>';// left coordinates 211 if ($y==1) $wout .= '<td rowspan="'.$height.'">'.$this->img($goban.'_l', ' ', $w2, $w1*$height).'</td>';// left edge 212 // board 213 foreach ($line as $char) { 214 $x++; 215 $wout .= '<td>'.$this->img($w1.'/'.$this->weiqi_code($char, $x, $y, $width, $height), $char, $w1).'</td>'; 216 } 217 if ($y==1) $wout .= '<td rowspan="'.$height.'">'.$this->img($goban.'_r', ' ', $w2, $w1*$height).'</td>';// right edge 218 if ($coords) $wout .= '<td>'.$this->img($w1.'/c'.$line_number, $line_number).'</td>';// left coordinates 219 $wout .= '</tr>'; 220 } 221 // bottom edge 222 $wout .= '<tr>'; 223 if ($coords) $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w2).'</td>';// blank for left coordinates 224 $wout .= '<td>'.$this->img($goban.'_dl', ' ').'</td>';// bottom left goban corner 225 $wout .= '<td colspan="'.$width.'">'.$this->img($goban.'_d', ' ', $w1*$width, $w2).'</td>'; 226 $wout .= '<td>'.$this->img($goban.'_dr', ' ').'</td>';// bottom right goban corner 227 if ($coords) $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w2).'</td>';// blank for right coordinates 228 $wout .= '</tr>'; 229 // bottom coordinates 230 if ($coords) { 231 $wout .= '<tr>'; 232 $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w1).'</td>';// blank for left coordinates 233 $wout .= '<td>'.$this->img('images/blank',' ',$w2,$w1).'</td>';// blank for left edge 234 for($x=1;$x<=$width;$x++) { 235 if ($reverse_letters) $row_letter = $this->letter($width - ($x - ($start_letter - 1)) + 1); 236 else $row_letter = $this->letter($x + ($start_letter - 1)); 237 $wout .= '<td>'.$this->img($w1.'/c'.$row_letter, $row_letter).'</td>';// bottom coordinates 238 } 239 $wout .= '<td>'.$this->img('images/blank',' ',$w2,$w1).'</td>';// blank for right edge 240 $wout .= '<td>'.$this->img('images/blank',' ',$w1,$w1).'</td>';// blank for right coordinates 241 $wout .= '</tr>'; 242 } 243 $wout.= '</table>'; 244 return $wout; 245 } 246 /** 247 * A character (or two in advanced mode) is translated to 248 * the relevant image file for the img() function to work. 249 * If ends with a #, img() will display text. 250 */ 251 function weiqi_code($str, $x, $y, $width, $height) { 252 $str = trim($str); 253 if (strlen($str) == 1) { 254 // normal code 255 switch ($str) { 256 // goban 257 case '.': return 'e'; 258 case ',': return 'h'; 259 case '+': 260 if ( $x==$width && $y==1 ) return 'ur'; 261 if ( $x==1 && $y==1 ) return 'ul'; 262 if ( $x==1 && $y==$height ) return 'dl'; 263 if ( $x==$width && $y==$height ) return 'dr'; 264 case '-': 265 if ( $y==1 ) return 'u'; 266 if ( $y==$height ) return 'd'; 267 case '|': 268 if ( $x==1 ) return 'el'; 269 if ( $x==$width ) return 'er'; 270 271 // stones 272 case 'x': return 'b'; 273 case 'X': return 'bm'; 274 case 'o': return 'w'; 275 case 'O': return 'wm'; 276 277 // for letters or numbers, we append an '#' 278 // for them to appear as plain text 279 280 // letters and numbers 281 case '1': return '1#'; 282 case '2': return '2#'; 283 case '3': return '3#'; 284 case '4': return '4#'; 285 case '5': return '5#'; 286 case '6': return '6#'; 287 case '7': return '7#'; 288 case '8': return '8#'; 289 case '9': return '9#'; 290 291 case 'a': return 'a#'; 292 case 'b': return 'b#'; 293 case 'c': return 'c#'; 294 case 'd': return 'd#'; 295 case 'e': return 'e#'; 296 case 'f': return 'f#'; 297 case 'g': return 'g#'; 298 case 'h': return 'h#'; 299 case 'i': return 'i#'; 300 case 'j': return 'j#'; 301 case 'k': return 'k#'; 302 case 'l': return 'l#'; 303 case 'm': return 'm#'; 304 case 'n': return 'n#'; 305 case 'p': return 'p#'; 306 case 'q': return 'q#'; 307 case 'r': return 'r#'; 308 case 's': return 's#'; 309 case 't': return 't#'; 310 case 'u': return 'u#'; 311 case 'v': return 'v#'; 312 case 'w': return 'w#'; 313 case 'y': return 'y#'; 314 case 'z': return 'z#'; 315 316 case 'A': return 'A#'; 317 case 'B': return 'B#'; 318 case 'C': return 'C#'; 319 case 'D': return 'D#'; 320 case 'E': return 'E#'; 321 case 'F': return 'F#'; 322 case 'G': return 'G#'; 323 case 'H': return 'H#'; 324 case 'I': return 'I#'; 325 case 'J': return 'J#'; 326 case 'K': return 'K#'; 327 case 'L': return 'L#'; 328 case 'M': return 'M#'; 329 case 'N': return 'N#'; 330 case 'P': return 'P#'; 331 case 'Q': return 'Q#'; 332 case 'R': return 'R#'; 333 case 'S': return 'S#'; 334 case 'T': return 'T#'; 335 case 'U': return 'U#'; 336 case 'V': return 'V#'; 337 case 'W': return 'W#'; 338 case 'Y': return 'Y#'; 339 case 'Z': return 'Z#'; 340 } 341 } else { 342 // advanced code 343 // for letters or numbers, we append an '#' 344 // for them to appear as plain text 345 346 // the rest of the numbers 347 if (is_numeric($str)) return $str.'#'; 348 else { 349 // the rest of the available symbols 350 $ch1 = substr($str, 0, 1); 351 $ch2 = substr($str, 1, 1); 352 // first, the rest of the letters 353 if ($ch2 == 'l') return $ch1.'#'; 354 // now the goban marks and stones 355 // ok it's a simple map, but DGS files for red squares have a d 356 $ch2 = ($ch2=='r')?'d':$ch2; 357 358 // some checks now 359 // if nothing found, let's return nothing 360 // so the img() function can report a mistake 361 $available_second_chars = 'cstbwxdg1234567890'; 362 if (strpos($available_second_chars, $ch2) === false) return ''; 363 // no black stone with a black square, same for white 364 if ($ch1.$ch2=='bb' OR $ch1.$ch2=='ww') return ''; 365 366 switch ($ch1) { 367 // goban and marks 368 case '.': return 'e'.$ch2; 369 case ',': return 'h'.$ch2; 370 case '+': 371 if ( $x==$width && $y==1 ) return 'ur'.$ch2; 372 if ( $x==1 && $y==1 ) return 'ul'.$ch2; 373 if ( $x==1 && $y==$height ) return 'dl'.$ch2; 374 if ( $x==$width && $y==$height ) return 'dr'.$ch2; 375 case '-': 376 if ( $y==1 ) return 'u'.$ch2; 377 if ( $y==$height ) return 'd'.$ch2; 378 case '|': 379 if ( $x==1 ) return 'el'.$ch2; 380 if ( $x==$width ) return 'er'.$ch2; 381 382 // stones 383 case 'x': ; 384 case 'X': return 'b'.($ch2=='0'?'10':$ch2); 385 case 'o': ; 386 case 'O': return 'w'.($ch2=='0'?'10':$ch2); 387 } 388 } 389 } 390 // if nothing found, let's return nothing, 391 // so the img() function can report a mistake 392 return ''; 393 } 394 395 /** 396 * Number to letter using $this->conf['letter_sequence']) 397 */ 398 function letter($n) { 399 $n--; 400 if ($n >= 0 AND $n < strlen($this->conf['letter_sequence'])) 401 return substr($this->conf['letter_sequence'], $n, 1); 402 // this can help debugging the wiki code 403 else return $n; 404 } 405 406 /** 407 * Returns an img tag or text to fille the table. 408 */ 409 function img($src, $alt=false, $w=false, $h=false) { 410 $text_px = floor($this->conf['plain_text_coeff']*$w); 411 // '/' at the end means we have to report a mistake 412 if (preg_match('@/$@', $src)) 413 return '<span class="w-report" style="font-size:'.$text_px.'px">@</span>'; 414 // '#' at the end means we have a number or a letter 415 // numbers and letters are displayed with simple text 416 if (preg_match('@/(.+)#$@', $src, $match)) 417 return '<span style="font-size:'.$text_px.'px">'.$match[1].'</span>'; 418 419 // the rest is done with the usual img html tag 420 $html = ''; 421 $html.= '<img'; 422 if($w) $html.= ' width="'.$w.'"'; 423 if($h) $html.= ' height="'.$h.'"'; 424 $html.= ' src="'.$this->conf['img_path_web'].$src.'.gif"'; 425 if($alt) $html.= ' alt="'.$alt.'"'; 426 $html.= ' />'; 427 return $html; 428 } 429 430 /** 431 * Parses the attribute lines. 432 * Returns an array that will be used by the render() function if everything 433 * is correct. 434 * Returns nothing and sets the error vars correctly if not. 435 */ 436 function parse_goban_attributes($str) { 437 $str = trim($str); 438 if ($str=='') return array(); 439 440 $ret = array(); 441 $attributes = explode(' ', $str); 442 $errors = array(); 443 foreach ($attributes as $attribute) { 444 list($key,$value) = preg_split('/=/',$attribute,2); 445 446 // report non allowed keys 447 if (!in_array($key, $this->conf['allowed_attribute_keys'])) 448 $errors[$key] = ''; 449 450 if ($key == 'demo') $ret[$key] = true; 451 452 if ($key == 'goban') { 453 if (!is_file(realpath($this->conf['img_path_fs'].'images/wood'.$value.'.gif'))) { 454 $errors[$key] = $value; 455 } 456 $ret['goban'] = $value; 457 } 458 459 if ($key == 'coords') $ret[$key] = true; 460 461 if ($key == 'grid_size') { 462 if (!is_dir(realpath($this->conf['img_path_fs'].$value))) { 463 $errors[$key] = $value; 464 } 465 $ret['grid_size'] = $value; 466 } 467 468 if ($key == 'reverse_numbers') $ret[$key] = true; 469 if ($key == 'reverse_letters') $ret[$key] = true; 470 471 if ($key == 'start_number') { 472 if ($value<1 OR $value>25) { 473 $errors[$key] = $value; 474 } 475 $ret['start_number'] = $value; 476 } 477 if ($key == 'start_letter') { 478 $strpos = strpos($this->conf['letter_sequence'], strtolower($value)); 479 if ($strpos===false) { 480 $errors[$key] = $value; 481 } 482 $ret['start_letter'] = $strpos + 1; 483 } 484 485 if ($key == 'advanced') $ret[$key] = true; 486 } 487 if (!empty($errors)) { 488 $this->error = true; 489 $this->error_code = WEIQI_ERROR_BAD_ATTRIBUTES; 490 $this->error_data = array('attributes_str' => $str, 'errors' => $errors); 491 return; 492 } 493 return $ret; 494 } 495} 496