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