9.0 KiB
9.0 KiB
Task 05: Result Formatting and Table Support
Phase: 1 - Core (MVP) Priority: Medium Estimated Time: 1-2 hours Dependencies: Task 02 (Basic Execution), Task 03 (Variable Injection)
Objective
Implement proper result formatting so Elixir lists become org tables and results are properly parsed back into Elisp data structures.
Prerequisites
- Task 02 and 03 completed
- Basic execution and variables working
Background
Org-babel can display results as:
- Scalar values (
:results scalar) - Tables (
:results table) - Raw org markup (
:results raw) - Verbatim (
:results verbatim)
When Elixir returns a list like [[1, 2], [3, 4]], org should display it as a table:
| 1 | 2 |
| 3 | 4 |
Steps
Step 1: Implement result parsing
Add to ob-elixir.el:
;;; Result Formatting
(defun ob-elixir--table-or-string (result)
"Convert RESULT to Emacs table or string.
If RESULT looks like a list, parse it into an Elisp list.
Otherwise return as string.
Uses `org-babel-script-escape' for parsing."
(let ((trimmed (string-trim result)))
(cond
;; Empty result
((string-empty-p trimmed) nil)
;; Looks like a list - try to parse
((string-match-p "^\\[.*\\]$" trimmed)
(condition-case nil
(let ((parsed (org-babel-script-escape trimmed)))
(ob-elixir--sanitize-table parsed))
(error trimmed)))
;; Looks like a tuple - convert to list first
((string-match-p "^{.*}$" trimmed)
(condition-case nil
(let* ((as-list (replace-regexp-in-string
"^{\\(.*\\)}$" "[\\1]" trimmed))
(parsed (org-babel-script-escape as-list)))
(ob-elixir--sanitize-table parsed))
(error trimmed)))
;; Scalar value
(t trimmed))))
Step 2: Implement table sanitization
(defvar ob-elixir-nil-to 'hline
"Elisp value to use for Elixir nil in table cells.
When nil appears in an Elixir list that becomes a table,
it is replaced with this value. Use `hline' for org table
horizontal lines, or nil for empty cells.")
(defun ob-elixir--sanitize-table (data)
"Sanitize DATA for use as an org table.
Replaces nil values according to `ob-elixir-nil-to'.
Ensures consistent structure for table rendering."
(cond
;; Not a list - return as-is
((not (listp data)) data)
;; Empty list
((null data) nil)
;; List of lists - could be table
((and (listp (car data)) (not (null (car data))))
(mapcar #'ob-elixir--sanitize-row data))
;; Simple list - single row
(t (ob-elixir--sanitize-row data))))
(defun ob-elixir--sanitize-row (row)
"Sanitize a single ROW for table display."
(if (listp row)
(mapcar (lambda (cell)
(cond
((null cell) ob-elixir-nil-to)
((eq cell 'nil) ob-elixir-nil-to)
(t cell)))
row)
row))
Step 3: Handle keyword lists and maps
(defun ob-elixir--parse-keyword-list (str)
"Parse STR as Elixir keyword list into alist.
Handles format like: [a: 1, b: 2]"
(when (string-match "^\\[\\(.*\\)\\]$" str)
(let ((content (match-string 1 str)))
(when (string-match-p "^[a-z_]+:" content)
(let ((pairs '()))
(dolist (part (split-string content ", "))
(when (string-match "^\\([a-z_]+\\):\\s-*\\(.+\\)$" part)
(push (cons (intern (match-string 1 part))
(ob-elixir--parse-value (match-string 2 part)))
pairs)))
(nreverse pairs))))))
(defun ob-elixir--parse-value (str)
"Parse STR as a simple Elixir value."
(let ((trimmed (string-trim str)))
(cond
((string= trimmed "nil") nil)
((string= trimmed "true") t)
((string= trimmed "false") nil)
((string-match-p "^[0-9]+$" trimmed)
(string-to-number trimmed))
((string-match-p "^[0-9]+\\.[0-9]+$" trimmed)
(string-to-number trimmed))
((string-match-p "^\".*\"$" trimmed)
(substring trimmed 1 -1))
((string-match-p "^:.*$" trimmed)
(intern (substring trimmed 1)))
(t trimmed))))
Step 4: Update the execute function
Ensure org-babel-execute:elixir uses the parsing:
(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."
(let* ((result-type (cdr (assq :result-type params)))
(result-params (cdr (assq :result-params params)))
(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
;; For output/scalar/verbatim - return as-is
result
;; For value - parse into Elisp data
(ob-elixir--table-or-string 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))))))
Step 5: Support column names
(defun ob-elixir--maybe-add-colnames (result params)
"Add column names to RESULT if specified in PARAMS."
(let ((colnames (cdr (assq :colnames params))))
(if (and colnames (listp result) (listp (car result)))
(cons (if (listp colnames) colnames (car result))
(if (listp colnames) result (cdr result)))
result)))
Step 6: Add tests
Add to test/test-ob-elixir.el:
;;; Result Formatting Tests
(ert-deftest ob-elixir-test-parse-simple-list ()
"Test parsing simple list result."
(should (equal '(1 2 3) (ob-elixir--table-or-string "[1, 2, 3]"))))
(ert-deftest ob-elixir-test-parse-nested-list ()
"Test parsing nested list (table) result."
(should (equal '((1 2) (3 4))
(ob-elixir--table-or-string "[[1, 2], [3, 4]]"))))
(ert-deftest ob-elixir-test-parse-tuple ()
"Test parsing tuple result."
(should (equal '(1 2 3) (ob-elixir--table-or-string "{1, 2, 3}"))))
(ert-deftest ob-elixir-test-parse-scalar ()
"Test that scalars are returned as strings."
(should (equal "42" (ob-elixir--table-or-string "42")))
(should (equal ":ok" (ob-elixir--table-or-string ":ok"))))
(ert-deftest ob-elixir-test-parse-string ()
"Test parsing string result."
(should (equal "\"hello\"" (ob-elixir--table-or-string "\"hello\""))))
(ert-deftest ob-elixir-test-sanitize-table-nil ()
"Test that nil values are sanitized in tables."
(let ((ob-elixir-nil-to 'hline))
(should (equal '((1 hline) (hline 2))
(ob-elixir--sanitize-table '((1 nil) (nil 2)))))))
(ert-deftest ob-elixir-test-execution-returns-table ()
"Test that list results become tables."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--table-or-string
(ob-elixir--execute "[[1, 2], [3, 4]]" 'value))))
(should (equal '((1 2) (3 4)) result))))
(ert-deftest ob-elixir-test-mixed-list ()
"Test parsing mixed-type list."
(skip-unless (executable-find ob-elixir-command))
(let ((result (ob-elixir--table-or-string
(ob-elixir--execute "[1, \"two\", :three]" 'value))))
(should (listp result))
(should (= 3 (length result)))))
Step 7: Test in an org buffer
Add to test.org:
* Result Formatting Tests
** Simple list as table row
#+BEGIN_SRC elixir
[1, 2, 3, 4, 5]
#+END_SRC
#+RESULTS:
| 1 | 2 | 3 | 4 | 5 |
** Nested list as table
#+BEGIN_SRC elixir
[
["Alice", 30],
["Bob", 25],
["Charlie", 35]
]
#+END_SRC
#+RESULTS:
| Alice | 30 |
| Bob | 25 |
| Charlie | 35 |
** Map result (verbatim)
#+BEGIN_SRC elixir :results verbatim
%{name: "Alice", age: 30}
#+END_SRC
#+RESULTS:
: %{age: 30, name: "Alice"}
** Enum operations returning lists
#+BEGIN_SRC elixir
Enum.map(1..5, fn x -> [x, x * x] end)
#+END_SRC
#+RESULTS:
| 1 | 1 |
| 2 | 4 |
| 3 | 9 |
| 4 | 16 |
| 5 | 25 |
** Tuple result
#+BEGIN_SRC elixir
{:ok, "success", 123}
#+END_SRC
Acceptance Criteria
- Simple lists
[1, 2, 3]become table rows - Nested lists
[[1, 2], [3, 4]]become tables - Tuples are handled (converted to lists)
- Scalar values remain as strings
:results verbatimbypasses table conversion- nil values in tables are handled according to config
- All tests pass:
make test
Result Format Reference
| Elixir Value | Org Display |
|---|---|
[1, 2, 3] |
| 1 | 2 | 3 | |
[[1], [2]] |
Multi-row table |
{:ok, 1} |
| ok | 1 | |
42 |
: 42 |
"hello" |
: "hello" |
%{a: 1} |
: %{a: 1} |
Files Modified
ob-elixir.el- Add result formatting functionstest/test-ob-elixir.el- Add formatting tests
References
- docs/03-org-babel-implementation-guide.md - Result Handling section
- Org Manual - Results of Evaluation