331 lines
9.0 KiB
Markdown
331 lines
9.0 KiB
Markdown
# 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)
|