# 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`: ```elisp ;;; 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 ```elisp (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 ```elisp (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: ```elisp (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 ```elisp (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`: ```elisp ;;; 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`: ```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 verbatim` bypasses 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 functions - `test/test-ob-elixir.el` - Add formatting tests ## References - [docs/03-org-babel-implementation-guide.md](../docs/03-org-babel-implementation-guide.md) - Result Handling section - [Org Manual - Results of Evaluation](https://orgmode.org/manual/Results-of-Evaluation.html)