docs and tasks
This commit is contained in:
330
tasks/05-result-formatting.md
Normal file
330
tasks/05-result-formatting.md
Normal file
@@ -0,0 +1,330 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user