Commit42271745Recorded20 Feb 2026Repositorysigil-org

feat: Add table parsing to sigil-org

Message

Parse Org Mode table syntax (| col1 | col2 |) into SXML with table/tr/th/td elements. Separator rows (|---+---|) are skipped. First data row is treated as header (th) when multiple rows exist. Inline markup inside cells is parsed correctly.

Changed
 src/sigil/org.sgl | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 test/test-org.sgl | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 132 insertions(+), 1 deletion(-)
Diff
src/sigil/org.sglmodified
@@ -26,6 +26,7 @@
26
;;; | Text markup | `*bold*`, `/italic/`, `~code~`, `=verb=` |
27
;;; | Links | `[[url][desc]]`, `[[url]]` |
28
;;; | Lists | `- item`, `1. item`, checkboxes |
+29
;;; | Tables | `| col1 | col2 |` with optional separators |
30
;;; | Horizontal rules | `-----` (5+ dashes on a line) |
31
;;; | Comments | Lines starting with `#` (not `#+`) |
32
;;; | Timestamps | `<2026-02-20 Fri>`, `[2026-02-20 Fri]` |
@@ -145,6 +146,21 @@
146
#t)
147
(else #f))))))
148
+149
(define (table-line? line)
+150
(let ((trimmed (string-trim line)))
+151
(and (> (string-length trimmed) 0)
+152
(char=? (string-ref trimmed 0) #\|))))
+153
+154
(define (table-separator? line)
+155
(let ((trimmed (string-trim line)))
+156
(and (> (string-length trimmed) 2)
+157
(char=? (string-ref trimmed 0) #\|)
+158
(let loop ((i 1))
+159
(cond
+160
((>= i (string-length trimmed)) #t)
+161
((memv (string-ref trimmed i) '(#\- #\+ #\| #\space)) (loop (+ i 1)))
+162
(else #f))))))
+163
164
(define (char-numeric? c)
165
(and (char>=? c #\0) (char<=? c #\9)))
166
@@ -607,6 +623,9 @@
623
(ordered-list-item? (car lines)))
624
(let-values (((list-elem rest) (parse-list lines)))
625
(loop rest (cons list-elem content))))
+626
((table-line? (car lines))
+627
(let-values (((tbl rest) (parse-table lines)))
+628
(loop rest (cons tbl content))))
629
(else
630
;; Paragraph
631
(let-values (((para rest) (parse-paragraph lines)))
@@ -700,6 +719,68 @@
719
(reverse items))
720
lines))))))
721
+722
;; ============================================================
+723
;; Table Parsing
+724
;; ============================================================
+725
+726
;; Parse a table (consecutive | lines)
+727
(define (parse-table lines)
+728
(let loop ((lines lines) (rows '()))
+729
(cond
+730
((or (null? lines) (not (table-line? (car lines))))
+731
(values (cons 'table (build-table-rows (reverse rows)))
+732
lines))
+733
((table-separator? (car lines))
+734
;; Skip separator rows (|---+---| lines)
+735
(loop (cdr lines) rows))
+736
(else
+737
(loop (cdr lines) (cons (parse-table-row (car lines)) rows))))))
+738
+739
;; Parse a single table row: | cell1 | cell2 | ... |
+740
(define (parse-table-row line)
+741
(let* ((trimmed (string-trim line))
+742
;; Remove leading and trailing pipes
+743
(inner (let ((len (string-length trimmed)))
+744
(substring trimmed
+745
(if (char=? (string-ref trimmed 0) #\|) 1 0)
+746
(if (char=? (string-ref trimmed (- len 1)) #\|)
+747
(- len 1)
+748
len)))))
+749
;; Split on | and parse inline content in each cell
+750
(map (lambda (cell-text)
+751
(cons 'td (parse-inline (string-trim cell-text))))
+752
(split-table-cells inner))))
+753
+754
;; Split cell text on unescaped | characters
+755
(define (split-table-cells text)
+756
(let ((len (string-length text)))
+757
(let loop ((i 0) (start 0) (cells '()))
+758
(cond
+759
((>= i len)
+760
(reverse (cons (substring text start len) cells)))
+761
((char=? (string-ref text i) #\|)
+762
(loop (+ i 1) (+ i 1)
+763
(cons (substring text start i) cells)))
+764
(else
+765
(loop (+ i 1) start cells))))))
+766
+767
;; Build table rows, detecting header row (first row before separator)
+768
(define (build-table-rows rows)
+769
(if (null? rows)
+770
'()
+771
;; First row of data cells — check if originally followed by separator
+772
;; For simplicity, first row is always treated as header if table has >1 rows
+773
(if (null? (cdr rows))
+774
;; Single row table
+775
(list (cons 'tr (car rows)))
+776
;; Multiple rows: first is header, rest are body
+777
(cons (cons 'tr (map (lambda (cell)
+778
;; Convert td to th for header
+779
(cons 'th (cdr cell)))
+780
(car rows)))
+781
(map (lambda (row) (cons 'tr row))
+782
(cdr rows))))))
+783
784
;; Parse a paragraph (consecutive non-special lines)
785
(define (parse-paragraph lines)
786
(let loop ((lines lines) (para-lines '()))
@@ -717,7 +798,8 @@
798
(horizontal-rule? (car lines))
799
(unordered-list-item? (car lines))
800
(ordered-list-item? (car lines))
720
(comment-line? (car lines)))
+801
(comment-line? (car lines))
+802
(table-line? (car lines)))
803
(values (cons 'paragraph
804
(parse-inline (string-join (reverse para-lines) " ")))
805
lines))
test/test-org.sglmodified
@@ -284,6 +284,55 @@
284
(assert-equal 1 (length content))
285
(assert-equal 'paragraph (sxml-tag (car content))))))
286
+287
;; ============================================================
+288
;; Tables
+289
;; ============================================================
+290
+291
(test-group "tables"
+292
(test "simple table"
+293
(let* ((doc (org->sxml "| A | B |\n| 1 | 2 |"))
+294
(content (sxml-content doc))
+295
(tbl (car content)))
+296
(assert-equal 'table (sxml-tag tbl))
+297
;; Two rows, first is header (th), second is data (td)
+298
(let ((rows (sxml-content tbl)))
+299
(assert-equal 2 (length rows))
+300
(assert-equal 'tr (sxml-tag (car rows)))
+301
(assert-equal 'th (sxml-tag (car (sxml-content (car rows)))))
+302
(assert-equal 'td (sxml-tag (car (sxml-content (cadr rows))))))))
+303
+304
(test "table with separator row"
+305
(let* ((doc (org->sxml "| Name | Value |\n|------+-------|\n| foo | 42 |"))
+306
(content (sxml-content doc))
+307
(tbl (car content)))
+308
(assert-equal 'table (sxml-tag tbl))
+309
;; Separator row is skipped, so 2 data rows
+310
(let ((rows (sxml-content tbl)))
+311
(assert-equal 2 (length rows))
+312
;; First row is header
+313
(assert-equal 'th (sxml-tag (car (sxml-content (car rows))))))))
+314
+315
(test "table with inline markup in cells"
+316
(let* ((doc (org->sxml "| Key | Desc |\n|-----+------|\n| ~C-x~ | =save= |"))
+317
(content (sxml-content doc))
+318
(tbl (car content))
+319
(rows (sxml-content tbl))
+320
(data-row (cadr rows))
+321
(cells (sxml-content data-row)))
+322
;; First cell should contain code element
+323
(let ((cell1-content (sxml-content (car cells))))
+324
(assert-true (pair? cell1-content))
+325
(assert-equal 'code (sxml-tag (car cell1-content))))))
+326
+327
(test "single row table"
+328
(let* ((doc (org->sxml "| only | row |"))
+329
(content (sxml-content doc))
+330
(tbl (car content))
+331
(rows (sxml-content tbl)))
+332
;; Single row should just be a tr
+333
(assert-equal 1 (length rows))
+334
(assert-equal 'tr (sxml-tag (car rows))))))
+335
336
;; ============================================================
337
;; Integration
338
;; ============================================================