;;; ob-elixir.el --- Org Babel functions for Elixir -*- lexical-binding: t; -*- ;; Copyright (C) 2024 Your Name ;; Author: Your Name ;; URL: https://github.com/username/ob-elixir ;; Keywords: literate programming, reproducible research, elixir ;; Version: 0.1.0 ;; Package-Requires: ((emacs "27.1") (org "9.4")) ;; This file is not part of GNU Emacs. ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;;; Commentary: ;; Org Babel support for evaluating Elixir code blocks. ;; ;; Features: ;; - Execute Elixir code in org-mode source blocks ;; - Support for :results value and :results output ;; - Variable passing with :var header argument ;; - Mix project context support ;; ;; Usage: ;; Add (elixir . t) to `org-babel-load-languages': ;; ;; (org-babel-do-load-languages ;; 'org-babel-load-languages ;; '((elixir . t))) ;;; Code: (require 'ob) (require 'ob-eval) (require 'cl-lib) ;;; Customization (defgroup ob-elixir nil "Org Babel support for Elixir." :group 'org-babel :prefix "ob-elixir-") (defcustom ob-elixir-command "elixir" "Command to execute Elixir code. Can be a full path or command name if in PATH." :type 'string :group 'ob-elixir :safe #'stringp) (defcustom ob-elixir-signal-errors t "Whether to signal Emacs errors on Elixir execution failure. When non-nil, Elixir errors will be signaled as Emacs errors. When nil, errors are returned as the result string." :type 'boolean :group 'ob-elixir) (defcustom ob-elixir-show-warnings t "Whether to include warnings in output. When non-nil, Elixir warnings are included in the result. When nil, warnings are stripped from the output." :type 'boolean :group 'ob-elixir) ;;; Header Arguments (defvar org-babel-default-header-args:elixir '((:results . "value") (:session . "none")) "Default header arguments for Elixir code blocks.") ;;; Language Registration ;; File extension for tangling (add-to-list 'org-babel-tangle-lang-exts '("elixir" . "ex")) ;; Associate with elixir-mode for syntax highlighting (if available) (with-eval-after-load 'org-src (add-to-list 'org-src-lang-modes '("elixir" . elixir))) ;;; Error Handling (defconst ob-elixir--error-regexp "^\\*\\* (\\([A-Za-z.]+Error\\))\\(.*\\)" "Regexp matching Elixir runtime errors. Group 1 is the error type, group 2 is the message.") (defconst ob-elixir--compile-error-regexp "^\\*\\* (\\(CompileError\\|TokenMissingError\\|SyntaxError\\))\\(.*\\)" "Regexp matching Elixir compile-time errors.") (defconst ob-elixir--warning-regexp "^warning: \\(.*\\)" "Regexp matching Elixir warnings.") (define-error 'ob-elixir-error "Elixir evaluation error") (define-error 'ob-elixir-compile-error "Elixir compilation error" 'ob-elixir-error) (define-error 'ob-elixir-runtime-error "Elixir runtime error" 'ob-elixir-error) (defun ob-elixir--detect-error (output) "Check OUTPUT for Elixir errors. Returns a plist with :type, :message, and :line if an error is found. Returns nil if no error detected." (cond ;; Compile-time error ((string-match ob-elixir--compile-error-regexp output) (list :type 'compile :error-type (match-string 1 output) :message (string-trim (match-string 2 output)) :full-output output)) ;; Runtime error ((string-match ob-elixir--error-regexp output) (list :type 'runtime :error-type (match-string 1 output) :message (string-trim (match-string 2 output)) :full-output output)) ;; No error (t nil))) (defun ob-elixir--format-error (error-info) "Format ERROR-INFO into a user-friendly message." (let ((type (plist-get error-info :type)) (error-type (plist-get error-info :error-type)) (message (plist-get error-info :message))) (format "Elixir %s: (%s) %s" (if (eq type 'compile) "Compile Error" "Runtime Error") error-type message))) (defun ob-elixir--strip-warnings (output) "Remove warning lines from OUTPUT if configured." (if ob-elixir-show-warnings output (let ((lines (split-string output "\n"))) (mapconcat #'identity (cl-remove-if (lambda (line) (string-match-p ob-elixir--warning-regexp line)) lines) "\n")))) (defun ob-elixir--process-result (result) "Process RESULT from Elixir execution. Checks for errors and handles them according to `ob-elixir-signal-errors'. Returns the cleaned result string." (let ((trimmed (string-trim result)) (error-info (ob-elixir--detect-error result))) (if error-info (if ob-elixir-signal-errors (signal (if (eq (plist-get error-info :type) 'compile) 'ob-elixir-compile-error 'ob-elixir-runtime-error) (list (ob-elixir--format-error error-info))) ;; Return error as result (plist-get error-info :full-output)) ;; No error, return trimmed result trimmed))) ;;; Type Conversion (defun ob-elixir--escape-string (str) "Escape special characters in STR for Elixir string literal." (let ((result str)) ;; Escape backslashes first (setq result (replace-regexp-in-string "\\\\" "\\\\\\\\" result)) ;; Escape double quotes (setq result (replace-regexp-in-string "\"" "\\\\\"" result)) ;; Escape newlines (setq result (replace-regexp-in-string "\n" "\\\\n" result)) ;; Escape tabs (setq result (replace-regexp-in-string "\t" "\\\\t" result)) result)) (defun ob-elixir--elisp-to-elixir (value) "Convert Elisp VALUE to Elixir literal syntax. Handles: - nil -> nil - t -> true - numbers -> numbers - strings -> quoted strings - symbols -> atoms - lists -> Elixir lists - vectors -> tuples" (cond ;; nil ((null value) "nil") ;; Boolean true ((eq value t) "true") ;; Numbers ((numberp value) (number-to-string value)) ;; Strings ((stringp value) (format "\"%s\"" (ob-elixir--escape-string value))) ;; Symbols become atoms (except special ones) ((symbolp value) (let ((name (symbol-name value))) (cond ((string= name "hline") ":hline") ((string-match-p "^[a-z_][a-zA-Z0-9_]*[?!]?$" name) (format ":%s" name)) (t (format ":\"%s\"" name))))) ;; Vectors become tuples ((vectorp value) (format "{%s}" (mapconcat #'ob-elixir--elisp-to-elixir (append value nil) ", "))) ;; Lists ((listp value) (format "[%s]" (mapconcat #'ob-elixir--elisp-to-elixir value ", "))) ;; Fallback (t (format "%S" value)))) ;;; Variable Handling (defun ob-elixir--var-name (name) "Convert NAME to a valid Elixir variable name. Elixir variables must start with lowercase or underscore." (let ((str (if (symbolp name) (symbol-name name) name))) ;; Ensure starts with lowercase or underscore (if (string-match-p "^[a-z_]" str) str (concat "_" str)))) (defun org-babel-variable-assignments:elixir (params) "Return list of Elixir statements assigning variables from PARAMS. Each statement has the form: var_name = value" (mapcar (lambda (pair) (let ((name (car pair)) (value (cdr pair))) (format "%s = %s" (ob-elixir--var-name name) (ob-elixir--elisp-to-elixir value)))) (org-babel--get-vars params))) ;;; Execution (defconst ob-elixir--value-wrapper "result = ( %s ) IO.puts(inspect(result, limit: :infinity, printable_limit: :infinity, charlists: :as_lists)) " "Wrapper template for capturing Elixir expression value. %s is replaced with the user's code.") (defun ob-elixir--wrap-for-value (body) "Wrap BODY to capture its return value. The wrapper evaluates BODY, then prints the result using `inspect/2` with infinite limits to avoid truncation." (format ob-elixir--value-wrapper body)) (defun ob-elixir--execute (body result-type) "Execute BODY as Elixir code. RESULT-TYPE is either `value' or `output'. For `value', wraps code to capture return value. For `output', captures stdout directly. Returns the result as a string. May signal `ob-elixir-error' if execution fails and `ob-elixir-signal-errors' is non-nil." (let* ((tmp-file (org-babel-temp-file "ob-elixir-" ".exs")) (code (if (eq result-type 'value) (ob-elixir--wrap-for-value body) body))) (with-temp-file tmp-file (insert code)) (let ((result (with-temp-buffer (call-process ob-elixir-command nil t nil (org-babel-process-file-name tmp-file)) ;; Capture both stdout and stderr (buffer-string)))) (ob-elixir--process-result result)))) (defun org-babel-execute:elixir (body params) "Execute a block of Elixir code with org-babel. BODY is the Elixir code to execute. PARAMS is an alist of header arguments. This function is called by `org-babel-execute-src-block'." (let* ((result-type (cdr (assq :result-type params))) (result-params (cdr (assq :result-params params))) ;; Expand body with variable assignments (full-body (org-babel-expand-body:generic body params (org-babel-variable-assignments:elixir params))) (result (ob-elixir--execute full-body result-type))) (org-babel-reassemble-table (org-babel-result-cond result-params result (org-babel-script-escape result)) (org-babel-pick-name (cdr (assq :colname-names params)) (cdr (assq :colnames params))) (org-babel-pick-name (cdr (assq :rowname-names params)) (cdr (assq :rownames params)))))) (provide 'ob-elixir) ;;; ob-elixir.el ends here