1<?php 2/** 3 * DokuWiki Plugin ABC2 (Syntax Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Anika Henke <anika@selfthinker.org> 7 */ 8 9// must be run within Dokuwiki 10if (!defined('DOKU_INC')) { 11 die(); 12} 13 14class syntax_plugin_abc2 extends DokuWiki_Syntax_Plugin 15{ 16 /** 17 * @return string Syntax mode type 18 */ 19 public function getType() 20 { 21 return 'protected'; 22 } 23 24 /** 25 * @return string Paragraph type 26 */ 27 public function getPType() 28 { 29 return 'block'; 30 } 31 32 /** 33 * @return int Sort order - Low numbers go before high numbers 34 */ 35 public function getSort() 36 { 37 return 190; 38 } 39 40 /** 41 * Connect lookup pattern to lexer. 42 * 43 * @param string $mode Parser mode 44 */ 45 public function connectTo($mode) 46 { 47 $this->Lexer->addEntryPattern('<abc(?=.*\x3C/abc\x3E)',$mode,'plugin_abc2'); 48 } 49 50 public function postConnect() 51 { 52 $this->Lexer->addExitPattern('</abc>','plugin_abc2'); 53 } 54 55 /** 56 * Handle matches of the abc2 syntax 57 * 58 * @param string $match The match of the syntax 59 * @param int $state The state of the handler 60 * @param int $pos The position in the document 61 * @param Doku_Handler $handler The handler 62 * 63 * @return array Data for the renderer 64 */ 65 public function handle($match, $state, $pos, Doku_Handler $handler) 66 { 67 if ( $state == DOKU_LEXER_UNMATCHED ) { 68 $matches = preg_split('/>/u',$match,2); 69 $matches[0] = trim($matches[0]); 70 return array($matches[1],$matches[0]); 71 } 72 return true; 73 } 74 75 /** 76 * Render xhtml output or metadata 77 * 78 * @param string $mode Renderer mode (supported modes: xhtml) 79 * @param Doku_Renderer $renderer The renderer 80 * @param array $data The data from the handler() function 81 * 82 * @return bool If rendering was successful. 83 */ 84 public function render($mode, Doku_Renderer $renderer, $data) 85 { 86 if ($mode !== 'xhtml') { 87 return false; 88 } 89 90 if(strlen($data[0] ?? null) > 1){ 91 $src = $data[0]; 92 $transStr = $data[1]; 93 94 // display just the code if 'abcok' is switched off 95 if (!$this->getConf('abcok')) { 96 $renderer->doc .= $renderer->file($src); 97 return true; 98 } 99 100 // render the main ABC block 101 $src = $this->_fixLibraryBugs($src); 102 $this->_renderAbcBlock($renderer, $src, true); 103 104 // transposition 105 // via adding `shift=xy` to key information field 106 // doesn't work with abcjs this way 107 if ($transStr && ($this->getConf('library') !== 'abcjs')) { 108 $transArray = $this->_transStringToArray($transStr); 109 110 foreach($transArray as &$trans) { 111 $transShiftStr = $this->_transposeToShift($trans); 112 $keyLine = $this->_getAbcLine($src, 'K'); 113 $titleLine = $this->_getAbcLine($src, 'T'); 114 115 // checking for already existing shift|score|sound not necessary 116 // a first 'shift' parameter is ignored 117 // 'score' or 'sound' will cause the score to be transposed further 118 if ($keyLine && $titleLine) { 119 $transSrc = $src; 120 121 // add shift parameter into key information field 122 $keyLineNew = $keyLine.' shift='.$transShiftStr; 123 $transSrc = $this->_replace_first($transSrc, $keyLine, $keyLineNew); 124 125 // add transposition semitone after title 126 $titleLineNew = $titleLine.' ['.$trans.']'; 127 $transSrc = $this->_replace_first($transSrc, $titleLine, $titleLineNew); 128 129 // render another ABC block per transposition 130 $this->_renderAbcBlock($renderer, $transSrc, false); 131 } 132 } 133 } 134 } 135 return true; 136 137 } 138 139 /** 140 * Get transposition parameters into reasonable array 141 * 142 * @param string $str ABC parameter, string of transposition numbers 143 * 144 * @return array Array with transposition numbers 145 */ 146 function _transStringToArray($str) { 147 $arr = explode(" ", $str); 148 // the semitones to transpose have to be integers 149 $arr = array_map("intval", $arr); 150 // do not transpose by the same amount of semitones more than once 151 $arr = array_unique($arr); 152 // do not transpose higher or lower than 12 semitones 153 $arr = array_filter($arr, function($t){ return($t<12 && $t >-12); }); 154 // do not allow transposition into more than 8 keys 155 array_splice($arr, 8); 156 return $arr; 157 } 158 159 /** 160 * Turn transposition number into 'shift' voice modifier 161 * 162 * ABC 2.1 had 'transpose' which worked with semitones 163 * ABC 2.2 has 'shift' which works with an interval of two notes 164 * 165 * @param int $num transpose, number of semitones 166 * 167 * @return string shift, string of two notes 168 */ 169 function _transposeToShift($num) { 170 $arr = array( 171 0 => 'CC', 172 1 => 'Bc', 173 2 => 'CD', 174 3 => 'Bd', 175 4 => 'CE', 176 5 => 'CF', 177 6 => 'BF', 178 7 => 'CG', 179 8 => 'Bg', 180 9 => 'CA', 181 10 => 'Ba', 182 11 => 'CB', 183 12 => 'Cc', 184 -1 => 'cB', 185 -2 => 'DC', 186 -3 => 'dB', 187 -4 => 'EC', 188 -5 => 'FC', 189 -6 => 'FB', 190 -7 => 'GC', 191 -8 => 'gB', 192 -9 => 'AC', 193 -10 => 'aB', 194 -11 => 'BC', 195 -12 => 'cC', 196 ); 197 return $arr[$num]; 198 } 199 200 /** 201 * Calculate default unit length 202 * according to http://abcnotation.com/wiki/abc:standard:v2.1#lunit_note_length 203 * 204 * @param string $meterLine line of meter (M) information field 205 * 206 * @return string string with default length 207 */ 208 function _getDefaultLength($meterLine) { 209 $meter = preg_replace('/\s?M\s?:/', '', $meterLine); 210 211 // default to 1/8 if meter is empty or "none" 212 if (!$meter || $meter == 'none') return "1/8"; 213 214 // replace meter symbols with standard meters 215 $meter = str_replace('C|', '2/4', $meterLine); 216 $meter = str_replace('C', '4/4', $meterLine); 217 218 // meter is usually in the form <number>/<number> 219 preg_match("/(\d)\/(\d)/", $meter, $matches); 220 // default to 1/8 if meter isn't in that form 221 if (count($matches) != 3) return "1/8"; 222 223 // default unit length calculation 224 $ratio = (int) $matches[1] / (int) $matches [2]; 225 if ($ratio < 0.75) { 226 $length = "1/16"; 227 } else { 228 $length = "1/8"; 229 } 230 231 return $length; 232 } 233 234 /** 235 * Build classes for abc container depending on chosen abc library 236 * 237 * @param bool $orig original source (not a transposition) 238 * 239 * @return array CSS classes 240 */ 241 function _getClasses($orig) { 242 switch($this->getConf('library')) { 243 case 'abcjs': 244 // makes the midi player bigger 245 $libClasses = 'abcjs-large'; 246 break; 247 248 case 'abc2svg': 249 $libClasses = 'abc'; 250 break; 251 252 case 'abc-ui': 253 // 'abc-source' is mandatory and needs to be first 254 $libClasses = 'abc-source '.$this->getConf('abcuiConfig'); 255 break; 256 } 257 258 // generic class plus class identifying the chosen library 259 $containerClasses = ' abc2-plugin lib-'.$this->getConf('library'); 260 261 if ($orig && $this->getConf('showSource')) { 262 $containerClasses .= ' show-source'; 263 } else { 264 $containerClasses .= ' hide-source'; 265 } 266 267 return array( 268 'lib-classes' => $libClasses, 269 'container-classes' => $containerClasses, 270 ); 271 } 272 273 /** 274 * Fix ABC library bugs: 275 * 276 * * abc2svg doesn't render anything if there is a space after the X: 277 * * $ABC_UI messes with note lengths if L isn't set 278 * 279 * @param string $src ABC code source 280 * 281 * @return string adjusted ABC code 282 */ 283 function _fixLibraryBugs($src) { 284 // remove spaces after 'X:' 285 // fixes a bug in abc2svg which won't render anything with a space after X: 286 // fixed upstream, see https://chiselapp.com/user/moinejf/repository/abc2svg/tktview?name=25d793e76f 287 $xLine = $this->_getAbcLine($src, 'X'); 288 $xLineNoSpaces = str_replace(' ', '', $xLine); 289 $src = $this->_replace_first($src, $xLine, $xLineNoSpaces); 290 291 // add L: line if there isn't one 292 // fixes bug in $ABC_UI which has a wrong default unit length 293 $lLine = $this->_getAbcLine($src, 'L'); 294 if (!$lLine) { 295 $mLine = $this->_getAbcLine($src, 'M'); 296 297 if ($mLine) { 298 $lValue = $this->_getDefaultLength($mLine); 299 $mLineAndLline = $mLine.NL.'L:'.$lValue; 300 $src = $this->_replace_first($src, $mLine, $mLineAndLline); 301 } 302 } 303 return $src; 304 } 305 306 /** 307 * Render block of ABC 308 * 309 * @param Doku_Renderer $renderer The renderer 310 * @param string $src ABC code source 311 * @param bool $orig original source (not a transposition) 312 * 313 * @return void 314 */ 315 function _renderAbcBlock($renderer, $src, $orig) { 316 $classes = $this->_getClasses($orig); 317 318 // needs an extra parent div because abc2svg will otherwise break any broken rhythm 319 // see https://chiselapp.com/user/moinejf/repository/abc2svg/tktview/f632b51e4da81e3bd8292a30d078a5810488b878 320 // cannot be used for all libs because otherwise abc-ui will break 321 if ($this->getConf('library') == 'abc2svg') { 322 $renderer->doc .= '<div class="'.$classes['container-classes'].'">'.NL; 323 $renderer->doc .= '<div class="'.$classes['lib-classes'].'">'; 324 } else { 325 // needs to be a div, otherwise abc-ui won't work 326 $renderer->doc .= '<div class="'.$classes['lib-classes'].$classes['container-classes'].'">'; 327 } 328 329 if ($this->getConf('library') == 'abc-ui') { 330 $renderer->doc .= '%%player_top'.NL; 331 } 332 $renderer->doc .= hsc($src); 333 334 if ($this->getConf('library') == 'abc2svg') { 335 $renderer->doc .= '</div>'; 336 } 337 $renderer->doc .= '</div>'.NL; 338 } 339 340 /** 341 * Get line of ABC with specific information field 342 * 343 * @param string $src ABC code source 344 * @param string $field ABC information field identifier 345 * 346 * @return string information field, whole line 347 */ 348 function _getAbcLine($src, $field) { 349 if (preg_match("/^\s?".$field."\s?:(.*?)$/m", $src, $result)) { 350 return $result[0]; 351 } else { 352 return false; 353 } 354 } 355 356 /** 357 * Replace first string 358 * 359 * @author Zombat [https://stackoverflow.com/users/81205/zombat] 360 * @source https://stackoverflow.com/a/1252710/340300 361 * @license CC BY-SA 3.0 [https://creativecommons.org/licenses/by-sa/3.0/] 362 * 363 * @param string $haystack 364 * @param string $needle 365 * @param string $replace 366 * 367 * @return string 368 */ 369 function _replace_first($haystack, $needle, $replace) { 370 $pos = strpos($haystack, $needle); 371 if ($pos !== false) { 372 $newstring = substr_replace($haystack, $replace, $pos, strlen($needle)); 373 } 374 return $newstring; 375 } 376 377} 378