diff --git a/ob-elixir.el b/ob-elixir.el index 23fdc5e..e4152e6 100644 --- a/ob-elixir.el +++ b/ob-elixir.el @@ -67,6 +67,95 @@ Can be a full path or command name if in PATH." (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 @@ -115,7 +204,11 @@ 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))) - (result (ob-elixir--execute body result-type))) + ;; 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 diff --git a/test/test-ob-elixir.el b/test/test-ob-elixir.el index ca52467..773e338 100644 --- a/test/test-ob-elixir.el +++ b/test/test-ob-elixir.el @@ -54,5 +54,82 @@ (should (or (string-match-p "%{a: 1, b: 2}" result) (string-match-p "%{b: 2, a: 1}" result))))) +;;; Type Conversion Tests + +(ert-deftest ob-elixir-test-convert-nil () + "Test nil conversion." + (should (equal "nil" (ob-elixir--elisp-to-elixir nil)))) + +(ert-deftest ob-elixir-test-convert-true () + "Test t conversion." + (should (equal "true" (ob-elixir--elisp-to-elixir t)))) + +(ert-deftest ob-elixir-test-convert-integer () + "Test integer conversion." + (should (equal "42" (ob-elixir--elisp-to-elixir 42))) + (should (equal "-10" (ob-elixir--elisp-to-elixir -10)))) + +(ert-deftest ob-elixir-test-convert-float () + "Test float conversion." + (should (equal "3.14" (ob-elixir--elisp-to-elixir 3.14)))) + +(ert-deftest ob-elixir-test-convert-string () + "Test string conversion." + (should (equal "\"hello\"" (ob-elixir--elisp-to-elixir "hello")))) + +(ert-deftest ob-elixir-test-convert-string-escaping () + "Test string escaping." + (should (equal "\"say \\\"hi\\\"\"" + (ob-elixir--elisp-to-elixir "say \"hi\""))) + (should (equal "\"line1\\nline2\"" + (ob-elixir--elisp-to-elixir "line1\nline2")))) + +(ert-deftest ob-elixir-test-convert-symbol () + "Test symbol conversion to atom." + (should (equal ":foo" (ob-elixir--elisp-to-elixir 'foo))) + (should (equal ":ok" (ob-elixir--elisp-to-elixir 'ok)))) + +(ert-deftest ob-elixir-test-convert-list () + "Test list conversion." + (should (equal "[1, 2, 3]" (ob-elixir--elisp-to-elixir '(1 2 3)))) + (should (equal "[\"a\", \"b\"]" (ob-elixir--elisp-to-elixir '("a" "b"))))) + +(ert-deftest ob-elixir-test-convert-nested-list () + "Test nested list conversion." + (should (equal "[[1, 2], [3, 4]]" + (ob-elixir--elisp-to-elixir '((1 2) (3 4)))))) + +(ert-deftest ob-elixir-test-convert-vector () + "Test vector to tuple conversion." + (should (equal "{1, 2, 3}" (ob-elixir--elisp-to-elixir [1 2 3])))) + +;;; Variable Injection Tests + +(ert-deftest ob-elixir-test-variable-assignments () + "Test variable assignment generation." + (let ((params '((:var . ("x" . 5)) + (:var . ("name" . "Alice"))))) + (let ((assignments (org-babel-variable-assignments:elixir params))) + (should (member "x = 5" assignments)) + (should (member "name = \"Alice\"" assignments))))) + +(ert-deftest ob-elixir-test-var-execution () + "Test code execution with variables." + (skip-unless (executable-find ob-elixir-command)) + (let* ((params '((:var . ("x" . 10)))) + (var-lines (org-babel-variable-assignments:elixir params)) + (full-body (concat (mapconcat #'identity var-lines "\n") + "\nx * 2"))) + (should (equal "20" (ob-elixir--execute full-body 'value))))) + +(ert-deftest ob-elixir-test-var-list () + "Test passing list as variable." + (skip-unless (executable-find ob-elixir-command)) + (let* ((params '((:var . ("data" . (1 2 3))))) + (var-lines (org-babel-variable-assignments:elixir params)) + (full-body (concat (mapconcat #'identity var-lines "\n") + "\nEnum.sum(data)"))) + (should (equal "6" (ob-elixir--execute full-body 'value))))) + (provide 'test-ob-elixir) ;;; test-ob-elixir.el ends here