1<?php
2/**
3 * Mathmulti Plugin: Render (La)Tex or plain text Math expression
4 * into images or mathml
5 * http://wiki.splitbrain.org/plugin:math
6 *
7 * Thanks to everyone how provided so many examples of plugin or
8 * contributed to the plugin tutorials... This was of great help!
9 * If you see part of your code here and I forgot to give credits
10 * for your work, please let me know so I could add your name.
11 *
12 * Syntax:  <math format idiom size>...mathematical formula..</math>
13 *
14 * format   (optional) Specify which program will be used to
15 *           render the math formulae.
16 *           Support:
17 *           img (use mimetex http://www.forkosh.com/mimetex.html)
18 *           mml (use:
19 *               itex2mml http://pear.math.pitt.edu/mathzilla/itex2mml.html
20 *               plain2mml http://math.wcupa.edu/~johnston/plain2mathml )
21 *           If might be better not to specify the format thus you
22 *           can change it at once for all math expression in your
23 *           wiki by changing the config value.
24 * idiom     (optional) Specify in which idiom (math dialect)
25 *           the math formulae is written in
26 *           So far, only 'tex' [(La)TeX] and 'plain' style are supported
27 *           'tex': http://www.forkosh.com/mimetex.html
28 *               or http://pear.math.pitt.edu/mathzilla/itex2mml.html
29 *           'plain': http://math.wcupa.edu/~johnston/plain2mathml/
30 *                    please note that output for plain are in mml only
31 *                    if img is specified, the raw text is displayed
32 *           Might be used to merge with Christopher Smith's
33 *           math plugin (phpmathpublisher own synthax)
34 * size     (optional) Default size of the Math char [points]
35 *
36 * Formulae syntax: (depend on Idiom) See mimetex, itex2mml, plain2mml above
37 *                  (may add phpmathpublisher own synthax)
38 *
39 * Config (in dokuwiki/conf/local.php)
40 * $conf['mathmulti_iscgi'] = false; //Are eqn images produced by cgi every time the page is viewed?
41 * $conf['mathmulti_mimetex'] = ""; //Path to mimetex bin (produce img)
42 * $conf['mathmulti_itex2mml'] = ""; //Path to itex2mml bin (produce mathml)
43 * $conf['mathmulti_plain2mml'] = ""; //Path to plain2mml bin (produce mathml)
44 * $conf['mathmulti_format'] = "img"; //default mathmulti rendering format
45 * $conf['mathmulti_idiom'] = "tex"; //default mathmulti math idiom (dialect)
46 * $conf['mathmulti_size'] = "12"; //default mathmulti math char size [points]
47 *
48 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
49 * @author     Stephane Chamberland <stephane.chamberland@ec.gc.ca>
50 * @date       2006-05-25
51 */
52
53if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
54if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
55require_once(DOKU_PLUGIN.'syntax.php');
56require_once(DOKU_INC.'inc/io.php');
57
58// ----[ mathmulti plugin globals ]---------------------------------------
59global $mathmultiplugin_urlimg,$mathmultiplugin_dirimg;
60
61  // base url to access images, should correspond to $dirimg below.
62  // if left at default, it will be modified to add a subfolder to avoid filling
63  // the root media folder with clutter, refer _cacheExists()
64  $mathmultiplugin_urlimg = DOKU_URL.'lib/exe/fetch.php?w=&amp;h=&amp;cache=cache&amp;media=';
65  $mathmultiplugin_dirimg = DOKU_INC;
66
67// -----------------------------------------------------------------------
68
69/**
70 * All DokuWiki plugins to extend the parser/rendering mechanism
71 * need to inherit from this class
72 */
73class syntax_plugin_mathmulti extends DokuWiki_Syntax_Plugin {
74
75    var $prefix = 'mathmulti_';
76
77    var $inlineopt = array();
78
79    //TODO: take these values from conf/metadata.php
80    var $mathmulti_options = array(
81        'format' => array('img', 'mml'),
82        'idiom' => array('tex', 'plain'),
83        'size' => array('9','10','11','12','13','14','15','16','17','18')
84        );
85
86    var $mathmulti_engines = array(
87        'mimetex'   => array('idiom' => 'tex',
88                            'format' => 'img'),
89        'itex2mml'  => array('idiom' => 'tex',
90                            'format' => 'mml'),
91        'plain2mml' => array('idiom' => 'plain',
92                            'format' => 'mml')
93        );
94
95    var $msg_disable = 'disable';
96
97    var $enable = false;
98    var $msg_sent = false;
99
100    /**
101     * Initialization?
102     */
103    function syntax_plugin_mathmulti() {
104       $this->msg_disable = $this->getLang($this->prefix.'disable');
105       $this->enable = $this->_requirements_ok();
106    }
107
108    /**
109     * return some info
110     */
111    function getInfo(){
112
113      return array(
114        'author' => 'Stephane Chamberland',
115        'email'  => 'stephane.chamberland@ec.gc.ca',
116        'date'   => '2006-05-23',
117        'name'   => 'MathMulti Plugin'.(!$this->enable ? ' ('.$this->getLang($this->prefix.'disable').')' : ''),
118        'desc'   => $this->getLang($this->prefix.'info').
119                    (!$this->enable ? "\n(".$this->getLang($this->prefix.'disable').")" : ''),
120        'url'    => 'http://wiki.splitbrain.org/plugin:math',
121      );
122    }
123
124    function getType(){ return 'protected'; }
125    function getPType(){ return 'normal'; }
126    function getSort(){ return 209; }
127
128    /**
129     * Connect pattern to lexer
130     */
131    function connectTo($mode) {
132      $this->Lexer->addEntryPattern('<math(?=[^\r\n]*?>.*?</math>)',$mode,'plugin_mathmulti');
133    }
134
135    function postConnect() {
136      $this->Lexer->addExitPattern('</math>','plugin_mathmulti');
137    }
138
139    /**
140     * Handle the match
141     */
142    function handle($match, $state, $pos, &$handler){
143
144      if ( $state == DOKU_LEXER_UNMATCHED ) {
145        list($optstring, $contentstring) = preg_split('/>/u', $match, 2);   // will split into format & math formulae
146
147        //TODO: ?needed? string2lower($optstring);
148
149        $tokens = preg_split('/\s+/', $optstring, 9);  // limit is defensive
150        $optionlist = $this->_parseoptions($this->mathmulti_options,
151                                          $this->prefix,$tokens);
152
153        $engine = $this->_getengine(
154                $optionlist,
155                $this->mathmulti_engines,
156                $this->prefix,
157                !$this->getConf($this->prefix.'iscgi'));
158
159        $align = $this->_checkalign($contentstring);
160
161        return (array($engine, $optionlist['size'], $align, trim($contentstring)));
162      }
163      return false;
164    }
165
166    /**
167     * Create output
168     */
169    function render($mode, &$renderer, $data) {
170
171        if (!$data) return;   // skip rendering for the enter and exit patterns
172        list($engine, $size, $align, $eqn) = $data;
173
174        if($mode == 'xhtml'){
175            $eqn_html = $renderer->_xmlEntities($eqn);
176            if ($this->enable) {
177                if ($engine == 'mimetex') {
178                    if ($this->getConf($this->prefix.'iscgi')) {
179                        $eqn_html =
180                            $this->_render_mimetexcgi($eqn,$eqn_html,$size, $align);
181                    } else {
182                        $eqn_html =
183                            $this->_render_mimetex($eqn,$eqn_html,$size, $align);
184                    }
185                } else if ($engine == 'itex2mml') {
186                    $eqn_html =
187                            $this->_render_itex2mml($eqn,$eqn_html,$size, $align);
188                } else if ($engine == 'plain2mml') {
189                    $eqn_html =
190                            $this->_render_plain2mml($eqn,$eqn_html,$size, $align);
191                } else {
192                    $eqn_html =
193                            $this->_render_plaintext($eqn,$eqn_html,$size, $align);
194                }
195            } else {
196                $this->_msg($this->msg_disable, -1);
197            }
198            $renderer->doc .= $eqn_html;
199            // return to previous error reporting level
200            error_reporting($error_level);
201            return true;
202        }
203        return false;
204    }
205
206
207    function _render_plaintext($eqn,$eqn_html,$size,$align) {
208        if ($align == 'normal') {
209            return '<span class="math">'.$eqn_html.'</span>';
210        } else {
211            return '<div class="math '.$align.'">'.$eqn_html.'</div>';
212        }
213    }
214
215    //TODO: use $size param
216    function _render_mimetexcgi($eqn,$eqn_html,$size,$align) {
217        $myclass=' class="math"';
218        if ($align != 'normal') {
219            $math_html = ' class="math media'.$align.'"';
220        }
221        return '<img src="'.$this->getConf($this->prefix.'mimetex').'?'.
222            $eqn_html.'" alt="'.$eqn_html.'" '.$myclass.'/>';
223    }
224
225    //TODO: create mathML eqn with mimetex
226    function _render_mimetex($eqn,$eqn_html,$size,$align) {
227        return $this->_render_plaintext($eqn,$eqn_html,$size, $align);
228    }
229
230    //TODO: create and cache img with mimetex
231//     function _old_mathmultiimage() {
232//         //return $renderer->_xmlEntities($mathmulti_eqn);
233//
234//         $fontsize0_7 = max(0,min(intval($msize-9,7)));
235//
236//         $hash = md5(serialize($data.strval($msize)));
237//
238//         //write eqn in temp file
239//         $tmpeqnfile= $mathmultiplugin_urlimg."";
240//         $fh = fopen($tmpeqnfile, 'w');
241//         fwrite($fh, $mathmulti_eqn);
242//         fclose($fh);
243//
244//         //TODO: remove old img.gif file if any
245//         $oldgiffile = "";
246//         if (is_file($oldgiffile)) {unlink($oldgiffile);}
247//         //TODO: define img.gif file name
248//         $giffile = "";
249//         //Create img file
250//         $fh = popen($this->getConf('mathmulti_mimetex').' -e '.$giffile.' -s '.$fontsize0_7." -f ".$mathmulti_eqn_file,'r');
251//         pclose($fh);
252//
253//         unlink($tmpeqnfile); //TODO: remove eqn temp file
254//
255//         //Write display image HTML code
256//         $myclass=' class="math"';
257//         if ($align != 'normal') {
258//             $math_html = 'class="math media'.$align.'"';
259//         }
260//         return '<img src="'.$giffile.'"
261//             alt="'.$renderer->_xmlEntities($mathmulti_eqn).
262//             $myclass.'/>';
263//     }
264
265    //TODO: create mathML eqn with itex2mml
266    function _render_itex2mml($eqn,$eqn_html,$size, $align) {
267        return $this->_render_plaintext($eqn,$eqn_html,$size, $align);
268    }
269
270   //TODO: create mathML eqn with plain2mml
271    function _render_plain2mml($eqn,$eqn_html,$size, $align) {
272         return $this->_render_plaintext($eqn,$eqn_html,$size, $align);
273    }
274
275
276    /**
277     * Check string alignment (return left,right,center,normal)
278     * param: Not trimed string
279     */
280    function _checkalign($mystring){
281        $align = 'normal';
282        if (strlen($mystring) > 1) {
283          $c_first = $mystring{0};
284          $c_last = $mystring{strlen($mystring)-1};
285
286          $align = ($c_first == ' ') ? ($c_last == ' ' ? 'center' : 'right') : ($c_last == ' ' ? 'left' : 'normal');
287        }
288        return $align;
289    }
290
291    /**
292     * Init options (from default config)
293     * Params:
294     *    $optionlistdict = array(
295     *                       'option_1' => array('val_1', 'val_2'),
296     *                        ...);
297     *    $confprefix: [string] Prefix of options name in global $conf
298     * Return:
299     *    $optionlist = array(
300     *                       'option_1' => 'val_opt_1',
301     *                        ...);
302     */
303    function _initoptions($optionlistdict,$confprefix) {
304        $optionlist = array();
305        foreach ($optionlistdict as $option => $optlist) {
306            $optionlist[$option] = $this->getConf($confprefix.$option);
307        }
308        return $optionlist;
309    }
310
311    /**
312     * Init options (from default config + inline options)
313     * Params:
314     *    $optionlistdict = array(
315     *                       'option_1' => array('val_1', 'val_2'),
316     *                        ...);
317     *    $confprefix: [string] Prefix of options name in global $conf
318     *    $tokenlist: [string array] List of option keywords
319     * Return:
320     *    $optionlist = array(
321     *                       'option_1' => 'val_opt_1',
322     *                        ...);
323     */
324    //TODO: gengeralize to take args as in config plugin
325    function _parseoptions($optionlistdict,$confprefix,$tokenlist) {
326        $optionlist = $this->_initoptions($optionlistdict,$confprefix);
327        foreach ($tokenlist as $token) {
328            foreach ($optionlistdict as $option => $optlist) {
329                foreach ($optlist as $optionval) {
330                    if (trim($token) == trim($optionval)) {
331                        $optionlist[$option] = $optionval;
332                    }
333                }
334            }
335        }
336        return $optionlist;
337    }
338
339    /**
340     * Look into options and "engine" spec (matching options) to
341     *
342     * Params:
343     *    $optionlist: as obtained by $this->_parseoptions()
344     *    $enginedictlist: list of engines and matching option => values
345     *        = array('engine_1' => array('option_1' => 'val_1',
346     *                                    'option_2' => 'val_2',...),
347     *                ...);
348     *    $confprefix: [string] Prefix of options name in global $conf
349     *    $checkfile: [logical] Check if specified file for engine exist
350     * Return:
351     *    $engine: [string] engine name (use to render code)
352     *             that matches default/specified  options
353     *    false: if no "engine" match the options
354     */
355    function _getengine($optionlist,$enginedictlist,$confprefix,$checkfile) {
356        foreach ($enginedictlist as $engine => $engoptdict) {
357            $engineok = true;
358            foreach ($engoptdict as $option => $optionval) {
359                if ($optionlist[$option] != $optionval) {
360                    $engineok = false;
361                }
362            }
363            if ($engineok) {
364                if ($checkfile) {
365                    if (!is_file($this->getConf($confprefix.$engine))) {
366                        return false;
367                    }
368                }
369                return $engine;
370            }
371        }
372        return false;
373    }
374
375    /**
376     * Cheque requirements...
377     */
378    function _requirements_ok() {
379
380        //check if at least one engine is specified
381        $engineok = false;
382        foreach ($this->mathmulti_engines as $engine => $engoptdict) {
383            if ($this->getConf($this->prefix.$engine) != "") {
384                $engineok = true;
385            }
386        }
387        if (!$engineok) {
388            $this->msg_disable .= $this->getLang($this->prefix.'noengine');
389            return false;
390        }
391
392        //check if the default settings/engine are ok
393        if (!$this->_getengine(
394                $this->_initoptions($this->mathmulti_options,$this->prefix),
395                $this->mathmulti_engines,
396                $this->prefix,
397                !$this->getConf($this->prefix.'iscgi'))) {
398          $this->msg_disable .= $this->getLang($this->prefix.'errdefault');
399          return false;
400        }
401
402        return true;
403    }
404
405
406    /**
407     *
408     */
409    function _cacheExists() {
410        global $dirimg, $mathmultiplugin_urlimg, $conf;
411
412        // check for default setting
413        if (!isset($dirimg) || !$dirimg) { $dirimg = $this->conf['mediadir']; }
414        if ($dirimg == $conf['mediadir']) {
415            // we don't want to clutter the root media dir, so create our own subfolder
416            $dirimg .= "/cache_mathmultiplugin";
417            $mathmultiplugin_urlimg .= "cache_mathmultiplugin%3a";
418
419            if (!@is_dir($dirimg)) {
420                $this->_mkdir($dirimg);
421            }
422        }
423
424        return @is_writable($dirimg);
425    }
426
427    /**
428     * used to avoid multiple messages
429     */
430    function _msg($str, $lvl=0) {
431        if ($this->msg_sent) return;
432
433        msg($str, $lvl);
434        $this->msg_sent = true;
435    }
436
437    /**
438     *
439     */
440    // would like to see this function in io.php :)
441    function _mkdir($d) {
442        global $conf;
443
444        umask($conf['dmask']);
445        $ok = io_mkdir_p($d);
446        umask($conf['umask']);
447        return $ok;
448    }
449
450}
451
452//Setup VIM: ex: et ts=4 enc=utf-8 :
453