ob-lilypond.el (16984B)
1 ;;; ob-lilypond.el --- Babel Functions for Lilypond -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2010-2023 Free Software Foundation, Inc. 4 5 ;; Author: Martyn Jago 6 ;; Keywords: babel language, literate programming 7 ;; URL: https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-lilypond.html 8 9 ;; This file is part of GNU Emacs. 10 11 ;; GNU Emacs is free software: you can redistribute it and/or modify 12 ;; it under the terms of the GNU General Public License as published by 13 ;; the Free Software Foundation, either version 3 of the License, or 14 ;; (at your option) any later version. 15 16 ;; GNU Emacs is distributed in the hope that it will be useful, 17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 ;; GNU General Public License for more details. 20 21 ;; You should have received a copy of the GNU General Public License 22 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. 23 24 ;;; Commentary: 25 26 ;; Installation, ob-lilypond documentation, and examples are available at 27 ;; https://orgmode.org/worg/org-contrib/babel/languages/ob-doc-lilypond.html 28 ;; 29 ;; Lilypond documentation can be found at 30 ;; https://lilypond.org/manuals.html 31 ;; 32 ;; This depends on epstopdf --- See https://www.ctan.org/pkg/epstopdf. 33 34 ;;; Code: 35 36 (require 'org-macs) 37 (org-assert-version) 38 39 (require 'ob) 40 41 (declare-function org-fold-show-all "org-fold" (&optional types)) 42 43 ;; FIXME: Doesn't this rather belong in lilypond-mode.el? 44 (defalias 'lilypond-mode 'LilyPond-mode) 45 46 (add-to-list 'org-babel-tangle-lang-exts '("LilyPond" . "ly")) 47 48 (defvar org-babel-default-header-args:lilypond '() 49 "Default header arguments for lilypond code blocks. 50 NOTE: The arguments are determined at lilypond compile time. 51 See `org-babel-lilypond-set-header-args' 52 To configure, see `ob-lilypond-header-args' 53 .") 54 55 (defvar ob-lilypond-header-args 56 '((:results . "file") (:exports . "results")) 57 "User-configurable header arguments for lilypond code blocks. 58 NOTE: The final value used by org-babel is computed at compile-time 59 and stored in `org-babel-default-header-args:lilypond' 60 See `org-babel-lilypond-set-header-args'.") 61 62 (defvar org-babel-lilypond-compile-post-tangle t 63 "Following the org-babel-tangle (C-c C-v t) command, 64 org-babel-lilypond-compile-post-tangle determines whether ob-lilypond should 65 automatically attempt to compile the resultant tangled file. 66 If the value is nil, no automated compilation takes place. 67 Default value is t.") 68 69 (defvar org-babel-lilypond-display-pdf-post-tangle t 70 "Following a successful LilyPond compilation 71 org-babel-lilypond-display-pdf-post-tangle determines whether to automate the 72 drawing / redrawing of the resultant pdf. If the value is nil, 73 the pdf is not automatically redrawn. Default value is t.") 74 75 (defvar org-babel-lilypond-play-midi-post-tangle t 76 "Following a successful LilyPond compilation 77 org-babel-lilypond-play-midi-post-tangle determines whether to automate the 78 playing of the resultant midi file. If the value is nil, 79 the midi file is not automatically played. Default value is t") 80 81 (defvar org-babel-lilypond-ly-command "" 82 "Command to execute lilypond on your system. 83 Do not set it directly. Customize `org-babel-lilypond-commands' instead.") 84 85 (defvar org-babel-lilypond-pdf-command "" 86 "Command to show a PDF file on your system. 87 Do not set it directly. Customize `org-babel-lilypond-commands' instead.") 88 89 (defvar org-babel-lilypond-midi-command "" 90 "Command to play a MIDI file on your system. 91 Do not set it directly. Customize `org-babel-lilypond-commands' instead.") 92 93 (defcustom org-babel-lilypond-commands 94 (cond 95 ((eq system-type 'darwin) 96 '("/Applications/lilypond.app/Contents/Resources/bin/lilypond" "open" "open")) 97 ((eq system-type 'windows-nt) 98 '("lilypond" "" "")) 99 (t 100 '("lilypond" "xdg-open" "xdg-open"))) 101 "Commands to run lilypond and view or play the results. 102 These should be executables that take a filename as an argument. 103 On some system it is possible to specify the filename directly 104 and the viewer or player will be determined from the file type; 105 you can leave the string empty on this case." 106 :group 'org-babel 107 :type '(list 108 (string :tag "Lilypond ") 109 (string :tag "PDF Viewer ") 110 (string :tag "MIDI Player")) 111 :version "24.4" 112 :package-version '(Org . "8.2.7") 113 :set 114 (lambda (symbol value) 115 (set-default-toplevel-value symbol value) 116 (setq 117 org-babel-lilypond-ly-command (nth 0 value) 118 org-babel-lilypond-pdf-command (nth 1 value) 119 org-babel-lilypond-midi-command (nth 2 value)))) 120 121 (defvar org-babel-lilypond-gen-png nil 122 "Non-nil means image generation (PNG) is turned on by default.") 123 124 (defvar org-babel-lilypond-gen-svg nil 125 "Non-nil means image generation (SVG) is be turned on by default.") 126 127 (defvar org-babel-lilypond-gen-html nil 128 "Non-nil means HTML generation is turned on by default.") 129 130 (defvar org-babel-lilypond-gen-pdf nil 131 "Non-nil means PDF generation is be turned on by default.") 132 133 (defvar org-babel-lilypond-use-eps nil 134 "Non-nil forces the compiler to use the EPS backend.") 135 136 (defvar org-babel-lilypond-arrange-mode nil 137 "Non-nil turns Arrange mode on. 138 In Arrange mode the following settings are altered from default: 139 :tangle yes, :noweb yes 140 :results silent :comments yes. 141 In addition lilypond block execution causes tangling of all lilypond 142 blocks.") 143 144 (defun org-babel-expand-body:lilypond (body params) 145 "Expand BODY according to PARAMS, return the expanded body." 146 (let ((vars (org-babel--get-vars params))) 147 (mapc 148 (lambda (pair) 149 (let ((name (symbol-name (car pair))) 150 (value (cdr pair))) 151 (setq body 152 (replace-regexp-in-string 153 (concat "$" (regexp-quote name)) 154 (if (stringp value) value (format "%S" value)) 155 body)))) 156 vars) 157 body)) 158 159 (defun org-babel-execute:lilypond (body params) 160 "This function is called by `org-babel-execute-src-block'. 161 Depending on whether we are in arrange mode either: 162 1. Attempt to execute lilypond block according to header settings 163 (This is the default basic mode) 164 2. Tangle all lilypond blocks and process the result (arrange mode)" 165 (org-babel-lilypond-set-header-args org-babel-lilypond-arrange-mode) 166 (if org-babel-lilypond-arrange-mode 167 (org-babel-lilypond-tangle) 168 (org-babel-lilypond-process-basic body params))) 169 170 (defun org-babel-lilypond-tangle () 171 "ob-lilypond specific tangle, attempts to invoke 172 =ly-execute-tangled-ly= if tangle is successful. Also passes 173 specific arguments to =org-babel-tangle=." 174 (interactive) 175 (if (org-babel-tangle nil "yes" "lilypond") 176 (org-babel-lilypond-execute-tangled-ly) nil)) 177 178 (defun org-babel-lilypond-process-basic (body params) 179 "Execute a lilypond block in basic mode." 180 (let* ((out-file (cdr (assq :file params))) 181 (cmdline (or (cdr (assq :cmdline params)) 182 "")) 183 (in-file (org-babel-temp-file "lilypond-"))) 184 185 (with-temp-file in-file 186 (insert (org-babel-expand-body:generic body params))) 187 (org-babel-eval 188 (concat 189 org-babel-lilypond-ly-command 190 " -dbackend=eps " 191 "-dno-gs-load-fonts " 192 "-dinclude-eps-fonts " 193 (or (cdr (assoc (file-name-extension out-file) 194 '(("pdf" . "--pdf ") 195 ("ps" . "--ps ") 196 ("png" . "--png ")))) 197 "--png ") 198 "--output=" 199 (file-name-sans-extension out-file) 200 " " 201 cmdline 202 in-file) "")) nil) 203 204 (defun org-babel-prep-session:lilypond (_session _params) 205 "Return an error because LilyPond exporter does not support sessions." 206 (error "Sorry, LilyPond does not currently support sessions!")) 207 208 (defun org-babel-lilypond-execute-tangled-ly () 209 "Compile result of block tangle with lilypond. 210 If error in compilation, attempt to mark the error in lilypond org file." 211 (when org-babel-lilypond-compile-post-tangle 212 (let ((org-babel-lilypond-tangled-file (org-babel-lilypond-switch-extension 213 (buffer-file-name) ".lilypond")) 214 (org-babel-lilypond-temp-file (org-babel-lilypond-switch-extension 215 (buffer-file-name) ".ly"))) 216 (if (not (file-exists-p org-babel-lilypond-tangled-file)) 217 (error "Error: Tangle Failed!") 218 (when (file-exists-p org-babel-lilypond-temp-file) 219 (delete-file org-babel-lilypond-temp-file)) 220 (rename-file org-babel-lilypond-tangled-file 221 org-babel-lilypond-temp-file)) 222 (org-switch-to-buffer-other-window "*lilypond*") 223 (erase-buffer) 224 (org-babel-lilypond-compile-lilyfile org-babel-lilypond-temp-file) 225 (goto-char (point-min)) 226 (if (org-babel-lilypond-check-for-compile-error org-babel-lilypond-temp-file) 227 (error "Error in Compilation!") 228 (other-window -1) 229 (org-babel-lilypond-attempt-to-open-pdf org-babel-lilypond-temp-file) 230 (org-babel-lilypond-attempt-to-play-midi org-babel-lilypond-temp-file))))) 231 232 (defun org-babel-lilypond-compile-lilyfile (file-name &optional test) 233 "Compile lilypond file and check for compile errors. 234 FILE-NAME is full path to lilypond (.ly) file." 235 (message "Compiling LilyPond...") 236 (let ((arg-1 org-babel-lilypond-ly-command) ;program 237 ;; (arg-2 nil) ;infile 238 (arg-3 "*lilypond*") ;buffer 239 (arg-4 t) ;display 240 (arg-5 (if org-babel-lilypond-gen-png "--png" "")) ;&rest... 241 (arg-6 (if org-babel-lilypond-gen-html "--html" "")) 242 (arg-7 (if org-babel-lilypond-gen-pdf "--pdf" "")) 243 (arg-8 (if org-babel-lilypond-use-eps "-dbackend=eps" "")) 244 (arg-9 (if org-babel-lilypond-gen-svg "-dbackend=svg" "")) 245 (arg-10 (concat "--output=" (file-name-sans-extension file-name))) 246 (arg-11 file-name)) 247 (if test 248 `(,arg-1 ,nil ,arg-3 ,arg-4 ,arg-5 ,arg-6 ;; arg-2 249 ,arg-7 ,arg-8 ,arg-9 ,arg-10 ,arg-11) 250 (call-process 251 arg-1 nil arg-3 arg-4 arg-5 arg-6 ;; arg-2 252 arg-7 arg-8 arg-9 arg-10 arg-11)))) 253 254 (defun org-babel-lilypond-check-for-compile-error (file-name &optional test) 255 "Check for compile error. 256 This is performed by parsing the *lilypond* buffer 257 containing the output message from the compilation. 258 FILE-NAME is full path to lilypond file. 259 If TEST is t just return nil if no error found, and pass 260 nil as file-name since it is unused in this context." 261 (let ((is-error (search-forward "error:" nil t))) 262 (if test 263 is-error 264 (when is-error 265 (org-babel-lilypond-process-compile-error file-name))))) 266 267 (defun org-babel-lilypond-process-compile-error (file-name) 268 "Process the compilation error that has occurred. 269 FILE-NAME is full path to lilypond file." 270 (let ((line-num (org-babel-lilypond-parse-line-num))) 271 (let ((error-lines (org-babel-lilypond-parse-error-line file-name line-num))) 272 (org-babel-lilypond-mark-error-line file-name error-lines) 273 (error "Error: Compilation Failed!")))) 274 275 (defun org-babel-lilypond-mark-error-line (file-name line) 276 "Mark the erroneous lines in the lilypond org buffer. 277 FILE-NAME is full path to lilypond file. 278 LINE is the erroneous line." 279 (org-switch-to-buffer-other-window 280 (concat (file-name-nondirectory 281 (org-babel-lilypond-switch-extension file-name ".org")))) 282 (let ((temp (point))) 283 (goto-char (point-min)) 284 (setq case-fold-search nil) 285 (if (search-forward line nil t) 286 (progn 287 (org-fold-show-all) 288 (set-mark (point)) 289 (goto-char (- (point) (length line)))) 290 (goto-char temp)))) 291 292 (defun org-babel-lilypond-parse-line-num (&optional buffer) 293 "Extract error line number." 294 (when buffer (set-buffer buffer)) 295 (let ((start 296 (and (search-backward ":" nil t) 297 (search-backward ":" nil t) 298 (search-backward ":" nil t) 299 (search-backward ":" nil t)))) 300 (when start 301 (forward-char) 302 (let ((num (string-to-number 303 (buffer-substring 304 (+ 1 start) 305 (- (search-forward ":" nil t) 1))))) 306 (and (numberp num) num))))) 307 308 (defun org-babel-lilypond-parse-error-line (file-name lineNo) 309 "Extract the erroneous line from the tangled .ly file. 310 FILE-NAME is full path to lilypond file. 311 LINENO is the number of the erroneous line." 312 (with-temp-buffer 313 (insert-file-contents (org-babel-lilypond-switch-extension file-name ".ly") 314 nil nil nil t) 315 (if (> lineNo 0) 316 (progn 317 (goto-char (point-min)) 318 (forward-line (- lineNo 1)) 319 (buffer-substring (point) (line-end-position))) 320 nil))) 321 322 (defun org-babel-lilypond-attempt-to-open-pdf (file-name &optional test) 323 "Attempt to display the generated pdf file. 324 FILE-NAME is full path to lilypond file. 325 If TEST is non-nil, the shell command is returned and is not run." 326 (when org-babel-lilypond-display-pdf-post-tangle 327 (let ((pdf-file (org-babel-lilypond-switch-extension file-name ".pdf"))) 328 (if (file-exists-p pdf-file) 329 (let ((cmd-string 330 (concat org-babel-lilypond-pdf-command " " pdf-file))) 331 (if test 332 cmd-string 333 (start-process 334 "\"Audition pdf\"" 335 "*lilypond*" 336 org-babel-lilypond-pdf-command 337 pdf-file))) 338 (message "No pdf file generated so can't display!"))))) 339 340 (defun org-babel-lilypond-attempt-to-play-midi (file-name &optional test) 341 "Attempt to play the generated MIDI file. 342 FILE-NAME is full path to lilypond file. 343 If TEST is non-nil, the shell command is returned and is not run." 344 (when org-babel-lilypond-play-midi-post-tangle 345 (let* ((ext (if (eq system-type 'windows-nt) 346 ".mid" ".midi")) 347 (midi-file (org-babel-lilypond-switch-extension file-name ext))) 348 (if (file-exists-p midi-file) 349 (let ((cmd-string 350 (concat org-babel-lilypond-midi-command " " midi-file))) 351 (if test 352 cmd-string 353 (start-process 354 "\"Audition midi\"" 355 "*lilypond*" 356 org-babel-lilypond-midi-command 357 midi-file))) 358 (message "No midi file generated so can't play!"))))) 359 360 (defun org-babel-lilypond-toggle-midi-play () 361 "Toggle whether midi will be played following a successful compilation." 362 (interactive) 363 (setq org-babel-lilypond-play-midi-post-tangle 364 (not org-babel-lilypond-play-midi-post-tangle)) 365 (message (concat "Post-Tangle MIDI play has been " 366 (if org-babel-lilypond-play-midi-post-tangle 367 "ENABLED." "DISABLED.")))) 368 369 (defun org-babel-lilypond-toggle-pdf-display () 370 "Toggle whether pdf will be displayed following a successful compilation." 371 (interactive) 372 (setq org-babel-lilypond-display-pdf-post-tangle 373 (not org-babel-lilypond-display-pdf-post-tangle)) 374 (message (concat "Post-Tangle PDF display has been " 375 (if org-babel-lilypond-display-pdf-post-tangle 376 "ENABLED." "DISABLED.")))) 377 378 (defun org-babel-lilypond-toggle-png-generation () 379 "Toggle whether png image will be generated by compilation." 380 (interactive) 381 (setq org-babel-lilypond-gen-png (not org-babel-lilypond-gen-png)) 382 (message (concat "PNG image generation has been " 383 (if org-babel-lilypond-gen-png "ENABLED." "DISABLED.")))) 384 385 (defun org-babel-lilypond-toggle-html-generation () 386 "Toggle whether html will be generated by compilation." 387 (interactive) 388 (setq org-babel-lilypond-gen-html (not org-babel-lilypond-gen-html)) 389 (message (concat "HTML generation has been " 390 (if org-babel-lilypond-gen-html "ENABLED." "DISABLED.")))) 391 392 (defun org-babel-lilypond-toggle-pdf-generation () 393 "Toggle whether pdf will be generated by compilation." 394 (interactive) 395 (setq org-babel-lilypond-gen-pdf (not org-babel-lilypond-gen-pdf)) 396 (message (concat "PDF generation has been " 397 (if org-babel-lilypond-gen-pdf "ENABLED." "DISABLED.")))) 398 399 (defun org-babel-lilypond-toggle-arrange-mode () 400 "Toggle whether in Arrange mode or Basic mode." 401 (interactive) 402 (setq org-babel-lilypond-arrange-mode 403 (not org-babel-lilypond-arrange-mode)) 404 (message (concat "Arrange mode has been " 405 (if org-babel-lilypond-arrange-mode "ENABLED." "DISABLED.")))) 406 407 (defun org-babel-lilypond-switch-extension (file-name ext) 408 "Utility command to swap current FILE-NAME extension with EXT." 409 (concat (file-name-sans-extension 410 file-name) 411 ext)) 412 413 (defun org-babel-lilypond-get-header-args (mode) 414 "Default arguments to use when evaluating a lilypond source block. 415 These depend upon whether we are in Arrange mode i.e. MODE is t." 416 (cond (mode 417 '((:tangle . "yes") 418 (:noweb . "yes") 419 (:results . "silent") 420 (:cache . "yes") 421 (:comments . "yes"))) 422 (t 423 ob-lilypond-header-args))) 424 425 (defun org-babel-lilypond-set-header-args (mode) 426 "Set org-babel-default-header-args:lilypond 427 dependent on ORG-BABEL-LILYPOND-ARRANGE-MODE." 428 (setq org-babel-default-header-args:lilypond 429 (org-babel-lilypond-get-header-args mode))) 430 431 (provide 'ob-lilypond) 432 433 ;;; ob-lilypond.el ends here