r106793 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r106792‎ | r106793 | r106794 >
Date:12:56, 20 December 2011
Author:mah
Status:deferred (Comments)
Tags:score 
Comment:
* ABC conversion support
* Updating README regarding ABC support
* Added auto-rendering exceptions
* Automated LilyPond version detection
(For complete commits see https://github.com/TheCount/score/commits/master/)
Author: Alexander Klauer
Modified paths:
  • /trunk/extensions/Score/README (modified) (history)
  • /trunk/extensions/Score/Score.body.php (modified) (history)
  • /trunk/extensions/Score/Score.i18n.php (modified) (history)
  • /trunk/extensions/Score/Score.php (modified) (history)

Diff [purge]

Index: trunk/extensions/Score/Score.i18n.php
@@ -27,35 +27,50 @@
2828
2929 /* English */
3030 $messages['en'] = array(
 31+ 'score-abc2lynotexecutable' => 'ABC to LilyPond converter could not be executed.',
 32+ 'score-abcconversionerr' => 'Unable to convert ABC file to LilyPond format:
 33+$1',
 34+ 'score-noabcinput' => 'ABC source file could not be created',
3135 'score-chdirerr' => 'Unable to change directory',
3236 'score-cleanerr' => 'Unable to clean out old files before re-rendering',
3337 'score-compilererr' => 'Unable to compile LilyPond input file:
3438 $1',
3539 'score-desc' => 'Adds a tag for rendering musical scores with LilyPond',
3640 'score-getcwderr' => 'Unable to obtain current working directory',
 41+ 'score-invalidlang' => 'Invalid score language specified. Currently recognised languages are lang="lilypond" (the default) and lang="ABC".',
3742 'score-nooutput' => 'Failed to create LilyPond image directory',
3843 'score-nofactory' => 'Failed to create LilyPond factory directory',
3944 'score-noinput' => 'Failed to create LilyPond input file',
 45+ 'score-notexecutable' => 'Could not execute LilyPond. Make sure <code>$wgLilyPond</code> is set correctly.',
4046 'score-page' => 'Page $1',
 47+ 'score-pregreplaceerr' => 'PCRE regular expression replacement failed',
 48+ 'score-readerr' => 'Unable to read file',
4149 'score-renameerr' => 'Error moving score files to upload directory',
4250 'score-trimerr' => 'Image could not be trimmed. Set $wgScoreTrim=false if this problem persists.',
43 - 'score-notexecutable' => 'Could not execute LilyPond. Make sure <code>$wgLilyPond</code> is set correctly.',
 51+ 'score-versionerr' => 'Unable to obtain LilyPond version.',
4452 );
4553
4654 /** Message documentation (Message documentation) */
4755 $messages['qqq'] = array(
 56+ 'score-abc2lynotexecutable' => 'Displayed if the ABC to LilyPond converter could not be executed.',
 57+ 'score-abcconversionerr' => 'Displayed if the ABC to LilyPond conversion failed. $1 is the error (generally big block of text in a pre tag)',
 58+ 'score-noabcinput' => 'Displayed if an ABC source file could not be created for lang="ABC".',
4859 'score-chdirerr' => 'Displayed if the extension cannot change its working directory.',
4960 'score-cleanerr' => 'Displayed if an old file cleanup operation fails.',
5061 'score-compilererr' => 'Displayed if the LilyPond code could not be compiled. $1 is the error (generally big block of text in a pre tag)',
5162 'score-desc' => '{{desc}}',
5263 'score-getcwderr' => 'Displayed if the extension cannot obtain the CWD.',
 64+ 'score-invalidlang' => 'Displayed if the lang="…" attribute contains an unrecognised score language.',
5365 'score-nooutput' => 'Displayed if the LilyPond image/midi dir cannot be created.',
5466 'score-nofactory' => 'Displayed if the LilyPond/ImageMagick working directory cannot be created.',
5567 'score-noinput' => 'Displayed if the LilyPond input file cannot be created.',
 68+ 'score-notexecutable' => 'Displayed if LilyPond binary can\'t be executed.',
5669 'score-page' => 'The word "Page" as used in pagination. $1 is the page number',
 70+ 'score-pregreplaceerr' => 'Displayed if a PCRE regular expression replacement failed.',
 71+ 'score-readerr' => 'Displayed if the extension could not read a file',
5772 'score-renameerr' => 'Displayed if moving the resultant files from the working environment to the upload directory fails.',
5873 'score-trimerr' => 'Displayed if the extension failed to trim an output image.',
59 - 'score-notexecutable' => "Displayed if LilyPond binary can't be executed.",
 74+ 'score-versionerr' => 'Displayed if the extension failed to obtain the version string of LilyPond.',
6075 );
6176
6277 /** German (Deutsch)
Index: trunk/extensions/Score/Score.php
@@ -47,6 +47,8 @@
4848 $wgScoreTrim = null;
4949 /* Path of lilypond executable */
5050 $wgLilyPond = '/usr/bin/lilypond';
 51+/* Path to converter from ABC */
 52+$wgAbc2Ly = '/usr/bin/abc2ly';
5153
5254 /*
5355 * Extension credits
Index: trunk/extensions/Score/README
@@ -26,6 +26,8 @@
2727
2828 require_once("$IP/extensions/Score/Score.php");
2929 $wgLilyPond = '/path/to/your/lilypond/executable'; /* required */
 30+ $wgAbc2Ly = '/path/to/your/abc2ly/executable'; /* if you want ABC to
 31+ LilyPond conversion */
3032 $wgScoreTrim = true; /* Set to false if you don't want score trimming */
3133
3234 to your LocalSettings.php file. If you get unexpected out-of-memory errors,
@@ -58,3 +60,22 @@
5961 You may also combine the "raw" and "midi" attributes, but remember that in
6062 this case, you need to provide the necessary \midi block yourself.
6163
 64+The extension also supports the ABC music notation through LilyPond's abc2ly
 65+converter (see the documentation for $wgAbc2Ly above). Use
 66+
 67+<score lang="ABC">…</score>
 68+
 69+to write music in ABC, for example
 70+
 71+<score lang="ABC" midi="1">
 72+X:1
 73+M:C
 74+L:1/4
 75+K:C
 76+C, D, E, F,|G, A, B, C|D E F G|A B c d|e f g a|b c' d' e'|f' g' a' b'|]
 77+</score>
 78+
 79+As you can see, you may combine the lang attribute with the midi attribute.
 80+The raw attribute will be ignored if lang is specified to be anything else than
 81+"lilypond".
 82+
Index: trunk/extensions/Score/Score.body.php
@@ -27,8 +27,8 @@
2828 die( "This file cannot be run standalone.\n" );
2929 }
3030
31 -/*
32 - * Score ecxceptions
 31+/**
 32+ * Score exception
3333 */
3434 class ScoreException extends Exception {
3535 /**
@@ -37,47 +37,192 @@
3838 public function __construct( $message, $code = 0, Exception $previous = null ) {
3939 parent::__construct( $message, $code, $previous );
4040 }
 41+
 42+ /**
 43+ * Auto-renders exception as HTML error message in the wiki's content
 44+ * language.
 45+ *
 46+ * @return error message HTML.
 47+ */
 48+ public function __toString() {
 49+ return Html::rawElement(
 50+ 'span',
 51+ array( 'class' => 'error' ),
 52+ wfMessage( $this->getMessage() )->inContentLanguage()->parse()
 53+ );
 54+ }
4155 }
4256
43 -/*
 57+/**
 58+ * Score call exception.
 59+ * This is a type of exception thrown when a call to some binary used by the
 60+ * Score extension fails.
 61+ */
 62+class ScoreCallException extends ScoreException {
 63+ /**
 64+ * Error message returned from the call.
 65+ */
 66+ private $callErrMsg;
 67+
 68+ /**
 69+ * Constructs a new ScoreCallException.
 70+ *
 71+ * @param $message Message key to be used. It should accept one
 72+ * parameter, the error message returned from the binary.
 73+ * @param $callErrMsg Raw error message returned by the binary.
 74+ */
 75+ public function __construct( $message, $callErrMsg, $code = 0, Exception $previous = null ) {
 76+ $this->callErrMsg = $callErrMsg;
 77+ parent::__construct( $message, $code, $previous );
 78+ }
 79+
 80+ /**
 81+ * Auto-renders exception as HTML error message in the wiki's content
 82+ * language.
 83+ *
 84+ * @return error message HTML.
 85+ */
 86+ public function __toString() {
 87+ return wfMessage( $this->getMessage() )
 88+ ->inContentLanguage()
 89+ ->rawParams( Html::rawElement( 'pre', array(), strip_tags( $this->callErrMsg ) ) )
 90+ ->parse();
 91+ }
 92+}
 93+
 94+/**
4495 * Score class
4596 */
4697 class Score {
4798 /**
 99+ * LilyPond version string.
 100+ * It defaults to null and is set the first time it is required.
 101+ */
 102+ private static $lilypondVersion = null;
 103+
 104+ /**
 105+ * Determines the version of LilyPond in use and writes the version
 106+ * string to self::$lilypondVersion.
 107+ *
 108+ * @throws ScoreException if LilyPond could not be executed properly.
 109+ */
 110+ private static function getLilypondVersion() {
 111+ global $wgLilyPond;
 112+
 113+ if ( !is_executable( $wgLilyPond ) ) {
 114+ throw new ScoreException( 'score-notexecutable' );
 115+ }
 116+
 117+ $cmd = wfEscapeShellArg( $wgLilyPond ) . ' --version';
 118+ $stdout = wfShellExec( $cmd, $rc );
 119+ if ( $rc != 0 ) {
 120+ throw new ScoreException( 'score-versionerr' );
 121+ }
 122+
 123+ $n = sscanf( $stdout, 'GNU LilyPond %s', self::$lilypondVersion );
 124+ if ( $n != 1 ) {
 125+ self::$lilypondVersion = null;
 126+ throw new ScoreException( 'score-versionerr' );
 127+ }
 128+ }
 129+
 130+ /**
48131 * Renders the lilypond code in a <score>…</score> tag.
49132 *
50 - * @param $lilypondCode
 133+ * @param $code
51134 * @param $args
52135 * @param $parser
53136 * @param $frame
54137 *
55138 * @return Image link HTML, and possibly anchor to MIDI file.
56139 */
57 - public static function render( $lilypondCode, array $args, Parser $parser, PPFrame $frame ) {
 140+ public static function render( $code, array $args, Parser $parser, PPFrame $frame ) {
 141+ global $wgTmpDirectory;
58142
59 - if ( array_key_exists( 'midi', $args ) ) {
60 - $renderMidi = $args['midi'];
61 - } else {
62 - $renderMidi = false;
 143+ try {
 144+ /* create working environment */
 145+ $factoryPrefix = 'MWLP.';
 146+ $fuzz = md5( mt_rand() );
 147+ $factoryDirectory = $wgTmpDirectory . "/$factoryPrefix$fuzz";
 148+ $rc = wfMkdirParents( $factoryDirectory, 0700, __METHOD__ );
 149+ if ( !$rc ) {
 150+ throw new ScoreException( 'score-nofactory' );
 151+ }
 152+ } catch ( ScoreException $e ) {
 153+ return $e;
63154 }
64 - if ( array_key_exists( 'raw', $args ) && $args['raw'] ) {
65 - return self::runRaw( $lilypondCode, $renderMidi );
66 - } else {
67 - return self::run( $lilypondCode, $renderMidi );
 155+
 156+ try {
 157+ /* Midi rendering? */
 158+ if ( array_key_exists( 'midi', $args ) ) {
 159+ $renderMidi = $args['midi'];
 160+ } else {
 161+ $renderMidi = false;
 162+ }
 163+
 164+ /* Score language selection */
 165+ if ( array_key_exists( 'lang', $args ) ) {
 166+ $lang = $args['lang'];
 167+ } else {
 168+ $lang = 'lilypond';
 169+ }
 170+
 171+ /* Create lilypond input file */
 172+ $lilypondFile = $factoryDirectory . '/file.ly';
 173+ switch ( $lang ) {
 174+ case 'lilypond':
 175+ if ( !array_key_exists( 'raw', $args ) || !$args['raw'] ) {
 176+ $lilypondCode = self::embedLilypondCode( $code, $renderMidi );
 177+ $altText = $code;
 178+ } else {
 179+ $lilypondCode = $code;
 180+ $altText = false;
 181+ }
 182+ $rc = file_put_contents( $lilypondFile, $lilypondCode );
 183+ if ( $rc === false ) {
 184+ throw new ScoreException( 'score-noinput' );
 185+ }
 186+ break;
 187+ case 'ABC':
 188+ $altText = false;
 189+ self::runAbc2Ly( $code, $factoryDirectory );
 190+ break;
 191+ default:
 192+ throw new ScoreException( 'score-invalidlang' );
 193+ }
 194+
 195+ $html = self::runLilypond( $factoryDirectory, $renderMidi, $altText );
 196+ } catch ( ScoreException $e ) {
 197+ self::eraseFactory( $factoryDirectory );
 198+ return $e;
68199 }
 200+
 201+ /* tear down working environment */
 202+ if ( !self::eraseFactory( $factoryDirectory ) ) {
 203+ self::debug( "Unable to delete temporary working directory.\n" );
 204+ }
 205+
 206+ return $html;
69207 }
70208
71209 /**
72 - * Runs lilypond with the code embedded in a score block.
 210+ * Embeds simple LilyPond code in a score block.
73211 *
74212 * @param $lilypondCode
75213 * @param $renderMidi
76214 *
77 - * @return Image link HTML, and possibly anchor to MIDI file.
 215+ * @return Raw lilypond code.
 216+ *
 217+ * @throws ScoreException if determining the LilyPond version fails.
78218 */
79 - private static function run( $lilypondCode, $renderMidi ) {
 219+ private static function embedLilypondCode( $lilypondCode, $renderMidi ) {
 220+ /* Get LilyPond version if we don't know it yet */
 221+ if ( self::$lilypondVersion === null ) {
 222+ self::getLilypondVersion();
 223+ }
 224+
80225 /* Raw code. Note: the "strange" ##f, ##t, etc., are actually part of the lilypond code!
81 - * The raw code was taken directly from the original LilyPond extension */
 226+ * The raw code is based on the raw code from the original LilyPond extension */
82227 $raw = "\\header {\n"
83228 . "\ttagline = ##f\n"
84229 . "}\n"
@@ -86,42 +231,100 @@
87232 . "\traggedbottom = ##t\n"
88233 . "\tindent = 0\mm\n"
89234 . "}\n"
90 - . "\\version \"2.12.3\"\n"
 235+ . '\version "' . self::$lilypondVersion . "\"\n"
91236 . "\\score {\n"
92237 . $lilypondCode
93238 . "\t\\layout { }\n"
94239 . ( $renderMidi ? "\t\\midi { }\n" : "" )
95240 . "}\n";
96 - return self::runRaw( $raw, $renderMidi, $lilypondCode );
 241+ return $raw;
97242 }
98243
99244 /**
 245+ * Runs abc2ly, creating the LilyPond input file.
 246+ *
 247+ * $code ABC code.
 248+ * $factoryDirectory Working environment. The LilyPond input file is
 249+ * created as "file.ly" in this directory.
 250+ *
 251+ * @throws ScoreException if the conversion fails.
 252+ */
 253+ private function runAbc2Ly( $code, $factoryDirectory ) {
 254+ global $wgAbc2Ly;
 255+
 256+ $abcFile = $factoryDirectory . '/file.abc';
 257+ $lyFile = $factoryDirectory . '/file.ly';
 258+
 259+ /* Create ABC input file */
 260+ $rc = file_put_contents( $abcFile, ltrim( $code ) ); // abc2ly is picky about whitespace at the start of the file
 261+ if ( $rc === false ) {
 262+ throw new ScoreException( 'score-noabcinput' );
 263+ }
 264+
 265+ /* Convert to LilyPond file */
 266+ if ( !is_executable( $wgAbc2Ly ) ) {
 267+ throw new ScoreException( 'score-abc2lynotexecutable' );
 268+ }
 269+
 270+ $cmd = wfEscapeShellArg( $wgAbc2Ly )
 271+ . ' -s'
 272+ . ' --output=' . wfEscapeShellArg( $lyFile )
 273+ . ' ' . wfEscapeShellArg( $abcFile )
 274+ . ' 2>&1'; // FIXME: this last bit is not portable
 275+ $output = wfShellExec( $cmd, $rc );
 276+ if ( $rc != 0 ) {
 277+ throw new ScoreCallException( 'score-abcconversionerr', $output );
 278+ }
 279+ if ( !file_exists( $lyFile ) ) {
 280+ /* Occasionally, abc2ly will return exit code 0 but not create an output file */
 281+ throw new ScoreCallException( 'score-abcconversionerr', $output );
 282+ }
 283+
 284+ /* The output file has a tagline which should be removed in a wiki context */
 285+ $lyData = file_get_contents( $lyFile );
 286+ if ( $lyData === false ) {
 287+ throw new ScoreException( 'score-readerr' );
 288+ }
 289+ $lyData = preg_replace( '/^(\s*tagline\s*=).*/m', '$1 ##f', $lyData );
 290+ if ( $lyData === null ) {
 291+ throw new ScoreException( 'score-pregreplaceerr' );
 292+ }
 293+ $rc = file_put_contents( $lyFile, $lyData );
 294+ if ( $rc === false ) {
 295+ throw new ScoreException( 'score-noinput' );
 296+ }
 297+ }
 298+
 299+ /**
100300 * Runs lilypond.
101301 *
102 - * @param $lilypondCode
 302+ * @param $factoryDirectory Directory of the working environment.
 303+ * The LilyPond input file "file.ly" is expected to be in
 304+ * this directory.
103305 * @param $renderMidi
104306 * @param $altText Alternate text for the score image.
105307 * If set to false, the alt text will contain pagination instead.
106308 *
107309 * @return Image link HTML, and possibly anchor to MIDI file.
108310 */
109 - private static function runRaw( $lilypondCode, $renderMidi, $altText = false ) {
110 - global $wgTmpDirectory, $wgUploadDirectory, $wgUploadPath, $wgLilyPond, $wgScoreTrim;
 311+ private static function runLilypond( $factoryDirectory, $renderMidi, $altText = false ) {
 312+ global $wgUploadDirectory, $wgUploadPath, $wgLilyPond, $wgScoreTrim;
111313
112314 wfProfileIn( __METHOD__ );
113315
114316 /* Various paths and filenames */
115 - $factoryPrefix = 'MWLP.';
116 - $fuzz = md5( mt_rand() );
117 - $factoryDirectory = $wgTmpDirectory . "/$factoryPrefix$fuzz";
118 - $lilypondFile = $factoryDirectory . "/file.ly";
119 - $factoryMidi = $factoryDirectory . "/file.midi";
120 - $factoryImage = $factoryDirectory . "/file.png";
121 - $factoryImageTrimmed = $factoryDirectory . "/file-trimmed.png";
122 - $factoryMultiFormat = $factoryDirectory . "/file-%d.png"; // for multi-page scores
123 - $factoryMultiTrimmedFormat = $factoryDirectory . "/file-%d-trimmed.png";
124 - $lilypondDir = "lilypond";
125 - $rel = $lilypondDir . "/" . md5( $lilypondCode ); // FIXME: Too many files in one directory?
 317+ $lilypondFile = $factoryDirectory . '/file.ly';
 318+ $factoryMidi = $factoryDirectory . '/file.midi';
 319+ $factoryImage = $factoryDirectory . '/file.png';
 320+ $factoryImageTrimmed = $factoryDirectory . '/file-trimmed.png';
 321+ $factoryMultiFormat = $factoryDirectory . '/file-%d.png'; // for multi-page scores
 322+ $factoryMultiTrimmedFormat = $factoryDirectory . '/file-%d-trimmed.png';
 323+ $lilypondDir = 'lilypond';
 324+ $md5 = md5_file( $lilypondFile );
 325+ if ( $md5 === false ) {
 326+ throw new ScoreException( 'score-noinput' );
 327+ }
 328+ $rel = $lilypondDir . '/' . $md5; // FIXME: Too many files in one directory?
126329 $filePrefix = "$wgUploadDirectory/$rel";
127330 $pathPrefix = "$wgUploadPath/$rel";
128331 $midi = "$filePrefix.midi";
@@ -171,17 +374,6 @@
172375 }
173376 }
174377
175 - /* create working environment */
176 - $rc = wfMkdirParents( $factoryDirectory, 0700, __METHOD__ );
177 - if ( !$rc ) {
178 - throw new ScoreException( 'score-nofactory' );
179 - }
180 -
181 - $rc = file_put_contents( $lilypondFile, $lilypondCode );
182 - if ( $rc === false ) {
183 - throw new ScoreException( 'score-noinput' );
184 - }
185 -
186378 /* generate lilypond output files in working environment */
187379 $oldcwd = getcwd();
188380 if ( $oldcwd === false ) {
@@ -204,29 +396,16 @@
205397 throw new ScoreException( 'score-chdir' );
206398 }
207399 if ( $rc2 != 0 ) {
208 - self::eraseFactory( $factoryDirectory );
209 - wfProfileOut( __METHOD__ );
210 - $msg = wfMessage( 'score-compilererr' )
211 - ->inContentLanguage()
212 - ->rawParams(
213 - ' ' . Html::rawElement( 'pre', array(), strip_tags( $output ) ) . "\n"
214 - );
215 - return $msg;
 400+ throw new ScoreCallException( 'score-compilererr', $output );
216401 }
217402
218403 /* trim output images if wanted */
219404 if ( $wgScoreTrim ) {
220405 if ( file_exists( $factoryImage ) ) {
221406 $rc = self::trimImage( $factoryImage, $factoryImageTrimmed );
222 - if ( !$rc ) {
223 - throw new ScoreException( 'score-trimerr' );
224 - }
225407 }
226408 for ( $i = 1; file_exists( $f = sprintf( $factoryMultiFormat, $i ) ); ++$i ) {
227409 $rc = self::trimImage( $f, sprintf( $factoryMultiTrimmedFormat, $i ) );
228 - if ( !$rc ) {
229 - throw new ScoreException( 'score-trimerr' );
230 - }
231410 }
232411 } else {
233412 $factoryImageTrimmed = $factoryImage;
@@ -248,20 +427,10 @@
249428 throw new ScoreException( 'score-renameerr' );
250429 }
251430
252 - /* tear down working environment */
253 - if ( !self::eraseFactory( $factoryDirectory ) ) {
254 - self::debug( "Unable to delete temporary working directory.\n" );
255 - }
256431 } catch ( ScoreException $e ) {
257 - self::eraseFactory( $factoryDirectory );
258432 wfProfileOut( __METHOD__ );
259 - return Html::rawElement(
260 - 'span',
261 - array( 'class' => 'error' ),
262 - wfMessage( $e->getMessage() )->inContentLanguage()->parse()
263 - );
 433+ throw $e;
264434 }
265 - wfProfileOut( __METHOD__ );
266435 }
267436
268437 /* return output link(s) */
@@ -309,7 +478,7 @@
310479 * @param $source
311480 * @param $dest
312481 *
313 - * @return true on success, false on error.
 482+ * @throws ScoreException on error.
314483 */
315484 private static function trimImage( $source, $dest ) {
316485 global $wgImageMagickConvertCommand;
@@ -319,11 +488,9 @@
320489 . wfEscapeShellArg( $source ) . ' '
321490 . wfEscapeShellArg( $dest );
322491 wfShellExec( $cmd, $rc );
323 - if ( $rc == 0 ) {
324 - return true;
 492+ if ( $rc != 0 ) {
 493+ throw new ScoreException( 'score-trimerr' );
325494 }
326 -
327 - return false;
328495 }
329496
330497 /**

Comments

#Comment by Siebrand (talk | contribs)   14:28, 20 December 2011

Does this extension make everything that is supported in extensions abc and lilypond obsolete? If so, can we remove those extensions after branching 1.19 and add an OBSOLETE file to them now?

#Comment by MarkAHershberger (talk | contribs)   14:45, 20 December 2011

I would like to think that it does. But so far I've only tested the lilypond use.

This is certainly better maintained (currently) than the other two extensions. Hopefully we can get it deployed and then it will definitely replace/obsolete the abc+lilypond extensions.

#Comment by GrafZahl (talk | contribs)   19:40, 21 December 2011

Not "everything that is supported" in ABC/LilyPond is supported yet. For example, OggHandler integration is still missing. Just stand by :)

#Comment by GrafZahl (talk | contribs)   00:19, 24 December 2011

OggHandler integration has been added as of r107191.

Status & tagging log