;;; 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) ;;; 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) ;;; 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))) ;;; 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." (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 (org-babel-eval (format "%s %s" ob-elixir-command (org-babel-process-file-name tmp-file)) ""))) (string-trim 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