1<?php
2/**
3 * ABC Plugin (http://dokuwiki.org/plugin:abc)
4 * for ABC notation (http://abcnotation.org.uk/)
5 * in DokuWiki (http://dokuwiki.org/)
6 *
7 * @license     GPL 2 (http://www.gnu.org/licenses/gpl.html)
8 * @author      Anika Henke <anika@selfthinker.org>
9 */
10
11if(!defined('DOKU_INC')) die();
12if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
13require_once(DOKU_PLUGIN.'syntax.php');
14
15class syntax_plugin_abc extends DokuWiki_Syntax_Plugin {
16
17    function getType(){
18        return 'protected';
19    }
20    function getPType(){
21        return 'block';
22    }
23    function getSort(){
24        return 192;
25    }
26
27    function connectTo($mode) {
28        $this->Lexer->addEntryPattern('<abc(?=.*\x3C/abc\x3E)',$mode,'plugin_abc');
29    }
30    function postConnect() {
31        $this->Lexer->addExitPattern('</abc>','plugin_abc');
32    }
33
34
35    function handle($match, $state, $pos, Doku_Handler $handler){
36        if ( $state == DOKU_LEXER_UNMATCHED ) {
37            $matches = preg_split('/>/u',$match,2);
38            $matches[0] = trim($matches[0]);
39            return array($matches[1],$matches[0]);
40        }
41        return true;
42    }
43
44    /**
45     * Create output
46     */
47    function render($mode, Doku_Renderer $renderer, $data) {
48        global $INFO;
49        global $ACT;
50        global $conf;
51        if($mode == 'xhtml' && strlen($data[0]) > 1){
52            $src = $data[0];
53            $origSrc = $src;
54            $trans = "0 ".$data[1]; // "0" includes the original key
55            $debug = $conf['allowdebug'];
56
57            $error = $this->_checkExecs();
58            if($this->getConf('abcok') && (!$INFO['rev'] || ($INFO['rev'] && ($ACT=='preview'))) && !$error){
59            //do not create/show files if an old revision is viewed, but always if the page is previewed and never when there is an error
60
61                $entitiesFile = dirname(__FILE__).'/conf/entities.conf';
62                if (@file_exists($entitiesFile)) {
63                    $entities = confToHash($entitiesFile);
64                    $src = strtr($src,$entities);
65                }
66                $fileBase = $this->_getFileBase($origSrc);
67                $srcFile = $fileBase.'.abc';
68                $srcChanged = (!file_exists($srcFile) || (file_exists($srcFile) && $src!=io_readFile($srcFile)));
69                if ($srcChanged) io_saveFile($srcFile, $src);
70
71                if ($this->getConf('abc2abc') && is_executable($this->getConf('abc2abc'))) {
72                    $transSrc = $this->_getTransSrc($trans);
73                    $transNew = $this->_getTransNew($fileBase, $transSrc);
74                } else {
75                    $transSrc = array(0);
76                    $transNew = array();
77                }
78                $renderList = $srcChanged ? $transSrc : $transNew;
79                if($debug || $_REQUEST['purge']) $renderList = $transSrc;
80
81                // create files
82                foreach ($renderList as $transMode) {
83                    // if no transposition is allowed and the tune shall be transposed
84                    // by 0 semitones (= not at all), then nothing is appended to the fileBase;
85                    // else append the amount of semitiones to the fileBase
86                    $curFileBase = ($transMode==0) ? $fileBase : $fileBase."_".$transMode;
87                    $abcFile = $curFileBase.'.abc';
88                    io_saveFile($abcFile, $src);
89
90                    ob_start();
91
92                    if ($transMode!=0) {
93                        $this->_transpose($abcFile, $srcFile, $transMode);
94                    }
95                    $debugLog = $this->_createImgFile($abcFile, $curFileBase);
96
97                    if ($this->getConf('displayType')==1 || $this->getConf('displayType')==2) {
98                        $this->_createMidiFile($abcFile, $curFileBase);
99                    }
100                    if ($this->getConf('displayType')==2) {
101                        $this->_createPsFile($abcFile, $curFileBase);
102                        if ($this->getConf('ps2pdf')) {
103                            $this->_createPdfFile($abcFile, $curFileBase);
104                        }
105                    }
106                    $errorLog = ob_get_contents();
107                    ob_end_clean();
108                }
109                if (($this->getConf('displayErrorlog') || $debug) && $errorLog) {
110                    $errorLog = str_replace($this->_getAbc2psVersion(), "abc2ps", $errorLog);
111                    //hide abc2ps version for security reasons
112                    //TODO: hide lines starting with "writing MIDI file", "File", "Output written on", ... for boring reasons
113                    msg(nl2br($errorLog), 2);
114                }
115                if ($debugLog) {
116                    msg($debugLog);
117                }
118                // display files
119                foreach ($transSrc as $transMode) {
120                    $curFileBase = ($transMode==0) ? $fileBase : $fileBase."_".$transMode;
121                    $renderer->doc .= $this->_showFiles($curFileBase);
122                }
123
124                // always have the abc source in the html source (for search engine optimization)
125                // only per css visible when displaySource = 1
126                if ($this->getConf('displaySource')) $visible = " visible";
127                $renderer->doc .= '<div class="abc_src'.$visible.'">'.NL;
128                $renderer->doc .= $renderer->file($origSrc);
129                $renderer->doc .= '</div>'.NL;
130            } else {
131                if ($error && $this->getConf('abcok')) {
132                    msg($error, -1);
133                }
134                $renderer->doc .= $renderer->file($origSrc);
135            }
136            return true;
137        }
138        return false;
139    }
140
141    /**
142     * check if all needed programs are set, existent and executable
143     */
144    function _checkExecs() {
145        global $conf;
146        $error .= $this->_checkExec($this->getConf('abc2ps'), 'abc2ps');
147        $error .= $this->_checkExec($conf['im_convert'], 'im_convert');
148        if (($this->getConf('displayType')==1) || ($this->getConf('displayType')==2)) {
149            $tmpError1 = $this->_checkExec($this->getConf('abc2midi'), 'abc2midi');
150            if ($tmpError1) $error .= $tmpError1."If you do not wish to install it, you can change the 'displayType' to '0' ('image only').<br />";
151        }
152        if($this->getConf('ps2pdf') && ($this->getConf('displayType')==2)) {
153            $tmpError2 = $this->_checkExec($this->getConf('ps2pdf'), 'ps2pdf');
154            if ($tmpError2) $error .= $tmpError2."If you do not wish to install it, you can leave it blank and a ps file will be generated instead.<br />";
155        }
156        return $error;
157    }
158    /**
159     * check if a program is set, existent and executable
160     */
161    function _checkExec($execFile, $execName) {
162        if (!$execFile) {
163            $error .= "The '<strong>".$execName."</strong>' config option is <strong>not set</strong>.<br />";
164        } else if (!file_exists($execFile)) {
165            $error .= "'".$execFile."' (<strong>".$execName."</strong>) is <strong>not existent</strong>.<br />";
166        } else if (!is_executable($execFile)) {
167            $error .= "'".$execFile."' (<strong>".$execName."</strong>) is <strong>not executable</strong>.<br />";
168        }
169        return $error;
170    }
171
172    /**
173     * get to-be directory and filename (without extension)
174     *
175     * all files are stored in the media directory into 'plugin_abc/<namespaces>/'
176     * and the filename is a mixture of abc-id and abc-title (e.g. 42_the_title.abc|...)
177     *
178     */
179    function _getFileBase($src) {
180        global $ID;
181        global $ACT;
182        global $conf;
183        $mediadir = $conf['mediadir'];
184
185        // where to store the abc media files
186        $abcdir = $this->getConf('mediaNS') ? $mediadir.'/'.$this->getConf('mediaNS') : $mediadir;
187        io_makeFileDir($abcdir);
188        $fileDir = $abcdir.'/'.utf8_encodeFN(str_replace(':','/',getNS($ID)));
189
190        // the abcID is what comes after the 'X:'
191        preg_match("/\s?X\s?:(.*?)\n/s", $src, $matchesX);
192        $abcID = preg_replace('/\s?X\s?:/', '', $matchesX[0]);
193        // the abcTitle is what comes after the (first) 'T:'
194        preg_match("/\s?T\s?:(.*?)\n/s", $src, $matchesT);
195        $abcTitle = preg_replace('/\s?T\s?:/', '', $matchesT[0]);
196        $fileName = cleanID($abcID."_".$abcTitle);
197
198        // no double slash when in root namespace
199        $slashStr = (getNS($ID)) ? "/" : "";
200        // have different fileBase for previewing
201        $previewPrefix = ($ACT!='preview') ? "" : "x";
202
203        $fileBase = $fileDir.$slashStr.$previewPrefix.$fileName;
204        // unfortunately abcm2ps seems not to be able to handle
205        // file names (realpath) of more than 120 characters
206        $realFileBaseLen = (strlen(fullpath($abcdir)) - strlen($abcdir)) + strlen($fileBase);
207        $char_len = 114;
208        if ($realFileBaseLen >= $char_len) {
209            $truncLen = strlen($fileBase) + ($char_len - $realFileBaseLen);
210            $fileBase = substr($fileBase, 0, $truncLen);
211        }
212        return $fileBase;
213    }
214
215    /**
216     * get transposition parameters from the source into a reasonable array
217     */
218    function _getTransSrc($trans) {
219        $transSrc = explode(" ", $trans);
220        // the semitones to transpose have to be integers
221        $transSrc = array_map("intval", $transSrc);
222        // do not transpose by the same amount of semitones more than once
223        $transSrc = array_unique($transSrc);
224        // do not transpose higher or lower than 24 semitones
225        $transSrc = array_filter($transSrc, create_function('$t', 'return($t<24 && $t >-24);'));
226        // do not allow transposition into more than 8 keys
227        array_splice($transSrc, 8);
228        return $transSrc;
229    }
230
231    /**
232     * get all new and old trans params
233     * return the new params, delete the corresponding old files
234     */
235    function _getTransNew($fileBase, $transSrc) {
236        // get all abc files belonging to the fileBase
237        $filesArrABC = glob(dirname($fileBase)."/{".basename($fileBase)."*.abc}", GLOB_BRACE);
238        $transFS = array(0); // always include the original key
239        // get all numbers after the '_' and before the '.abc'
240        foreach ($filesArrABC as $f) {
241            $f = basename($f, ".abc");
242            $tr = substr(strrchr($f,'_'),1);
243            if (intval($tr)) $transFS[] = $tr;
244        }
245
246        $transNew = array_diff($transSrc, $transFS);
247        $transOld = array_diff($transFS, $transSrc);
248
249        // delete old transposed files
250        foreach ($transOld as $o) {
251            $filesArrAll = glob(dirname($fileBase)."/{".basename($fileBase)."_".$o."*}", GLOB_BRACE);
252            foreach ($filesArrAll as $d) {
253                unlink($d);
254            }
255        }
256        return $transNew;
257    }
258
259    /**
260     * transpose and create transposed abc
261     */
262    function _transpose($abcFile, $srcFile, $trans) {
263        passthru(fullpath($this->getConf('abc2abc'))." $srcFile -e -t $trans > $abcFile");
264    }
265
266    /**
267     * create img file
268     */
269    function _createImgFile($abcFile, $fileBase) {
270        global $conf;
271        $epsFile = $fileBase.'001.eps';
272        $imgFile = $fileBase.'.png';
273        $debug = $conf['allowdebug'];
274        $debugOutput = '';
275
276        // create eps file
277        $epsCommand = fullpath($this->getConf('abc2ps'))." $abcFile ".$this->getConf('params4img')." -E -O $fileBase.";
278        passthru($epsCommand." 2>&1");
279
280        if($debug) {
281            $debugOutput .= "<h3>Debug Info for $abcFile</h3>";
282            $debugOutput .= "<p>EPS file '".$epsFile."'";
283            $debugOutput .= file_exists($epsFile) ? " <strong>exists</strong>" : " <strong>does not exist</strong>";
284            $debugOutput .= ", command used to create it:</p>";
285            $debugOutput .= "<pre>".$epsCommand."</pre>";
286        }
287
288        // convert eps to png file
289        $pngCommand = fullpath($conf['im_convert'])." $epsFile $imgFile";
290        passthru($pngCommand);
291
292        if($debug) {
293            $debugOutput .= "<p>PNG file '".$imgFile."'";
294            $debugOutput .= file_exists($imgFile) ? " <strong>exists</strong>" : " <strong>does not exist</strong>";
295            $debugOutput .= ", command used to create it:</p>";
296            $debugOutput .= "<pre>".$pngCommand."</pre>";
297        } else {
298            if(file_exists($epsFile)) unlink($epsFile);
299        }
300
301        return $debugOutput;
302    }
303    /**
304     * create ps file
305     */
306    function _createPsFile($abcFile, $fileBase) {
307        $psFile  = $fileBase.'.ps';
308        $fmt = $this->getConf('fmt');
309        $addFmt = ($fmt && file_exists($fmt)) ? " -F ".fullpath($fmt) : "";
310        passthru(fullpath($this->getConf('abc2ps'))." $abcFile $addFmt ".$this->getConf('params4ps')." -O $psFile 2>&1");
311    }
312    /**
313     * create pdf file
314     */
315    function _createPdfFile($abcFile, $fileBase) {
316        $psFile  = $fileBase.'.ps';
317        $pdfFile  = $fileBase.'.pdf';
318        passthru(fullpath($this->getConf('ps2pdf'))." $psFile $pdfFile");
319        if(file_exists($psFile)) unlink($psFile);
320    }
321    /**
322     * create midi file
323     */
324    function _createMidiFile($abcFile, $fileBase) {
325        $midFile = $fileBase.'.mid';
326        passthru(fullpath($this->getConf('abc2midi'))." $abcFile -o $midFile");
327    }
328    /**
329     * get abc2ps version
330     */
331    function _getAbc2psVersion() {
332        ob_start();
333        passthru(fullpath($this->getConf('abc2ps'))." -V 2>&1");
334        $version = ob_get_contents();
335        $version = explode("\n",$version);
336        ob_end_clean();
337        return $version[0];
338    }
339
340    /**
341     * get file and check if it exists
342     */
343    function _getFile($fileBase, $ext) {
344        $file = $fileBase.$ext;
345        return (file_exists($file)) ? $file : 0;
346    }
347
348    /**
349     * get ID that has to be called from fetch.php
350     */
351    function _getFileID($file) {
352        global $ID;
353        return (getNS($ID)) ? getNS($ID).":".basename($file) : basename($file);
354    }
355
356    /**
357     * html for internal media
358     */
359    function _showFile($file) {
360        if (!$file) {
361            return "Error: The file could not be generated.";
362        }
363
364        $mediaNS = $this->getConf('mediaNS').":";
365        $name = $this->_getFileID($file);
366        $id = $mediaNS.$name;
367        $url = ml($id, array('t' => time())); // add timestamp for cache busting
368        list($ext, $mime) = mimetype($file, false);
369
370        if(substr($mime, 0, 5) == 'image') {
371            $imgSize = getimagesize($file);
372            $imgSize = $imgSize[3];
373            return '<img src="'.$url.'" '.$imgSize.' alt="" />';
374        } else {
375            $class = 'mediafile mf_'.preg_replace('/[^_\-a-z0-9]+/i', '_', $ext);
376            return '<a href="'.$url.'" class="'.$class.'">'.$name.'</a>';
377        }
378    }
379
380
381    /**
382     * html for displaying all files; dependant on displayType
383     */
384    function _showFiles($fileBase) {
385        $imgFile = $this->_getFile($fileBase, '.png');
386        $midFile = $this->_getFile($fileBase, '.mid');
387        $abcFile = $this->_getFile($fileBase, '.abc');
388        $psFile  = $this->_getFile($fileBase, '.ps');
389        $pdfFile = $this->_getFile($fileBase, '.pdf');
390        $mediaNS = $this->getConf('mediaNS').":";
391        $showImg = $this->_showFile($imgFile);
392
393        switch ($this->getConf('displayType')) {
394            // image only (case 0 and default)
395            default:
396            case 0:
397                $display = '<p>'.$showImg.'</p>'.NL;
398                break;
399
400            // image linked to midi
401            case 1:
402                $display = $showImg;
403                if($midFile) {
404                    $display = '<a href="'.ml($mediaNS.$this->_getFileID($midFile)).'">'.$display.'</a>';
405                }
406                $display .= '<p>'.$display.'</p>'.NL;
407                break;
408
409            // image with list of abc, midi, ps/pdf
410            case 2:
411                $display = '<ul>';
412                // abc file is always there
413                $display .= '<li>'.$this->_showFile($abcFile).'</li>';
414                // midi file
415                $display .= '<li>'.$this->_showFile($midFile).'</li>';
416                // display pdf file if there is any, else display ps file
417                if ($this->getConf('ps2pdf') && $pdfFile) {
418                    $display .= '<li>'.$this->_showFile($pdfFile).'</li>';
419                } else {
420                    $display .= '<li>'.$this->_showFile($psFile).'</li>';
421                }
422                $display .= '</ul>'.NL;
423                $display .= '<p>'.$showImg.'</p>'.NL;
424                break;
425        }
426        $display = '<div class="abc">'.NL.$display.'</div>'.NL;
427        return $display;
428    }
429
430
431}
432