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
;; ============================================================