ob-ruby.el (10599B)
1 ;;; ob-ruby.el --- Babel Functions for Ruby -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2009-2024 Free Software Foundation, Inc. 4 5 ;; Author: Eric Schulte 6 ;; Keywords: literate programming, reproducible research 7 ;; URL: https://orgmode.org 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 ;; Org-Babel support for evaluating ruby source code. 27 28 ;;; Requirements: 29 30 ;; - ruby and irb executables :: https://www.ruby-lang.org/ 31 ;; 32 ;; - ruby-mode :: Comes with Emacs. 33 ;; 34 ;; - inf-ruby mode :: Can be installed through ELPA, or from 35 ;; https://raw.githubusercontent.com/nonsequitur/inf-ruby/master/inf-ruby.el 36 37 ;;; Code: 38 39 (require 'org-macs) 40 (org-assert-version) 41 42 (require 'ob) 43 (require 'org-macs) 44 45 (declare-function run-ruby-or-pop-to-buffer "ext:inf-ruby" (command &optional name buffer)) 46 (declare-function inf-ruby-buffer "ext:inf-ruby" ()) 47 (declare-function xmp "ext:rcodetools" (&optional option)) 48 49 (defvar inf-ruby-default-implementation) 50 (defvar inf-ruby-implementations) 51 52 (defvar org-babel-tangle-lang-exts) 53 (add-to-list 'org-babel-tangle-lang-exts '("ruby" . "rb")) 54 55 (defvar org-babel-default-header-args:ruby '()) 56 57 (defvar org-babel-ruby-command "ruby" 58 "Name of command to use for executing ruby code. 59 It's possible to override it by using a header argument `:ruby'") 60 61 (defcustom org-babel-ruby-hline-to "nil" 62 "Replace hlines in incoming tables with this when translating to ruby." 63 :group 'org-babel 64 :version "24.4" 65 :package-version '(Org . "8.0") 66 :type 'string) 67 68 (defcustom org-babel-ruby-nil-to 'hline 69 "Replace nil in ruby tables with this before returning." 70 :group 'org-babel 71 :version "24.4" 72 :package-version '(Org . "8.0") 73 :type 'symbol) 74 75 (defun org-babel-execute:ruby (body params) 76 "Execute Ruby BODY according to PARAMS. 77 This function is called by `org-babel-execute-src-block'." 78 (let* ((session (org-babel-ruby-initiate-session 79 (cdr (assq :session params)) params)) 80 (result-params (cdr (assq :result-params params))) 81 (result-type (cdr (assq :result-type params))) 82 (org-babel-ruby-command 83 (or (cdr (assq :ruby params)) 84 org-babel-ruby-command)) 85 (full-body (org-babel-expand-body:generic 86 body params (org-babel-variable-assignments:ruby params))) 87 (result (if (member "xmp" result-params) 88 (with-temp-buffer 89 (org-require-package 'rcodetools "rcodetools (gem package)") 90 (insert full-body) 91 (xmp (cdr (assq :xmp-option params))) 92 (buffer-string)) 93 (org-babel-ruby-evaluate 94 session full-body result-type result-params)))) 95 (org-babel-reassemble-table 96 (org-babel-result-cond result-params 97 result 98 (org-babel-ruby-table-or-string result)) 99 (org-babel-pick-name (cdr (assq :colname-names params)) 100 (cdr (assq :colnames params))) 101 (org-babel-pick-name (cdr (assq :rowname-names params)) 102 (cdr (assq :rownames params)))))) 103 104 (defun org-babel-prep-session:ruby (session params) 105 "Prepare SESSION according to the header arguments specified in PARAMS." 106 ;; (message "params=%S" params) ;; debugging 107 (let* ((session (org-babel-ruby-initiate-session session)) 108 (var-lines (org-babel-variable-assignments:ruby params))) 109 (org-babel-comint-in-buffer session 110 (sit-for .5) (goto-char (point-max)) 111 (mapc (lambda (var) 112 (insert var) (comint-send-input nil t) 113 (org-babel-comint-wait-for-output session) 114 (sit-for .1) (goto-char (point-max))) 115 var-lines)) 116 session)) 117 118 (defun org-babel-load-session:ruby (session body params) 119 "Load BODY into SESSION." 120 (save-window-excursion 121 (let ((buffer (org-babel-prep-session:ruby session params))) 122 (with-current-buffer buffer 123 (goto-char (process-mark (get-buffer-process (current-buffer)))) 124 (insert (org-babel-chomp body))) 125 buffer))) 126 127 ;; helper functions 128 129 (defun org-babel-variable-assignments:ruby (params) 130 "Return list of ruby statements assigning the block's variables. 131 The assignments are defined in PARAMS." 132 (mapcar 133 (lambda (pair) 134 (format "%s=%s" 135 (car pair) 136 (org-babel-ruby-var-to-ruby (cdr pair)))) 137 (org-babel--get-vars params))) 138 139 (defun org-babel-ruby-var-to-ruby (var) 140 "Convert VAR into a ruby variable. 141 Convert an elisp value into a string of ruby source code 142 specifying a variable of the same value." 143 (if (listp var) 144 (concat "[" (mapconcat #'org-babel-ruby-var-to-ruby var ", \n") "]") 145 (if (eq var 'hline) 146 org-babel-ruby-hline-to 147 (format "%S" var)))) 148 149 (defun org-babel-ruby-table-or-string (results) 150 "Convert RESULTS into an appropriate elisp value. 151 If RESULTS look like a table, then convert them into an 152 Emacs-lisp table, otherwise return the results as a string." 153 (let ((res (org-babel-script-escape results))) 154 (if (listp res) 155 (mapcar (lambda (el) (if (not el) 156 org-babel-ruby-nil-to el)) 157 res) 158 res))) 159 160 (defvar org-babel-ruby-prompt "_org_babel_ruby_prompt " 161 "String used for unique prompt.") 162 163 (defvar org-babel-ruby-define-prompt 164 (format "IRB.conf[:PROMPT][:CUSTOM] = { :PROMPT_I => \"%s\" }" org-babel-ruby-prompt)) 165 166 (defun org-babel-ruby-initiate-session (&optional session params) 167 "Initiate a ruby session. 168 If there is not a current inferior-process-buffer in SESSION 169 then create one. Return the initialized session. 170 Session settings (`:ruby' header arg value) are taken from PARAMS." 171 (unless (string= session "none") 172 (org-require-package 'inf-ruby) 173 (let* ((command (cdr (or (assq :ruby params) 174 (assoc inf-ruby-default-implementation 175 inf-ruby-implementations)))) 176 (buffer (get-buffer (format "*%s*" session))) 177 (new-session? (not buffer)) 178 (session-buffer (or buffer (save-window-excursion 179 (run-ruby-or-pop-to-buffer 180 (if (functionp command) 181 (funcall command) 182 command) 183 (or session "ruby") 184 (unless session 185 (inf-ruby-buffer))) 186 (current-buffer))))) 187 (if (org-babel-comint-buffer-livep session-buffer) 188 (progn 189 (sit-for .25) 190 ;; Setup machine-readable prompt: no echo, prompts matching 191 ;; uniquely by regexp. 192 (when new-session? 193 (with-current-buffer session-buffer 194 (setq-local 195 org-babel-comint-prompt-regexp-old comint-prompt-regexp 196 comint-prompt-regexp (concat "^" org-babel-ruby-prompt)) 197 (insert org-babel-ruby-define-prompt ";") 198 (insert "_org_prompt_mode=conf.prompt_mode;conf.prompt_mode=:CUSTOM;") 199 (insert "conf.echo=false\n") 200 (comint-send-input nil t))) 201 session-buffer) 202 (sit-for .5) 203 (org-babel-ruby-initiate-session session))))) 204 205 (defvar org-babel-ruby-eoe-indicator ":org_babel_ruby_eoe" 206 "String to indicate that evaluation has completed.") 207 208 (defvar org-babel-ruby-f-write 209 "File.open('%s','w'){|f| f.write((_.class == String) ? _ : _.inspect)}") 210 211 (defvar org-babel-ruby-pp-f-write 212 "File.open('%s','w'){|f| $stdout = f; pp(results); $stdout = orig_out}") 213 214 (defvar org-babel-ruby-wrapper-method 215 " 216 def main() 217 %s 218 end 219 results = main() 220 File.open('%s', 'w'){ |f| f.write((results.class == String) ? results : results.inspect) } 221 ") 222 223 (defvar org-babel-ruby-pp-wrapper-method 224 " 225 require 'pp' 226 def main() 227 %s 228 end 229 results = main() 230 File.open('%s', 'w') do |f| 231 $stdout = f 232 pp results 233 end 234 ") 235 236 (defun org-babel-ruby-evaluate 237 (buffer body &optional result-type result-params) 238 "Pass BODY to the Ruby process in BUFFER. 239 If RESULT-TYPE equals `output' then return a list of the outputs 240 of the statements in BODY, if RESULT-TYPE equals `value' then 241 return the value of the last statement in BODY, as elisp." 242 (if (not buffer) 243 ;; external process evaluation 244 (pcase result-type 245 (`output (org-babel-eval org-babel-ruby-command body)) 246 (`value (let ((tmp-file (org-babel-temp-file "ruby-"))) 247 (org-babel-eval 248 org-babel-ruby-command 249 (format (if (member "pp" result-params) 250 org-babel-ruby-pp-wrapper-method 251 org-babel-ruby-wrapper-method) 252 body (org-babel-process-file-name tmp-file 'noquote))) 253 (org-babel-eval-read-file tmp-file)))) 254 ;; comint session evaluation 255 (pcase result-type 256 (`output 257 (let ((eoe-string (format "puts \"%s\"" org-babel-ruby-eoe-indicator))) 258 ;; Force the session to be ready before the actual session 259 ;; code is run. There is some problem in comint that will 260 ;; sometimes show the prompt after the input has already 261 ;; been inserted and that throws off the extraction of the 262 ;; result for Babel. 263 (org-babel-comint-with-output 264 (buffer org-babel-ruby-eoe-indicator t eoe-string) 265 (insert eoe-string) (comint-send-input nil t)) 266 (mapconcat 267 #'identity 268 (butlast 269 (split-string 270 (mapconcat 271 #'org-trim 272 (org-babel-comint-with-output 273 (buffer org-babel-ruby-eoe-indicator t body) 274 (insert (org-babel-chomp body) "\n" eoe-string) 275 (comint-send-input nil t)) 276 "\n") "[\r\n]")) "\n"))) 277 (`value 278 (let* ((tmp-file (org-babel-temp-file "ruby-")) 279 (ppp (or (member "code" result-params) 280 (member "pp" result-params)))) 281 (org-babel-comint-with-output 282 (buffer org-babel-ruby-eoe-indicator t body) 283 (when ppp (insert "require 'pp';") (comint-send-input nil t)) 284 (mapc 285 (lambda (line) 286 (insert (org-babel-chomp line)) (comint-send-input nil t)) 287 (append 288 (list body) 289 (if (not ppp) 290 (list (format org-babel-ruby-f-write 291 (org-babel-process-file-name tmp-file 'noquote))) 292 (list 293 "results=_" "require 'pp'" "orig_out = $stdout" 294 (format org-babel-ruby-pp-f-write 295 (org-babel-process-file-name tmp-file 'noquote)))) 296 (list (format "puts \"%s\"" org-babel-ruby-eoe-indicator)))) 297 (comint-send-input nil t)) 298 (org-babel-eval-read-file tmp-file)))))) 299 300 (provide 'ob-ruby) 301 302 ;;; ob-ruby.el ends here