Import new table renderer.

This commit is contained in:
Dominik Pantůček 2023-06-15 14:26:50 +02:00
parent 3a59a9293a
commit 3f7f1356a4
12 changed files with 3859 additions and 1 deletions

View file

@ -51,7 +51,10 @@ HACKERBASE-OBJS=hackerbase.o testing.o listing.o cal-month.o \
util-string.o util-io.o util-parser.o texts.o tests.o \ util-string.o util-io.o util-parser.o texts.o tests.o \
util-proc.o util-mail.o notifications.o util-format.o \ util-proc.o util-mail.o notifications.o util-format.o \
brmember-format.o logging.o specification.o util-git.o \ brmember-format.o logging.o specification.o util-git.o \
cal-day.o util-stdout.o cal-format.o util-dict-bst.o cal-day.o util-stdout.o cal-format.o util-dict-bst.o table.o \
sgr-list.o sgr-block.o table-processor.o table-border.o \
table-style.o sgr-state.o util-utf8.o sgr-cell.o \
template-list-expander.o box-drawing.o
.PHONY: imports .PHONY: imports
imports: $(HACKERBASE-DEPS) imports: $(HACKERBASE-DEPS)
@ -389,3 +392,70 @@ util-dict-bst.import.scm: $(UTIL-DICT-BST-SOURCES)
RACKET-KWARGS-SOURCES=racket-kwargs.scm RACKET-KWARGS-SOURCES=racket-kwargs.scm
racket-kwargs.import.scm: $(RACKET-KWARGS-SOURCES) racket-kwargs.import.scm: $(RACKET-KWARGS-SOURCES)
TABLE-SOURCES=table.scm sgr-list.import.scm sgr-block.import.scm \
racket-kwargs.import.scm table-processor.import.scm \
table-border.import.scm table-style.import.scm
table.o: table.import.scm
table.import.scm: $(TABLE-SOURCES)
SGR-LIST-SOURCES=sgr-list.scm raket-kwargs.import.scm \
sgr-state.import.scm testing.import.scm util-utf8.import.scm
sgr-list.o: sgr-list.import.scm
sgr-list.import.scm: $(SGR-LIST-SOURCES)
SGR-BLOCK-SOURCES=sgr-block.scm racket-kwargs.import.scm \
sgr-state.import.scm sgr-list.import.scm testing.import.scm
sgr-block.o: sgr-block.import.scm
sgr-block.import.scm: $(SGR-BLOCK-SOURCES)
TABLE-PROCESSOR-SOURCES=table-processor.scm sgr-cell.import.scm \
template-list-expander.import.scm
table-processor.o: table-processor.import.scm
table-processor.import.scm: $(TABLE-PROCESSOR-SOURCES)
TABLE-BORDER-SOURCES=table-border.scm racket-kwargs.import.scm \
box-drawing.import.scm util-utf8.import.scm \
sgr-block.import.scm
table-border.o: table-border.import.scm
table-border.import.scm: $(TABLE-BORDER-SOURCES)
TABLE-STYLE-SOURCES=table-style.scm box-drawing.import.scm \
testing.import.scm template-list-expander.import.scm
table-style.o: table-style.import.scm
table-style.import.scm: $(TABLE-STYLE-SOURCES)
SGR-STATE-SOURCES=sgr-state.scm testing.import.scm \
racket-kwargs.import.scm
sgr-state.o: sgr-state.import.scm
sgr-state.import.scm: $(SGR-STATE-SOURCES)
UTIL-UTF8-SOURCES=util-utf8.scm testing.import.scm
util-utf8.o: util-utf8.import.scm
util-utf8.import.scm: $(UTIL-UTF8-SOURCES)
SGR-CELL-SOURCES=sgr-cell.scm racket-kwargs.import.scm \
sgr-state.import.scm sgr-list.import.scm sgr-block.import.scm
sgr-cell.o: sgr-cell.import.scm
sgr-cell.import.scm: $(SGR-CELL-SOURCES)
TEMPLATE-LIST-EXPANDER-SOURCES=template-list-expander.scm \
testing.import.scm
template-list-expander.o: template-list-expander.import.scm
template-list-expander.import.scm: $(TEMPLATE-LIST-EXPANDER-SOURCES)
BOX-DRAWING-SOURCES=box-drawing.scm util-utf8.import.scm \
racket-kwargs.import.scm testing.import.scm
box-drawing.o: box-drawing.import.scm
box-drawing.import.scm: $(BOX-DRAWING-SOURCES)

765
src/box-drawing.scm Normal file
View file

@ -0,0 +1,765 @@
;;
;; box-drawing.scm
;;
;; Unicode box drawing combiners.
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit box-drawing))
(module
box-drawing
(
combine-line-cells
line-cell-somewhat-heavy?
line-cell->unicode-char
line-cell->utf8-string
line-cell->ascii-char
char->line-cell
combine-line-char
line-style-none
combine-line-style
line-cell-north
line-cell-east
line-cell-west
line-cell-south
spec->line-style
line-style-spec?
make-line-cell
make-straight-horizontal-line-cell
make-straight-vertical-line-cell
make-straight-horizontal-line-cell*
make-straight-vertical-line-cell*
line-cell-none
set-line-cell-north
set-line-cell-west
set-line-cell-east
set-line-cell-south
spec->horizontal-line-cell
spec->vertical-line-cell
spec->top-left-corner-line-cell
spec->top-right-corner-line-cell
spec->bottom-left-corner-line-cell
spec->bottom-right-corner-line-cell
extract-line-cell-top-left
extract-line-cell-top-right
extract-line-cell-bottom-left
extract-line-cell-bottom-right
box-drawing-tests!
)
(import scheme
(chicken base)
(chicken bitwise)
util-utf8
racket-kwargs
testing)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Bit manipulators
(define-syntax define-bit-predicate
(syntax-rules ()
((_ name mask value)
(define (name bits)
(= (bitwise-and bits mask) value)))))
(define-syntax define-bit-setter
(syntax-rules ()
((_ name andmask ormask)
(define (name bits)
(bitwise-ior (bitwise-and bits andmask) ormask)))))
(define-syntax define-bit-combiner
(syntax-rules ()
((_ name mask)
(define (name a b)
(bitwise-ior
(bitwise-and a mask)
(bitwise-and b mask))))))
(define-syntax define-bit-accessor
(syntax-rules ()
((_ name shift mask)
(define (name bits)
(bitwise-and
(arithmetic-shift bits (- shift)) mask)))))
(define-syntax define-bit-accessor-setter
(syntax-rules ()
((_ name shift mask)
(define (name bits ormask)
(bitwise-ior (bitwise-and (bitwise-not (arithmetic-shift mask shift))
bits)
(arithmetic-shift (bitwise-and ormask mask)
shift))))))
(define-syntax define-inverted-bit-predicate
(syntax-rules ()
((_ name mask value)
(define (name bits)
(not (= (bitwise-and bits mask) 0))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Line thickness
;; * none
;; * light
;; * heavy
;; Bit indices: 10
;; ||_ lower bit: light thickness
;; |_ higher bit: heavy thickness
;; Values:
;; 00 - none
;; 01 - light
;; 10 - heavy
;; 11 - heavy
(define line-thickness-none 0)
(define line-thickness-light 1)
(define line-thickness-heavy 2)
(define-bit-predicate line-thickness-light? 3 1)
(define-bit-predicate line-thickness-heavy? 2 2)
(define-bit-predicate line-thickness-none? 3 0)
(define-bit-setter set-line-thickness-light 3 1)
(define-bit-setter set-line-thickness-heavy 3 2)
(define-bit-setter set-line-thickness-none 0 0)
(define-bit-combiner combine-line-thickness 3)
(define-inverted-bit-predicate line-thickness-some? 3 0)
(define (line-thickness-normalize thickness)
(if (line-thickness-heavy? thickness)
line-thickness-heavy
thickness))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Line type
;; * none
;; * dashed
;; * solid
;; Bit indices: 10
;; ||_ lower bit: dashed type
;; |_ higher bit: solid type
;; Values:
;; * 00 - none
;; * 01 - dashed
;; * 10 - solid
;; * 11 - solid
(define line-type-none 0)
(define line-type-dashed 1)
(define line-type-solid 2)
(define-bit-predicate line-type-dashed? 3 1)
(define-bit-predicate line-type-solid? 2 2)
(define-bit-predicate line-type-none? 3 0)
(define-bit-setter set-line-type-dashed 3 1)
(define-bit-setter set-line-type-solid 3 2)
(define-bit-setter set-line-type-none 0 0)
(define-bit-combiner combine-line-type 3)
(define-inverted-bit-predicate line-type-some? 3 0)
(define (line-type-normalize type)
(if (line-type-solid? type)
line-type-solid
type))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Line style
;; * thickness
;; * type
;; Double-bit indices: 10
;; ||_ lower nibble: thickness
;; |_ higher nibble: type
;; Bit indices: 3210
;; ||||_ light thickness
;; |||_ heavy thickness
;; ||_ dashed type
;; |_ solid type
(define line-style-none 0)
;; Creates line style from specific thickness and type
(define (make-line-style thickness type)
(bitwise-ior
(arithmetic-shift (bitwise-and type 3) 2)
(bitwise-and (bitwise-and thickness 3))))
(define-bit-accessor line-style-thickness 0 3)
(define-bit-accessor line-style-type 2 3)
(define-bit-predicate line-style-thickness-light? 3 1)
(define-bit-predicate line-style-thickness-heavy? 2 2)
(define-bit-predicate line-style-thickness-none? 3 0)
(define-bit-predicate line-style-type-dashed? 12 4)
(define-bit-predicate line-style-type-solid? 8 8)
(define-bit-predicate line-style-type-none? 12 0)
(define-bit-setter set-line-style-thickness-light 15 1)
(define-bit-setter set-line-style-thickness-heavy 15 2)
(define-bit-setter set-line-style-thickness-none 12 0)
(define-bit-setter set-line-style-type-dashed 15 4)
(define-bit-setter set-line-style-type-solid 15 8)
(define-bit-setter set-line-style-type-none 3 0)
(define-bit-combiner combine-line-style 15)
(define-inverted-bit-predicate line-style-thickness-some? 3 0)
(define-inverted-bit-predicate line-style-type-some? 12 0)
(define (line-style-some? style)
(and (line-style-thickness-some? style)
(line-style-type-some? style)))
(define (line-style-none? style)
(or (line-style-thickness-none? style)
(line-style-type-none? style)))
(define (line-style-normalize style)
(make-line-style
(line-thickness-normalize (line-style-thickness style))
(line-type-normalize (line-style-type style))))
(define (line-style-heavy? style)
(and (line-style-type-some? style)
(line-style-thickness-heavy? style)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Line cell
;; * north
;; * west
;; * east
;; * south
;; Nibble indices: 3210
;; ||||_ north style
;; |||_ west style
;; ||_ east style
;; |_ south style
;; Bit indices: fedcba9876543210
;; ||||||||||||||||_ north light thickness
;; |||||||||||||||_ north heavy thickness
;; ||||||||||||||_ north dashed type
;; |||||||||||||_ north solid type
;; ||||||||||||_ west light thickness
;; |||||||||||_ west heavy thickness
;; ||||||||||_ west dashed type
;; |||||||||_ west solid type
;; ||||||||_ east light thickness
;; |||||||_ east heavy thickness
;; ||||||_ east dashed type
;; |||||_ east solid type
;; ||||_ south light thickness
;; |||_ south heavy thickness
;; ||_ south dashed type
;; |_ south solid type
(define line-cell-none 0)
(define (make-line-cell n w e s)
(bitwise-ior
(bitwise-and n 15)
(arithmetic-shift (bitwise-and w 15) 4)
(arithmetic-shift (bitwise-and e 15) 8)
(arithmetic-shift (bitwise-and s 15) 12)))
(define-bit-accessor line-cell-north 0 15)
(define-bit-accessor line-cell-west 4 15)
(define-bit-accessor line-cell-east 8 15)
(define-bit-accessor line-cell-south 12 15)
(define-bit-accessor-setter set-line-cell-north 0 15)
(define-bit-accessor-setter set-line-cell-west 4 15)
(define-bit-accessor-setter set-line-cell-east 8 15)
(define-bit-accessor-setter set-line-cell-south 12 15)
(define-bit-combiner combine-line-cells 65535)
(define (make-straight-horizontal-line-cell thickness type)
(let ((style (make-line-style thickness type)))
(make-line-cell 0 style style 0)))
(define (make-straight-vertical-line-cell thickness type)
(let ((style (make-line-style thickness type)))
(make-line-cell style 0 0 style)))
(define (make-straight-horizontal-line-cell* style)
(make-line-cell 0 style style 0))
(define (make-straight-vertical-line-cell* style)
(make-line-cell style 0 0 style))
(define (make-cross-line-cell thickness type)
(let ((style (make-line-style thickness type)))
(make-line-cell style style style style)))
(define (line-cell-straight-horizontal? cell)
(and (line-style-none? (line-cell-north cell))
(line-style-none? (line-cell-south cell))
(line-style-some? (line-cell-west cell))
(line-style-some? (line-cell-east cell))
(= (line-style-normalize (line-cell-west cell))
(line-style-normalize (line-cell-east cell)))))
(define (line-cell-straight-vertical? cell)
(and (line-style-some? (line-cell-north cell))
(line-style-none? (line-cell-west cell))
(line-style-none? (line-cell-east cell))
(line-style-some? (line-cell-south cell))
(= (line-style-normalize (line-cell-north cell))
(line-style-normalize (line-cell-south cell)))))
(define (line-cell-mostly-horizontal? cell)
(and (line-style-none? (line-cell-north cell))
(line-style-none? (line-cell-south cell))
(line-style-some? (line-cell-west cell))
(line-style-some? (line-cell-east cell))))
(define (line-cell-mostly-vertical? cell)
(and (line-style-some? (line-cell-north cell))
(line-style-none? (line-cell-west cell))
(line-style-none? (line-cell-east cell))
(line-style-some? (line-cell-south cell))))
(define (line-cell-none? cell)
(and (line-style-none? (line-cell-north cell))
(line-style-none? (line-cell-west cell))
(line-style-none? (line-cell-east cell))
(line-style-none? (line-cell-south cell))))
(define (line-cell-somewhat-heavy? cell)
(or (line-style-heavy? (line-cell-north cell))
(line-style-heavy? (line-cell-west cell))
(line-style-heavy? (line-cell-east cell))
(line-style-heavy? (line-cell-south cell))))
(define (line-cell-straight-horizontal-type cell)
(line-style-type
(line-cell-west cell)))
(define (line-cell-straight-vertical-type cell)
(line-style-type
(line-cell-north cell)))
(define (line-cell-straight-horizontal-dashed? cell)
(and (line-cell-straight-horizontal? cell)
(line-type-dashed?
(line-cell-straight-horizontal-type cell))))
(define (line-cell-straight-vertical-dashed? cell)
(and (line-cell-straight-vertical? cell)
(line-type-dashed?
(line-cell-straight-vertical-type cell))))
(define (line-cell-straight-horizontal-thickness cell)
(line-style-thickness
(line-cell-west cell)))
(define (line-cell-straight-vertical-thickness cell)
(line-style-thickness
(line-cell-north cell)))
(define (line-cell-junction-compress cell)
(let ((n (line-thickness-normalize (line-style-thickness (line-cell-north cell))))
(w (line-thickness-normalize (line-style-thickness (line-cell-west cell))))
(e (line-thickness-normalize (line-style-thickness (line-cell-east cell))))
(s (line-thickness-normalize (line-style-thickness (line-cell-south cell)))))
(+ n
(* w 3)
(* e 9)
(* s 27))))
(define line-cell-char-junctions
(vector ;; SEWN (base-3 representation)
#\space ;; 0000
#\x2575 ;; 0001
#\x2579 ;; 0002
#\x2574 ;; 0010
#\x2518 ;; 0011
#\x251a ;; 0012
#\x2578 ;; 0020
#\x2519 ;; 0021
#\x251b ;; 0022
#\x2576 ;; 0100
#\x2514 ;; 0101
#\x2516 ;; 0102
#\x2500 ;; 0110
#\x2534 ;; 0111
#\x2538 ;; 0112
#\x257e ;; 0120
#\x2535 ;; 0121
#\x2539 ;; 0122
#\x257a ;; 0200
#\x2515 ;; 0201
#\x2517 ;; 0202
#\x257c ;; 0210
#\x2536 ;; 0211
#\x253a ;; 0212
#\x2501 ;; 0220
#\x2537 ;; 0221
#\x253b ;; 0222
#\x2577 ;; 1000
#\x2502 ;; 1001
#\x257f ;; 1002
#\x2510 ;; 1010
#\x2524 ;; 1011
#\x2526 ;; 1012
#\x2511 ;; 1020
#\x2525 ;; 1021
#\x2529 ;; 1022
#\x250c ;; 1100
#\x251c ;; 1101
#\x251e ;; 1102
#\x252c ;; 1110
#\x253c ;; 1111
#\x2540 ;; 1112
#\x252d ;; 1120
#\x253d ;; 1121
#\x2543 ;; 1122
#\x250d ;; 1200
#\x251d ;; 1201
#\x2521 ;; 1202
#\x252e ;; 1210
#\x253e ;; 1211
#\x2544 ;; 1212
#\x252f ;; 1220
#\x253f ;; 1221
#\x2547 ;; 1222
#\x257b ;; 2000
#\x257d ;; 2001
#\x2503 ;; 2002
#\x2512 ;; 2010
#\x2527 ;; 2011
#\x2528 ;; 2012
#\x2513 ;; 2020
#\x252a ;; 2021
#\x252b ;; 2022
#\x250e ;; 2100
#\x251f ;; 2101
#\x2520 ;; 2102
#\x2530 ;; 2110
#\x2541 ;; 2111
#\x2542 ;; 2112
#\x2531 ;; 2120
#\x2545 ;; 2121
#\x2549 ;; 2122
#\x250f ;; 2200
#\x2522 ;; 2201
#\x2523 ;; 2202
#\x2532 ;; 2210
#\x2546 ;; 2211
#\x254a ;; 2212
#\x2533 ;; 2220
#\x2548 ;; 2221
#\x254b ;; 2222
))
(define line-cell-char-horizontal-light-dashed #\x254c)
(define line-cell-char-horizontal-heavy-dashed #\x254d)
(define line-cell-char-vertical-light-dashed #\x254e)
(define line-cell-char-vertical-heavy-dashed #\x254f)
(define (line-cell->unicode-char cell)
(cond ((line-cell-straight-horizontal-dashed? cell)
(let ((thickness (line-cell-straight-horizontal-thickness cell)))
(if (line-thickness-light? thickness)
line-cell-char-horizontal-light-dashed
line-cell-char-horizontal-heavy-dashed)))
((line-cell-straight-vertical-dashed? cell)
(let ((thickness (line-cell-straight-vertical-thickness cell)))
(if (line-thickness-light? thickness)
line-cell-char-vertical-light-dashed
line-cell-char-vertical-heavy-dashed)))
(else
(let ((compressed (line-cell-junction-compress cell)))
(vector-ref line-cell-char-junctions compressed)))))
(define line-cell-string-junctions
(apply
vector
(map utf8-char->string
(vector->list line-cell-char-junctions))))
(define line-cell-string-horizontal-light-dashed
(utf8-char->string line-cell-char-horizontal-light-dashed))
(define line-cell-string-horizontal-heavy-dashed
(utf8-char->string line-cell-char-horizontal-heavy-dashed))
(define line-cell-string-vertical-light-dashed
(utf8-char->string line-cell-char-vertical-light-dashed))
(define line-cell-string-vertical-heavy-dashed
(utf8-char->string line-cell-char-vertical-heavy-dashed))
(define (line-cell->utf8-string cell)
(cond ((line-cell-straight-horizontal-dashed? cell)
(let ((thickness (line-cell-straight-horizontal-thickness cell)))
(if (line-thickness-light? thickness)
line-cell-string-horizontal-light-dashed
line-cell-string-horizontal-heavy-dashed)))
((line-cell-straight-vertical-dashed? cell)
(let ((thickness (line-cell-straight-vertical-thickness cell)))
(if (line-thickness-light? thickness)
line-cell-string-vertical-light-dashed
line-cell-string-vertical-heavy-dashed)))
(else
(let ((compressed (line-cell-junction-compress cell)))
(vector-ref line-cell-string-junctions compressed)))))
(define (line-cell->ascii-char cell)
(cond ((line-cell-mostly-horizontal? cell)
#\-)
((line-cell-mostly-vertical? cell)
#\|)
((line-cell-none? cell)
#\space)
(else
#\+)))
(define line-cell-reverse-lookup (make-vector 128 line-cell-none))
(let loop ((idx 1))
(when (< idx 81)
(let* ((ch (vector-ref line-cell-char-junctions idx))
(cp (char->integer ch))
(ridx (- cp #x2500))
(n (modulo idx 3))
(w (modulo (quotient idx 3) 3))
(e (modulo (quotient idx 9) 3))
(s (modulo (quotient idx 27) 3)))
(vector-set! line-cell-reverse-lookup
ridx
(make-line-cell (make-line-style n line-type-solid)
(make-line-style w line-type-solid)
(make-line-style e line-type-solid)
(make-line-style s line-type-solid)))
(loop (add1 idx)))))
(define* (char->line-cell char #:bold (bold? #f))
(case char
((#\-) (make-straight-horizontal-line-cell bold? 'solid))
((#\|) (make-straight-vertical-line-cell bold? 'solid))
((#\+) (make-cross-line-cell bold? 'solid))
((#\space) line-cell-none)
(else
(let ((cp (char->integer char)))
(cond ((or (< cp #x2500)
(> cp #x257f))
line-cell-none)
(else
(vector-ref line-cell-reverse-lookup (- cp #x2500))))))))
(define (combine-line-char ch1 ch2)
(line-cell->unicode-char
(combine-line-cells
(char->line-cell ch1)
(char->line-cell ch2))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Style specifications
(define (spec->line-thickness spec)
(case spec
((light) line-thickness-light)
((heavy) line-thickness-heavy)
(else line-thickness-none)))
(define (spec->line-type spec)
(case spec
((dashed) line-type-dashed)
((solid) line-type-solid)
(else line-type-none)))
(define (spec->line-style spec)
(cond ((symbol? spec)
(case spec
((light heavy)
(make-line-style (spec->line-thickness spec)
line-type-solid))
((dashed solid)
(make-line-style line-thickness-light
(spec->line-type spec)))
(else 0)))
((list? spec)
(let ((res (foldl
(lambda (acc spec1)
(combine-line-style
acc
(case spec1
((light heavy)
(make-line-style (spec->line-thickness spec1)
line-type-none))
((dashed solid)
(make-line-style line-thickness-none
(spec->line-type spec1)))
(else line-style-none))))
line-style-none
spec)))
(cond ((and (line-style-thickness-some? res)
(line-style-type-none? res))
(set-line-style-type-solid res))
((and (line-style-thickness-none? res)
(line-style-type-some? res))
(set-line-style-thickness-light res))
(else
res))))
(else line-style-none)))
(define (line-style-spec? spec)
(or (memq spec '(none light heavy dashed solid))
(and (list? spec)
(let loop ((lst spec))
(if (null? lst)
#t
(if (line-style-spec? (car spec))
(loop (cdr lst))
#f))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Cell makers
(define-syntax define-spec->line-cell
(syntax-rules ()
((_ name spec style a b c d)
(define (name spec)
(let ((style (spec->line-style spec)))
(make-line-cell a b c d))))))
(define-spec->line-cell spec->horizontal-line-cell spec style 0 style style 0)
(define-spec->line-cell spec->vertical-line-cell spec style style 0 0 style)
(define-spec->line-cell spec->north-west-line-cell spec style style style 0 0)
(define-spec->line-cell spec->north-east-line-cell spec style style 0 style 0)
(define-spec->line-cell spec->south-west-line-cell spec style 0 style 0 style)
(define-spec->line-cell spec->south-east-line-cell spec style 0 0 style style)
(define spec->top-left-corner-line-cell spec->south-east-line-cell)
(define spec->top-right-corner-line-cell spec->south-west-line-cell)
(define spec->bottom-left-corner-line-cell spec->north-east-line-cell)
(define spec->bottom-right-corner-line-cell spec->north-west-line-cell)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Cell extractors
(define ((make-line-cell-extractor n w e s) c)
(let ((sm (if s 15 0))
(em (if e 240 0))
(wm (if w 3840 0))
(nm (if n 61440 0)))
(bitwise-and c (bitwise-ior sm em wm nm))))
(define extract-line-cell-north-west
(make-line-cell-extractor #t #t #f #f))
(define extract-line-cell-north-east
(make-line-cell-extractor #t #f #t #f))
(define extract-line-cell-south-west
(make-line-cell-extractor #f #t #f #t))
(define extract-line-cell-south-east
(make-line-cell-extractor #f #f #t #t))
(define extract-line-cell-top-left extract-line-cell-south-east)
(define extract-line-cell-top-right extract-line-cell-south-west)
(define extract-line-cell-bottom-left extract-line-cell-south-east)
(define extract-line-cell-bottom-right extract-line-cell-south-west)
;; Self-tests
(define (box-drawing-tests!)
(run-tests
box-drawing
(test-equal? spec->line-thickness
(spec->line-thickness 'none)
0)
(test-equal? spec->line-thickness
(spec->line-thickness 'light)
1)
(test-equal? spec->line-thickness
(spec->line-thickness 'heavy)
2)
(test-equal? spec->line-type
(spec->line-type 'none)
0)
(test-equal? spec->line-type
(spec->line-type 'dashed)
1)
(test-equal? spec->line-type
(spec->line-type 'solid)
2)
(test-equal? spec->line-style
(spec->line-style 'solid) ;; light implied
#b1001)
(test-equal? spec->line-style
(spec->line-style 'dashed) ;; light implied
#b101)
(test-equal? spec->line-style
(spec->line-style 'light) ;; solid implied
#b1001)
(test-equal? spec->line-style
(spec->line-style 'heavy) ;; solid implied
#b1010)
(test-equal? combine-line-style
(combine-line-style #b1001 #b110)
#b1111)
(test-equal? spec->line-style
(spec->line-style '(solid light))
#b1001)
(test-equal? spec->line-style
(spec->line-style '(solid)) ;; light implied
#b1001)
(test-equal? spec->line-style
(spec->line-style '(dashed)) ;; light implied
#b101)
(test-equal? spec->line-style
(spec->line-style '(light)) ;; solid implied
#b1001)
(test-equal? spec->line-style
(spec->line-style '(heavy)) ;; solid implied
#b1010)
))
)

527
src/sgr-block.scm Normal file
View file

@ -0,0 +1,527 @@
;;
;; sgr-block.scm
;;
;; Represents a block of sgr-list rows.
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit sgr-block))
(module
sgr-block
(
sgr-list->sgr-block
sgr-block->string-list
sgr-block-width
sgr-block-height
sgr-line-render
sgr-block-render
sgr-block-vexpand
sgr-block-happend
sgr-block-tests!
)
(import scheme
(chicken base)
racket-kwargs
sgr-state
sgr-list
testing)
;; Converts sgr-list (possibly containing multiple lines) into a
;; sgr-block
(define* (sgr-list->sgr-block sl
(initial-state empty-sgr-state))
(let loop ((sl sl)
(state initial-state)
(row '())
(res '()))
(if (null? sl)
(reverse (if (null? row)
res
(cons (reverse row) res)))
(let ((token (car sl)))
(cond
((sgr-token-newline? token)
(loop (cdr sl)
state
(list state)
(cons (reverse row) res)))
((sgr-state? token)
(loop (cdr sl)
token
(if (or (null? row)
(not (sgr-state? (car row))))
(cons token row)
(cons token (cdr row)))
res))
(else
(loop (cdr sl)
state
(cons token row)
res)))))))
;; Creates a plain list of strings from given block
(define* (sgr-block->string-list sb #:reset-state (reset-state #f))
(map (lambda (sl)
(sgr-list->string sl
#:reset-state reset-state))
sb))
;; Returns the width of the longest SGR line in the block
(define (sgr-block-width sb)
(apply max (map sgr-list-length/stretch sb)))
;; Returns the number of SGR lines in the block
(define (sgr-block-height sb)
(length sb))
;; Creates filler for justification of SGR lines
(define (make-sgr-list-filler n)
(cons (make-string n #\space) n))
;; If first or last non-sgr-state token is space, removes it,
;; converts all spaces to glues. Removes unglue.
(define (sgr-line-preprocess sl justify?)
(let loop ((sl sl)
(res '())
(content? #f))
(if (null? sl)
(if (and (not (null? res))
(sgr-token-spaces? (car res)))
(reverse (cdr res))
(if (and (not (null? (cdr res)))
(sgr-state? (car res))
(sgr-token-spaces? (cadr res)))
(reverse (cons (car res)
(cddr res)))
(reverse res)))
(let ((t (car sl)))
(loop (cdr sl)
(if (sgr-token-spaces? t)
(if content?
(cons (if justify?
(cons "\t" 1)
t)
res)
res)
(if (sgr-token-unglue? t)
res
(cons t res)))
(or content?
(and (not (sgr-token-spaces? t))
(not (sgr-token-unglue? t)))))))))
;; Splits the remaining evenly prefering first and last
(define (compute-glue-lens num-glues remaining)
(let loop ((num num-glues)
(remaining remaining)
(res '()))
(if (eq? num 0)
(if (<= num 2)
(reverse res)
(let* ((res0 (reverse res))
(first-glue (car res0))
(rem-glues (cdr res0)))
(cons first-glue
(reverse rem-glues))))
(let ((len (quotient remaining num)))
(loop (sub1 num)
(- remaining len)
(cons len res))))))
;; Expands given SGR line to width by expanding spaces
(define (sgr-line-expand sl width)
(let* ((sll (sgr-list-length-w/o-glues sl))
(rem0 (- width sll)))
(if (>= rem0 0)
(let ((num-glues (sgr-list-num-glues sl)))
(if (> num-glues 0)
(let loop ((gluelens (compute-glue-lens
num-glues
rem0))
(sl sl)
(res '()))
(if (null? sl)
(reverse res)
(let ((tk (car sl)))
(if (sgr-token-glue? tk)
(loop (cdr gluelens)
(cdr sl)
(cons (cons (make-string (car gluelens))
(car gluelens))
res))
(loop gluelens
(cdr sl)
(cons (car sl) res))))))
;; Nothing to expand, fill-in remainder with spaces
(reverse (cons (cons (make-string rem0) rem0)
(reverse sl)))))
;; Nowhere to expand
sl)))
;; Returns justification type: left, right, center, justify
(define (analyze-sgr-line sl)
(if (null? sl)
'left
(let* ((tk0 (if (sgr-state? (car sl))
(if (null? (cdr sl))
#f
(cadr sl))
(car sl)))
(rsl (reverse sl))
(tkl (if (sgr-state? (car rsl))
(if (null? (cdr rsl))
#f
(cadr rsl))
(car rsl))))
(if (not tk0)
'left
(if (sgr-token-glue? tk0)
(if (sgr-token-glue? tkl)
'center
'right)
(if (sgr-token-unglue? tkl)
'justify
'left))))))
;; Extracts initial state
(define (sgr-line-extract-initial-state sl initial-state)
(if (null? sl)
(values sl initial-state)
(if (sgr-state? (car sl))
(values (cdr sl) (car sl))
(if (and (not (null? (cdr sl)))
(sgr-state? (cadr sl)))
(values (cons (car sl)
(cddr sl))
(cadr sl))
(values sl initial-state)))))
;; Finishes line handling right glue properly
(define (sgr-line-finish sl rightglue?)
(if (null? sl)
(if rightglue?
(list (cons "\t" 1))
'())
(if rightglue?
(if (sgr-token-spaces? (car sl))
(reverse (cons (cons "\t" 1)
(cdr sl)))
(reverse (cons (cons "\t" 1)
sl)))
(if (sgr-token-spaces? (car sl))
(reverse (cdr sl))
(reverse sl)))))
;; Returns a list of wrapped sgr-lines and final state
(define (sgr-line-wrap sl-arg width height initial-state-arg leftglue? rightglue? justify?)
(let-values (((sl initial-state) (sgr-line-extract-initial-state sl-arg initial-state-arg)))
(let loop ((sl sl)
(line (if leftglue?
(list (cons "\t" 1) initial-state)
(list initial-state)))
(content? #f)
(llen 0)
(res '())
(rheight 1)
(state initial-state))
(if (or (null? sl)
(and height
(> rheight height)))
(values (if content?
(reverse (cons (sgr-line-finish line rightglue?)
res))
(reverse res))
state)
(let ((tk (car sl)))
(if (sgr-state? tk)
;; State change, add and keep
(loop (cdr sl)
(cons tk line)
content?
llen
res
rheight
tk)
(let ((tklen (cdr tk)))
;; Spaces, texts
(if (sgr-token-spaces? tk)
;; Append only after content
(if (> (+ llen tklen) width)
;; Spaces force line wrap
(loop (cdr sl)
(if leftglue?
(list (cons "\t" 1) state)
(list state))
#f
0
(cons (sgr-line-finish line rightglue?)
res)
(add1 rheight)
state)
;; Spaces continue on the same line
(loop (cdr sl)
(if content?
(cons tk line)
line)
content?
(if content?
(+ llen tklen)
llen)
res
rheight
state))
(if (> (+ llen tklen) width)
;; Wrap word
(loop (cdr sl)
(if leftglue?
(list tk (cons "\t" 1) state)
(list tk state))
#t
tklen
(cons (sgr-line-finish line rightglue?)
res)
(add1 rheight)
state)
;; Keep on going
(loop (cdr sl)
(cons tk line)
#t
(+ llen tklen)
res
rheight
state))))))))))
;; Neutralizes line like sgr-list-neutralize, but returns final state
;; as well
(define (sgr-line-neutralize sl initial-state)
(let loop ((sl sl)
(res '())
(state initial-state))
(if (null? sl)
(values (reverse res)
state)
(let ((tk (car sl)))
(loop (cdr sl)
(cons (sgr-token-neutralize tk) res)
(if (sgr-state? tk)
tk
state))))))
;; Renders single SGR line as block. If width is unspecified, only
;; glues are removed. If height is unspecified, the block can have
;; any height. With width the line is rendered and glues are expanded
;; accordingly. With #:justify all spaces are glues and first and
;; last are removed before rendering. If there are no glues, the
;; width is set and the result is shorter than the specified width,
;; the line is simply right-padded with #\space.
(define* (sgr-line-render sl
#:width (width #f)
#:height (height #f)
#:initial-state (initial-state empty-sgr-state))
(if width
(let* ((alignment (analyze-sgr-line sl))
(sl (sgr-line-preprocess sl (eq? alignment 'justify))))
(let-values (((slw state)
(sgr-line-wrap sl
width height
initial-state
(memq alignment '(right center))
(memq alignment '(left center))
(eq? alignment 'justify))))
(values (map (lambda (sl)
(let ((sle (sgr-line-expand sl width)))
sle))
(if (null? slw)
(list '())
slw))
state)))
(let-values (((sln) (sgr-list-neutralize sl)))
(values sln initial-state))))
;; Renders all the lines and appends the resulting blocks
(define* (sgr-block-render sb
#:width (width (sgr-block-width sb))
#:height (height #f)
#:initial-state (initial-state empty-sgr-state))
(let loop ((sb sb)
(res '())
(total-height 0)
(state initial-state))
(if (or (null? sb)
(and height
(> total-height height)))
(let ((res (if (and height
(< total-height height))
(let floop ((res0 '())
(idx (if height
(- height total-height)
0)))
(if (= idx 0)
(cons res0 res)
(floop (cons (sgr-line-expand (list state (cons "\t" 1))
width)
res0)
(sub1 idx))))
res)))
(apply append (reverse res)))
(let ((sl (car sb)))
(let-values (((slb final-state)
(sgr-line-render sl
#:width width
#:height (if height
(- height total-height)
height)
#:initial-state state)))
(loop (cdr sb)
(cons slb
res)
(+ total-height
(sgr-block-height slb))
final-state
))))))
;; Expands to given height
(define (sgr-block-vexpand sb h)
(let ((sbh (sgr-block-height sb)))
(if (>= sbh h)
sb
(let* ((rsb (reverse sb))
(sbw (if (null? sb)
0
(sgr-list-length (car sb))))
(state (if (null? sb)
0
(sgr-list-last-state (car rsb))))
(filler (list state
(cons (make-string sbw)
sbw))))
(let loop ((rsb rsb)
(rh (- h sbh)))
(if (= 0 rh)
(reverse rsb)
(loop (cons filler rsb)
(sub1 rh))))))))
;; Appends blocks horizontally
(define (sgr-block-happend b0 . rest)
(if (null? rest)
b0
(let loop ((b0 b0)
(b1 (car rest))
(rres '()))
(if (null? b0)
(apply sgr-block-happend
(reverse rres)
(cdr rest))
(loop (cdr b0)
(cdr b1)
(cons (append (car b0)
(car b1))
rres))))))
;; Module self-tests
(define (sgr-block-tests!)
(run-tests
sgr-block
(test-equal? sgr-list->sgr-block
(sgr-list->sgr-block '(("Hello" . 5)))
'((("Hello" . 5))))
(test-equal? sgr-list->sgr-block
(sgr-list->sgr-block '(("Hello" . 5)
("\n" . 1)
("World" . 5)))
'((("Hello" . 5))
(0 ("World" . 5))))
(test-equal? sgr-list->sgr-block
(sgr-list->sgr-block '(1
("Hello" . 5)
("\n" . 1)
0
("World" . 5)))
'((1 ("Hello" . 5))
(0 ("World" . 5))))
(test-equal? sgr-block->string-list
(sgr-block->string-list '((("Hello" . 5))
(1 ("World" . 5))))
'("Hello"
"\x1b[1mWorld"))
(test-equal? sgr-block-width
(sgr-block-width '((("Hello" . 5))
(("Scheme" . 6)
(" " . 1)
("World!" . 6))))
13)
(test-equal? sgr-line-preprocess
(sgr-line-preprocess '(("Hello" . 5)
1
(" " . 3)
0
("World" . 5))
#t)
'(("Hello" . 5)
1
("\t" . 1)
0
("World" . 5)))
(test-equal? sgr-line-preprocess
(sgr-line-preprocess '((" " . 2)
("Hello" . 5)
1
(" " . 3)
0
("World" . 5)
(" " . 2))
#t)
'(("Hello" . 5)
1
("\t" . 1)
0
("World" . 5)))
(test-equal? compute-glue-lens
(compute-glue-lens 1 10)
'(10))
(test-equal? compute-glue-lens
(compute-glue-lens 2 10)
'(5 5))
(test-equal? compute-glue-lens
(compute-glue-lens 2 11)
'(5 6))
(test-equal? compute-glue-lens
(compute-glue-lens 3 11)
'(3 4 4))
(test-equal? compute-glue-lens
(compute-glue-lens 3 14)
'(4 5 5))
(test-equal? sgr-line-expand
(sgr-line-expand
'(("Hello" . 5) ("\t" . 1) ("World!" . 6))
20)
'(("Hello" . 5) (" " . 9) ("World!" . 6)))
))
)

67
src/sgr-cell.scm Normal file
View file

@ -0,0 +1,67 @@
;;
;; sgr-cell.scm
;;
;; Surface API for handling strings with SGR sequences as table cells.
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit sgr-cell))
(module
sgr-cell
(
string->sgr-cell
sgr-cell-width
sgr-cell-height
sgr-cell-min-width
sgr-cell-render
sgr-cell-vexpand
)
(import scheme
racket-kwargs
sgr-state
sgr-list
sgr-block)
(define* (string->sgr-cell str (initial-state empty-sgr-state))
(let ((cell0 (sgr-list->sgr-block
(string->sgr-list/words str initial-state)
initial-state)))
(if (null? cell0)
(list (list initial-state))
cell0)))
(define sgr-cell-width sgr-block-width)
(define sgr-cell-height sgr-block-height)
(define (sgr-cell-min-width sc)
(apply max (cons 0 (map sgr-list-min-width sc))))
(define (sgr-cell-render . args)
(apply sgr-block-render args))
(define sgr-cell-vexpand sgr-block-vexpand)
)

485
src/sgr-list.scm Normal file
View file

@ -0,0 +1,485 @@
;;
;; sgr-list.scm
;;
;; Intermediate representation of strings with SGR state changes.
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit sgr-list))
(module
sgr-list
(
string->sgr-list
string->sgr-list/words
sgr-list->string
sgr-list-length
sgr-list-length/stretch
sgr-list-length-w/o-glues
sgr-token-spaces?
sgr-token-glue?
sgr-token-newline?
sgr-token-unglue?
sgr-token-neutralize
sgr-list-neutralize
sgr-list-num-glues
sgr-list-min-width
sgr-list-last-state
sgr-list-tests!
)
(import scheme
(chicken base)
(chicken string)
racket-kwargs
sgr-state
testing
util-utf8)
;; Converts given string into a list of string parts and sgr-states
(define* (string->sgr-list str
(initial-state empty-sgr-state))
(let ((src-len (string-length str)))
(let loop ((src-idx 0)
(token-start 0)
(token-end 0)
(token-pos 0)
(state initial-state)
(res '()))
(if (>= src-idx src-len)
(reverse
(if (= token-start token-end)
res
(cons (cons (substring str token-start token-end)
token-pos)
res)))
(let ((ch (string-ref str src-idx)))
(cond
((eq? ch #\x1b)
(let-values (((sgr-parsed next-idx)
(parse-csi-sgr-sequence+pos str src-idx state)))
(let ((token (update-sgr-state-from-list state sgr-parsed)))
(loop next-idx
next-idx
next-idx
0
token
(cons token
(if (= token-start token-end)
res
(cons (cons (substring str token-start token-end)
token-pos)
res)))))))
(else
(let ((next-idx (utf8-string-next-char str src-idx)))
(loop next-idx
token-start
next-idx
(add1 token-pos)
state
res)))))))))
;; Converts given string into a list of string parts and sgr-states,
;; handling continuous whitespace as separate tokens.
(define* (string->sgr-list/words str
(initial-state empty-sgr-state))
(let ((src-len (string-length str)))
(let loop ((src-idx 0)
(token-start 0)
(token-end 0)
(token-pos 0)
(state initial-state)
(space? #f)
(res '()))
(if (>= src-idx src-len)
(reverse
(if (= token-start token-end)
res
(cons (cons (substring str token-start token-end)
token-pos)
res)))
(let ((ch (string-ref str src-idx)))
(cond
((eq? ch #\x1b)
(let-values (((sgr-parsed next-idx)
(parse-csi-sgr-sequence+pos str src-idx state)))
(let ((token (update-sgr-state-from-list state sgr-parsed)))
(loop next-idx
next-idx
next-idx
0
token
#f
(cons token
(if (= token-start token-end)
res
(cons (cons (substring str token-start token-end)
token-pos)
res)))))))
((eq? ch #\backspace)
(let ((next-idx (add1 src-idx)))
(loop next-idx
next-idx
next-idx
0
state
#f
(cons (cons "\b" 1)
(if (= token-start token-end)
res
(cons (cons (substring str token-start token-end)
token-pos)
res))))))
((eq? ch #\newline)
(let ((next-idx (add1 src-idx)))
(loop next-idx
next-idx
next-idx
0
state
#f
(cons (cons "\n" 1)
(if (= token-start token-end)
res
(cons (cons (substring str token-start token-end)
token-pos)
res))))))
((memq ch '(#\space #\tab))
(let ((next-idx (add1 src-idx)))
(loop next-idx
(if space?
token-start
src-idx)
next-idx
(if space?
(add1 token-pos)
1)
state
#t
(if (and (not space?)
(> token-end token-start))
(cons (cons (substring str token-start token-end)
token-pos)
res)
res))))
(else
(let ((next-idx (utf8-string-next-char str src-idx)))
(loop next-idx
(if space?
src-idx
token-start)
next-idx
(if space?
1
(add1 token-pos))
state
#f
(if space?
(cons (cons (substring str token-start token-end)
token-pos)
res)
res))))))))))
;; Converts a SGR list into a single string
(define* (sgr-list->string lst
(initial-state empty-sgr-state)
#:reset-state (reset-state #f))
(let loop ((lst lst)
(res '())
(state initial-state))
(if (null? lst)
(string-intersperse
(reverse (if reset-state
(cons reset-state res)
res))
"")
(let ((token (car lst)))
(if (sgr-state? token)
(loop (cdr lst)
(cons (sgr-state-change->string state token) res)
token)
(loop (cdr lst)
(cons (car token) res)
state))))))
;; Predicate for SGR list tokens
(define (sgr-list-token? v)
(and (pair? v)
(string? (car v))
(fixnum? (cdr v))))
;; Returns the length of all utf8 strings in the sgr-list
(define (sgr-list-length sl)
(foldl
(lambda (acc tk)
(if (sgr-list-token? tk)
(+ acc (cdr tk))
acc))
0 sl))
;; Returns the length of all utf8 strings without glues. Glues must
;; have at least width 1 if they are not the first or last
;; token. Those can have zero length. All spaces have width of
;; 1. Used for wrapping blocks.
(define (sgr-list-length/stretch sl)
(let loop ((sl sl)
(len 0)
(seen-string? #f)
(last-space? #f))
(if (null? sl)
(if last-space?
(sub1 len)
len)
(let* ((tk (car sl))
(is-spaces? (sgr-token-spaces? tk))
(is-string? (and (not (sgr-state? tk))
(not is-spaces?))))
(loop (cdr sl)
(cond ((sgr-token-spaces? tk)
(if seen-string?
(add1 len)
len))
((sgr-state? tk)
len)
(else
(+ len (cdr tk))))
(or seen-string?
is-string?)
(if is-string?
#f
(if is-spaces?
#t
last-space?)))))))
;; Returns length without glues
(define (sgr-list-length-w/o-glues sl)
(foldl (lambda (acc tk)
(if (or (sgr-state? tk)
(sgr-token-glue? tk)
(sgr-token-unglue? tk))
acc
(+ acc (cdr tk))))
0 sl))
;; Predicate for string token containing only spaces and tabs
(define (sgr-token-spaces? t)
(and (pair? t)
(string? (car t))
(let loop ((i 0))
(if (= i (cdr t))
#t
(if (memq (string-ref (car t) i) '(#\space #\tab))
(loop (add1 i))
#f)))))
;; Predicate for string token with only spaces and at least one tab
(define (sgr-token-glue? t)
(and (pair? t)
(string? (car t))
(let loop ((i 0)
(glue #f))
(if (= i (cdr t))
glue
(let ((ch (string-ref (car t) i)))
(if (eq? ch #\tab)
(loop (add1 i) #t)
(if (eq? ch #\space)
(loop (add1 i) glue)
#f)))))))
;; Predicate for string containing only a newline
(define (sgr-token-newline? t)
(and (pair? t)
(string? (car t))
(eq? (cdr t) 1)
(eq? (string-ref (car t) 0) #\newline)))
;; Predicate for unglue (used for justify)
(define (sgr-token-unglue? t)
(and (pair? t)
(string? (car t))
(eq? (cdr t) 1)
(eq? (string-ref (car t) 0) #\backspace)))
;; Replaces all occurences of #\tab with #\space and removes trailing
;; #\backspace
(define (sgr-token-neutralize t)
(if (and (pair? t)
(string? (car t)))
(let* ((str (string-copy (car t)))
(len (string-length str)))
(if (> len 0)
(let loop ((idx 0))
(if (= idx len)
(if (eq? (string-ref str (sub1 len)) #\backspace)
(cons (substring str 0 (sub1 len))
(sub1 len))
(cons str len))
(let ((ch (string-ref str idx)))
(when (eq? ch #\tab)
(string-set! str idx #\space))
(loop (add1 idx)))))
t))))
;; Neutralizes whole SGR list
(define (sgr-list-neutralize sl)
(map sgr-token-neutralize sl))
;; Returns the number of glues in given SGR list
(define (sgr-list-num-glues sl)
(foldl (lambda (acc t)
(if (sgr-token-glue? t)
(add1 acc)
acc))
0 sl))
;; Returns the longest word
(define (sgr-list-min-width sl)
(let loop ((sl sl)
(res 0))
(if (null? sl)
res
(let ((tk (car sl)))
(loop (cdr sl)
(if (sgr-state? tk)
res
(max res (cdr tk))))))))
;; Returns the last SGR state in given sgr-list
(define* (sgr-list-last-state sl (initial-state empty-sgr-state))
(let loop ((sl sl)
(state initial-state))
(if (null? sl)
state
(loop (cdr sl)
(if (sgr-state? (car sl))
(car sl)
state)))))
;; Module self-tests
(define (sgr-list-tests!)
(run-tests
sgr-list
(test-equal? sgr-list->string
(sgr-list->string '(("Hello" . 5)))
"Hello")
(test-true sgr-list-token?
(sgr-list-token? '("Hello" . 5)))
(test-false sgr-list-token?
(sgr-list-token? empty-sgr-state))
(test-false sgr-list-token?
(sgr-list-token? (set-sgr-state-foreground
empty-sgr-state
(make-sgr-truecolor 1 2 3))))
(test-equal? sgr-list-length
(sgr-list-length '(("Hello" . 5)))
5)
(test-true sgr-token-spaces?
(sgr-token-spaces? '(" " . 5)))
(test-true sgr-token-spaces?
(sgr-token-spaces? '(" \x09 " . 5)))
(test-false sgr-token-spaces?
(sgr-token-spaces? '(" x " . 5)))
(test-true sgr-token-newline?
(sgr-token-newline? '("\n" . 1)))
(test-false sgr-token-newline?
(sgr-token-newline? '("\na" . 2)))
(test-false sgr-token-newline?
(sgr-token-newline? '("x" . 1)))
(test-true sgr-token-glue?
(sgr-token-glue? '(" \t " . 5)))
(test-true sgr-token-glue?
(sgr-token-glue? '("\t" . 1)))
(test-false sgr-token-glue?
(sgr-token-glue? '(" " . 5)))
(test-false sgr-token-glue?
(sgr-token-glue? '(" x " . 5)))
(test-equal? string->sgr-list
(string->sgr-list "Hello")
'(("Hello" . 5)))
(test-equal? string->sgr-list
(string->sgr-list "\x1b[1mHello \x1b[0mWorld!")
'(1 ("Hello " . 6) 0 ("World!" . 6)))
(test-equal? string->sgr-list/words
(string->sgr-list/words "Hello World!")
'(("Hello" . 5)
(" " . 1)
("World!" . 6)))
(test-equal? string->sgr-list/words
(string->sgr-list/words "\x1b[1mHello \x1b[0mWorld!")
'(1
("Hello" . 5)
(" " . 1)
0
("World!" . 6)))
(test-equal? string->sgr-list/words
(string->sgr-list/words "Hello World!\b")
'(("Hello" . 5)
(" " . 1)
("World!" . 6)
("\b" . 1)))
(test-equal? string->sgr-list/words
(string->sgr-list/words "Hello\nWorld!")
'(("Hello" . 5)
("\n" . 1)
("World!" . 6)))
(test-equal? sgr-list-length/stretch
(sgr-list-length/stretch
'(("Hello" . 5)))
5)
(test-equal? sgr-list-length/stretch
(sgr-list-length/stretch
'(("Hello" . 5) (" " . 4)))
5)
(test-equal? sgr-list-length/stretch
(sgr-list-length/stretch
'(("Hello" . 5) (" " . 4) 1))
5)
(test-equal? sgr-list-length/stretch
(sgr-list-length/stretch
'(1 (" " . 4) ("Hello" . 5) (" " . 4) 1))
5)
(test-equal? sgr-list-length/stretch
(sgr-list-length/stretch
'(("Hello" . 5) (" " . 4) ("World" . 5)))
11)
(test-equal? sgr-list-length-w/o-glues
(sgr-list-length-w/o-glues
'(("Hello" . 5) (" \t " . 5) ("World" . 5)))
10)
(test-true sgr-token-unglue?
(sgr-token-unglue? '("\b" . 1)))
(test-false sgr-token-unglue?
(sgr-token-unglue? '(" \b" . 2)))
(test-equal? sgr-token-neutralize
(sgr-token-neutralize '("Hello\b" . 6))
'("Hello" . 5))
))
)

756
src/sgr-state.scm Normal file
View file

@ -0,0 +1,756 @@
;;
;; sgr-state.scm
;;
;; ECMA-48 Set Graphics Rendition state management.
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit sgr-state))
(module
sgr-state
(
sgr-color-valid?
sgr-truecolor?
make-sgr-truecolor
split-sgr-truecolor
make-sgr-state
empty-sgr-state
sgr-state?
sgr-state-intensity
set-sgr-state-intensity
sgr-state-italic
set-sgr-state-italic
sgr-state-underline
set-sgr-state-underline
sgr-state-blink
set-sgr-state-blink
sgr-state-reverse
set-sgr-state-reverse
sgr-state-crossed
set-sgr-state-crossed
sgr-state-foreground
sgr-state-background
set-sgr-state-foreground
set-sgr-state-background
sgr-state-change->string
parse-csi-sgr-sequence+pos
parse-csi-sgr-sequence
update-sgr-state-from-list
update-sgr-state-from-string
sgr-state-tests!
)
(import scheme
(chicken base)
(chicken bitwise)
(chicken string)
testing
racket-kwargs)
;; Checks whether this is indexed color or truecolor representation
;; validating the bit mask as well
(define (sgr-color-valid? c)
(or (< c 256)
(and (>= c #x1000000)
(<= c #x1ffffff))))
;; Returns true if given color is truecolor representation (assumes
;; #x1000000 bit set)
(define (sgr-truecolor? c)
(> c 255))
;; Creates valid truecolor representation
(define (make-sgr-truecolor r g b)
(bitwise-ior #x1000000
(arithmetic-shift (bitwise-and r 255) 16)
(arithmetic-shift (bitwise-and g 255) 8)
(bitwise-and b 255)))
;; Returns the separated RGB alues from SGR truecolor representation
(define (split-sgr-truecolor c)
(values (bitwise-and (arithmetic-shift c -16) 255)
(bitwise-and (arithmetic-shift c -8) 255)
(bitwise-and c 255)))
;; Creates empty SGR state
(define (make-sgr-state)
0)
;; Constant representing empty (default) SGR state
(define empty-sgr-state 0)
;; Returns #t if this is valid SGR state representation
(define (sgr-state? v)
(or (and (fixnum? v)
(= (bitwise-and v (bitwise-not sgr-state-bit-mask)) 0))
(and (pair? v)
(fixnum? (car v))
(= (bitwise-and v (bitwise-not sgr-state-bit-mask)) 0)
(pair? (cdr v))
(or (not (cadr v))
(and (fixnum? (cadr v))
(= (bitwise-and (cadr v) (bitwise-not #x1ffffff)) 0)))
(or (not (cddr v))
(and (fixnum? (cddr v))
(= (bitwise-and (cddr v) (bitwise-not #x1ffffff)) 0))))))
;; Basic SGR state is just a number, extended state is a pair of
;; number and pair of fg/bg truecolors.
(define (sgr-state-bits s)
(if (pair? s)
(car s)
s))
;; Sets the bits part of given SGR state, if it is not a pair, just
;; returns the new bits as state value
(define (set-sgr-state-bits s b)
(if (pair? s)
(cons b (cdr s))
b))
;; Computes positional mask, creates getter and setter for given
;; numeric attribute at given bit-offset
(define-syntax define-sgr-num-attribute
(syntax-rules ()
((_ bit-offset bit-size mask getter setter)
(begin
(define mask (arithmetic-shift (sub1 (arithmetic-shift 1 bit-size))
bit-offset))
(define (getter s)
(let ((v (arithmetic-shift
(bitwise-and (sgr-state-bits s) mask)
(- bit-offset))))
(if (> v 0) v #f)))
(define (setter s vf)
(let ((v (case vf
((#f) 0)
((#t) 1)
(else vf)))
(b (sgr-state-bits s)))
(set-sgr-state-bits
s
(bitwise-ior
(bitwise-and (arithmetic-shift v bit-offset) mask)
(bitwise-and (bitwise-not mask) b)))))))))
;; Wrapper for defining all attributes iteratively, incrementing
;; bit-offset as needed
(define-syntax define-sgr-num-attribute+
(syntax-rules ()
((_ bit-offset total bit-size mask getter setter remaining ...)
(begin
(define-sgr-num-attribute bit-offset bit-size mask getter setter)
(define-sgr-num-attribute+ (+ bit-offset bit-size) total remaining ...)))
((_ bit-offset total)
(define total bit-offset))))
;; Wrapper for definer starting at bit offset 0
(define-syntax define-sgr-num-attributes
(syntax-rules ()
((_ total all ...)
(begin
(define-sgr-num-attribute+ 0 total all ...)))))
;; Define simple (numeric) attributes
(define-sgr-num-attributes
sgr-state-num-bits
;; 1: bold, 2: half-bright
2 sgr-state-intensity-mask sgr-state-intensity set-sgr-state-intensity
1 sgr-state-italic-mask sgr-state-italic set-sgr-state-italic
;; 1: single, 2: double
2 sgr-state-underline-mask sgr-state-underline set-sgr-state-underline
1 sgr-state-blink-mask sgr-state-blink set-sgr-state-blink
1 sgr-state-reverse-mask sgr-state-reverse set-sgr-state-reverse
1 sgr-state-crossed-mask sgr-state-crossed set-sgr-state-crossed
9 sgr-state-fg256-mask sgr-state-fg256 set-sgr-state-fg256
9 sgr-state-bg256-mask sgr-state-bg256 set-sgr-state-bg256)
;; Used in sgr-state? predicate
(define sgr-state-bit-mask (sub1 (arithmetic-shift 1 sgr-state-num-bits)))
;; Returns true, color 0-255 or RGB triplet with bit 17 set to 1 if
;; truecolor
(define (sgr-state-foreground s)
(if (pair? s)
(cadr s)
(let ((c+ (sgr-state-fg256 s)))
(if c+
(bitwise-and c+ 255)
#f))))
;; Returns true, color 0-255 or RGB triplet with bit 17 set to 1 if
;; truecolor
(define (sgr-state-background s)
(if (pair? s)
(cddr s)
(let ((c+ (sgr-state-bg256 s)))
(if c+
(bitwise-and c+ 255)
#f))))
;; Sets foreground color, possibly compacting the SGR state, #f is
;; valid value representing default color
(define (set-sgr-state-foreground s c)
(if (pair? s)
;; Initially it is a pair
(if c
;; Setting some foreground
(if (< c 256)
;; Foreground does not need pair
(if (and (cddr s)
(>= (cddr s) 256))
;; Background needs a pair, set foreground to bits
;; and cadr
(cons (set-sgr-state-fg256 (car s)
(bitwise-ior c 256))
(cons c (cddr s)))
;; Neither background nor foreground need pair,
;; switch to bit-only representation
(set-sgr-state-fg256 (car s) (bitwise-ior c 256)))
;; Foreground needs pair, just update cadr and reset
;; bits
(cons (set-sgr-state-fg256 (car s) 0)
(cons (bitwise-ior c #x1000000)
(cddr s))))
;; Removing foreground
(if (and (cddr s)
(>= (cddr s) 256))
;; Still pair needed for background, store both in bits
;; and cadr
(cons (set-sgr-state-fg256 (car s) 0)
(cons #f (cddr s)))
;; Just reset, no pair for background needed
(set-sgr-state-fg256 s 0)))
;; Initially it is a bit representation
(if c
;; Setting foreground
(if (< c 256)
;; Just set the bits
(set-sgr-state-fg256 s (bitwise-ior c 256))
;; Create pair representation, store exclusively in cadr
(cons (set-sgr-state-fg256 s 0)
(cons (bitwise-ior c #x1000000) #f)))
;; Just clear the bits
(set-sgr-state-fg256 s 0))))
;; Sets background color, possibly compacting the SGR state, #f is
;; valid value representing default color
(define (set-sgr-state-background s c)
(if (pair? s)
;; Initially it is a pair
(if c
;; Setting some background
(if (< c 256)
;; Background does not need pair
(if (and (cadr s)
(>= (cadr s) 256))
;; Foreground needs a pair, set background to bits
;; and cddr
(cons (set-sgr-state-bg256 (car s)
(bitwise-ior c 256))
(cons (cadr s) c))
;; Neither background nor foreground need pair,
;; switch to bit-only representation
(set-sgr-state-bg256 (car s) (bitwise-ior c 256)))
;; Background needs pair, just update cddr and reset
;; bits
(cons (set-sgr-state-bg256 (car s) 0)
(cons (cadr s)
(bitwise-ior c #x1000000))))
;; Removing background
(if (and (cadr s)
(>= (cadr s) 256))
;; Still pair needed for foreground, store both in bits
;; and cddr
(cons (set-sgr-state-bg256 (car s) 0)
(cons (cadr s) #f))
;; Just reset, no pair for foreground needed
(set-sgr-state-bg256 s 0)))
;; Initially it is a bit representation
(if c
;; Setting background
(if (< c 256)
;; Just set the bits
(set-sgr-state-bg256 s (bitwise-ior c 256))
;; Create pair representation, store exclusively in cddr
(cons (set-sgr-state-bg256 s 0)
(cons #f (bitwise-ior c #x1000000))))
;; Just clear the bits
(set-sgr-state-bg256 s 0))))
;; Prepends required CSI SGR sequence for given color change
(define (sgr-prepend-color-change lst color background?)
(let ((off (if background? 10 0)))
(cond ((eq? color #f)
(cons (+ off 39) lst))
((< color 8)
(cons (+ off 30 color) lst))
((< color 16)
(cons (+ off 82 color) lst))
((< color 256)
(cons color
(cons 5
(cons (+ 38 off)
lst))))
(else
(let-values (((r g b) (split-sgr-truecolor color)))
(cons b
(cons g
(cons r
(cons 2
(cons (+ 38 off)
lst))))))))))
;; Produces an CSI SGR sequence to change from the orig state to the
;; next state.
(define (sgr-state-change->string orig next)
(cond
((equal? orig next)
"")
((equal? next empty-sgr-state)
"\x1b[0m")
(else
(let* ((cs0 '())
(cs1 (if (eq? (sgr-state-intensity orig)
(sgr-state-intensity next))
cs0
(cons (case (sgr-state-intensity next)
((1) 1)
((2) 2)
(else 22))
cs0)))
(cs2 (if (eq? (sgr-state-italic orig)
(sgr-state-italic next))
cs1
(cons (if (sgr-state-italic next)
3
23)
cs1)))
(cs3 (if (eq? (sgr-state-underline orig)
(sgr-state-underline next))
cs2
(cons (case (sgr-state-underline next)
((1) 4)
((2) 21)
(else 24))
cs2)))
(cs4 (if (eq? (sgr-state-blink orig)
(sgr-state-blink next))
cs3
(cons (if (sgr-state-blink next)
5
25)
cs3)))
(cs5 (if (eq? (sgr-state-reverse orig)
(sgr-state-reverse next))
cs4
(cons (if (sgr-state-reverse next)
7
27)
cs4)))
(cs6 (if (eq? (sgr-state-crossed orig)
(sgr-state-crossed next))
cs5
(cons (if (sgr-state-crossed next)
9
29)
cs5)))
(cs7 (if (eq? (sgr-state-foreground orig)
(sgr-state-foreground next))
cs6
(sgr-prepend-color-change cs6 (sgr-state-foreground next) #f)))
(cs8 (if (eq? (sgr-state-background orig)
(sgr-state-background next))
cs7
(sgr-prepend-color-change cs7 (sgr-state-background next) #t)))
(cs cs8))
(string-append
"\x1b["
(string-intersperse
(map number->string (reverse cs))
";")
"m")))))
;; Parses a CSI SGR sequence. Returns a list of numeric arguments in
;; the same order as they are present in the sequence, prepends the
;; terminating character (sequence type). Returns position just after
;; parsed sequence as second value.
(define (parse-csi-sgr-sequence+pos str . ps)
(let ((pos (if (null? ps) 0 (car ps)))
(len (string-length str)))
(if (or (>= pos len)
(not (eq? (string-ref str pos) #\x1b)))
(values '() 0)
(if (or (>= (add1 pos) len)
(not (eq? (string-ref str (add1 pos)) #\[)))
(values '() 1)
(let loop ((pos (+ pos 2))
(res '())
(pending #f))
(if (>= pos len)
(values '() pos)
(let ((ch (string-ref str pos)))
(cond ((and (char>=? ch #\0)
(char<=? ch #\9))
(let ((digit (- (char->integer ch)
(char->integer #\0))))
(loop (add1 pos)
res
(if pending
(+ (* pending 10) digit)
digit))))
((eq? ch #\;)
(loop (add1 pos)
(cons pending res)
#f))
(else
(values (cons ch
(reverse (if pending
(cons pending res)
res)))
(add1 pos)))))))))))
;; Parses CSI SGR sequence and returns the sequence list only.
(define (parse-csi-sgr-sequence str . ps)
(let-values (((lst pos)
(parse-csi-sgr-sequence+pos str (if (null? ps) 0 (car ps)))))
lst))
;; Parses 256 and 16M color sequences
(define (parse-extended-sgr-color lst state background?)
(if (null? lst)
(list lst state)
(case (car lst)
((5)
(if (null? (cdr lst))
(list (cdr lst) state)
(list (cddr lst)
(if background?
(set-sgr-state-background state (cadr lst))
(set-sgr-state-foreground state (cadr lst))))))
((2)
(if (null? (cdr lst)) ; R?
(list (cdr lst) state)
(if (null? (cddr lst)) ; G?
(list (cddr lst) state)
(if (null? (cdddr lst)) ; B?
(list (cdddr lst) state)
(list (cddddr lst)
(let ((c (make-sgr-truecolor (cadr lst)
(caddr lst)
(cadddr lst))))
(if background?
(set-sgr-state-background state c)
(set-sgr-state-foreground state c))))))))
(else
(list (cdr lst) state)))))
;; Update given state by parsed SGR state list
(define* (update-sgr-state-from-list state lst
#:default (default empty-sgr-state))
(if (or (null? lst)
(not (eq? (car lst) #\m)))
state
(let loop ((lst (cdr lst))
(state state))
(if (null? lst)
state
(case (car lst)
((0)
(loop (cdr lst)
default))
((1)
(loop (cdr lst)
(set-sgr-state-intensity state 1)))
((2)
(loop (cdr lst)
(set-sgr-state-intensity state 2)))
((3)
(loop (cdr lst)
(set-sgr-state-intensity state #t)))
((4)
(loop (cdr lst)
(set-sgr-state-underline state 1)))
((5)
(loop (cdr lst)
(set-sgr-state-blink state #t)))
((7)
(loop (cdr lst)
(set-sgr-state-reverse state #t)))
((9)
(loop (cdr lst)
(set-sgr-state-crossed state #t)))
((21)
(loop (cdr lst)
(set-sgr-state-underline state 2)))
((22)
(loop (cdr lst)
(set-sgr-state-intensity state #f)))
((23)
(loop (cdr lst)
(set-sgr-state-italic state #f)))
((24)
(loop (cdr lst)
(set-sgr-state-underline state #f)))
((25)
(loop (cdr lst)
(set-sgr-state-blink state #f)))
((27)
(loop (cdr lst)
(set-sgr-state-reverse state #f)))
((29)
(loop (cdr lst)
(set-sgr-state-crossed state #f)))
((30 31 32 33 34 35 36 37)
(loop (cdr lst)
(set-sgr-state-foreground state (- (car lst) 30))))
((38)
(apply loop (parse-extended-sgr-color (cdr lst) state #f)))
((39)
(loop (cdr lst)
(set-sgr-state-foreground state #f)))
((40 41 42 43 44 45 46 47)
(loop (cdr lst)
(set-sgr-state-background state (- (car lst) 40))))
((48)
(apply loop (parse-extended-sgr-color (cdr lst) state #f)))
((49)
(loop (cdr lst)
(set-sgr-state-background state #f)))
((90 91 92 93 94 95 96 97)
(loop (cdr lst)
(set-sgr-state-foreground state (- (car lst) 82))))
((100 101 102 103 104 105 106 107)
(loop (cdr lst)
(set-sgr-state-background state (- (car lst) 92))))
(else
(loop (cdr lst)
state)))))))
;; Updates given SGR state based on ESC sequence(s) given in the
;; string. Optional starting position can be given. Processes only
;; one sequence.
(define* (update-sgr-state-from-string state str
(pos 0)
#:default (default empty-sgr-state))
(let ((lst (parse-csi-sgr-sequence str pos)))
(update-sgr-state-from-list state lst #:default default)))
;; Module self-tests
(define (sgr-state-tests!)
(run-tests
sgr-state
(test-true sgr-color-valid?
(sgr-color-valid? 1))
(test-true sgr-color-valid?
(sgr-color-valid? #x1234567))
(test-false sgr-color-valid?
(sgr-color-valid? 1000))
(test-false sgr-color-valid?
(sgr-color-valid? #x2000000))
(test-false sgr-truecolor?
(sgr-truecolor? 123))
(test-true sgr-truecolor?
(sgr-truecolor? #x1234567))
(test-equal? make-sgr-truecolor
(make-sgr-truecolor 1 2 3)
#x1010203)
(test-equal? split-sgr-truecolor
(call-with-values
(lambda ()
(split-sgr-truecolor #x1112233))
list)
'(17 34 51))
(test-equal? sgr-state-intensity-mask
sgr-state-intensity-mask
3)
(test-equal? sgr-state-italic-mask
sgr-state-italic-mask
4)
(test-equal? sgr-state-italic
(sgr-state-italic 4)
1)
(test-equal? set-sgr-state-italic
(set-sgr-state-italic 0 1)
4)
(test-equal? set-sgr-state-italic
(set-sgr-state-italic 15 0)
11)
(test-equal? set-sgr-state-italic
(set-sgr-state-italic 15 #f)
11)
(test-equal? set-sgr-state-foreground
(set-sgr-state-foreground 0 255)
(set-sgr-state-fg256 0 511))
(test-equal? set-sgr-state-foreground
(set-sgr-state-foreground 0 256)
(cons 0 (cons #x1000100 #f)))
(test-equal? set-sgr-state-foreground
(set-sgr-state-foreground
(set-sgr-state-foreground 0 256) 255)
(set-sgr-state-fg256 0 511))
(test-equal? sgr-state-change->string
(sgr-state-change->string empty-sgr-state
(set-sgr-state-intensity empty-sgr-state 1))
"\x1b[1m")
(test-equal? sgr-state-change->string
(sgr-state-change->string (set-sgr-state-intensity
(set-sgr-state-italic
empty-sgr-state 1) 1)
empty-sgr-state)
"\x1b[0m")
(test-equal? sgr-state-change->string
(sgr-state-change->string (set-sgr-state-intensity
(set-sgr-state-italic
empty-sgr-state #t) 2)
(set-sgr-state-italic
empty-sgr-state #t))
"\x1b[22m")
(test-equal? sgr-state-change->string
(sgr-state-change->string (set-sgr-state-intensity
(set-sgr-state-italic
empty-sgr-state #t) 2)
(set-sgr-state-intensity
empty-sgr-state 2))
"\x1b[23m")
(test-equal? sgr-state-change->string
(sgr-state-change->string (set-sgr-state-intensity
empty-sgr-state 2)
(set-sgr-state-intensity
(set-sgr-state-italic
empty-sgr-state #t) 2))
"\x1b[3m")
(test-equal? sgr-state-change->string
(sgr-state-change->string (set-sgr-state-intensity
empty-sgr-state 2)
(set-sgr-state-intensity
(set-sgr-state-underline
empty-sgr-state 2) 2))
"\x1b[21m")
(test-equal? sgr-state-change->string
(sgr-state-change->string (set-sgr-state-intensity
empty-sgr-state 2)
(set-sgr-state-intensity
(set-sgr-state-blink
empty-sgr-state #t) 2))
"\x1b[5m")
(test-equal? sgr-state-change->string
(sgr-state-change->string (set-sgr-state-intensity
empty-sgr-state 2)
(set-sgr-state-intensity
(set-sgr-state-reverse
empty-sgr-state #t) 2))
"\x1b[7m")
(test-equal? sgr-state-change->string
(sgr-state-change->string (set-sgr-state-intensity
empty-sgr-state 2)
(set-sgr-state-intensity
(set-sgr-state-crossed
empty-sgr-state #t) 2))
"\x1b[9m")
(test-equal? sgr-state-change->string
(sgr-state-change->string empty-sgr-state
(set-sgr-state-foreground
empty-sgr-state
5))
"\x1b[35m")
(test-equal? sgr-state-change->string
(sgr-state-change->string empty-sgr-state
(set-sgr-state-foreground
empty-sgr-state
15))
"\x1b[97m")
(test-equal? sgr-state-change->string
(sgr-state-change->string empty-sgr-state
(set-sgr-state-foreground
empty-sgr-state
115))
"\x1b[38;5;115m")
(test-equal? sgr-state-change->string
(sgr-state-change->string empty-sgr-state
(set-sgr-state-foreground
empty-sgr-state
#x1112233))
"\x1b[38;2;17;34;51m")
(test-equal? sgr-state-change->string
(sgr-state-change->string empty-sgr-state
(set-sgr-state-background
empty-sgr-state
5))
"\x1b[45m")
(test-equal? sgr-state-change->string
(sgr-state-change->string empty-sgr-state
(set-sgr-state-background
empty-sgr-state
15))
"\x1b[107m")
(test-equal? sgr-state-change->string
(sgr-state-change->string empty-sgr-state
(set-sgr-state-background
empty-sgr-state
115))
"\x1b[48;5;115m")
(test-equal? sgr-state-change->string
(sgr-state-change->string empty-sgr-state
(set-sgr-state-background
empty-sgr-state
#x1112233))
"\x1b[48;2;17;34;51m")
(test-equal? parse-csi-sgr-sequence
(parse-csi-sgr-sequence "\x1b[38;2;1;2;3m")
'(#\m 38 2 1 2 3))
(test-equal? update-sgr-state-from-string
(update-sgr-state-from-string
empty-sgr-state
"\x1b[1m")
1)
(test-equal? update-sgr-state-from-string
(update-sgr-state-from-string
empty-sgr-state
"\x1b[31m")
(set-sgr-state-foreground 0 1))
(test-equal? update-sgr-state-from-string
(update-sgr-state-from-string
empty-sgr-state
"\x1b[91m")
(set-sgr-state-foreground 0 9))
(test-equal? update-sgr-state-from-string
(update-sgr-state-from-string
empty-sgr-state
"\x1b[38;2;17;34;51m")
(set-sgr-state-foreground empty-sgr-state #x1112233))
))
)

240
src/table-border.scm Normal file
View file

@ -0,0 +1,240 @@
;;
;; table-border.scm
;;
;; Table border rendering.
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit table-border))
(module
table-border
(
table-rows-border
table-border-vertical
table-border-between-rows?
table-col-separators?
table-row-merge
)
(import scheme
(chicken base)
racket-kwargs
box-drawing
util-utf8
sgr-block)
;; Vertical separator char (horizontal lines)
(define* (table-border-char-vertical top-borders bottom-borders (unicode? #t))
(let* ((top-south (line-cell-south top-borders))
(bottom-north (line-cell-north bottom-borders))
(combined (combine-line-style top-south bottom-north))
(cell (make-straight-horizontal-line-cell* combined)))
(if unicode?
(line-cell->unicode-char cell)
(line-cell->ascii-char cell))))
;; Horizontal separator char (vertical lines)
(define* (table-border-char-horizontal left-borders right-borders (unicode? #t))
(let* ((left-east (line-cell-east left-borders))
(right-west (line-cell-west right-borders))
(combined (combine-line-style left-east right-west))
(cell (make-straight-vertical-line-cell* combined)))
(if unicode?
(line-cell->unicode-char cell)
(line-cell->ascii-char cell))))
;; Corner between four adjacent cells
(define* (table-border-char-cross tl-b tr-b bl-b br-b (unicode? #t))
(let* ((tl-east (line-cell-east tl-b))
(tl-south (line-cell-south tl-b))
(tr-west (line-cell-west tr-b))
(tr-south (line-cell-south tr-b))
(bl-north (line-cell-north bl-b))
(bl-east (line-cell-east bl-b))
(br-north (line-cell-north br-b))
(br-west (line-cell-west br-b))
(north (combine-line-style tl-east tr-west))
(west (combine-line-style tl-south bl-north))
(east (combine-line-style tr-south br-north))
(south (combine-line-style bl-east br-west))
(cell (make-line-cell north west east south)))
(if unicode?
(line-cell->unicode-char cell)
(line-cell->ascii-char cell))))
;; Appends to list, lst should be pointing at the last cons cell of
;; the list, returns the new last cons cell.
(define (append-to-list lst ch n)
(cond
((= 0 n)
lst)
(else
(set-cdr! lst (cons ch '()))
(append-to-list (cdr lst) ch (sub1 n)))))
;; Returns SGR-list of table-rows border
;; TODO: add corner only if applicable (add separators argument)
(define* (table-rows-border col-widths top-row-borders bottom-row-borders
col-separators (unicode? #t))
(let ((ch0 (cons #f '()))
(trb0 (append (or top-row-borders
(map (lambda x 0) bottom-row-borders))
(list 0)))
(brb0 (append (or bottom-row-borders
(map (lambda x 0) top-row-borders))
(list 0))))
(let loop ((chl ch0)
(trb:l (cons 0 trb0))
(brb:l (cons 0 brb0))
(trb:r trb0)
(brb:r brb0)
(cws (append col-widths
(list 0)))
(col-seps col-separators))
(if (null? trb:r)
(list (list (cons (list->utf8-string (cdr ch0))
(length (cdr ch0)))))
(let* ((tl (car trb:l))
(tr (car trb:r))
(bl (car brb:l))
(br (car brb:r))
(lc (table-border-char-cross tl tr bl br unicode?))
(sc (table-border-char-vertical tr br unicode?))
(cw (car cws)))
(loop (append-to-list (if (car col-seps)
(append-to-list chl lc 1)
chl)
sc cw)
(cdr trb:l)
(cdr brb:l)
(cdr trb:r)
(cdr brb:r)
(cdr cws)
(cdr col-seps)))))))
;; Returns a SGR-block of correct height
(define* (table-border-vertical height left-borders right-borders (unicode? #t))
(let* ((ch (table-border-char-horizontal left-borders right-borders unicode?))
(str (utf8-char->string ch)))
(let loop ((height height)
(res '()))
(if (= height 0)
res
(loop (sub1 height)
(cons (list 0 (cons str 1))
res))))))
;; Returns true if the border should be drawn between these two rows
(define (table-border-between-rows? row0 row1)
(let loop ((row0 (if row0
row0
(map (lambda x 0) row1)))
(row1 (if row1
row1
(map (lambda x 0) row0)))
(res #f))
(if (null? row0)
res
(loop (cdr row0)
(cdr row1)
(or res
(not (eq? (table-border-char-vertical (car row0)
(car row1) #f)
#\space)))))))
;; Returns a list of boolean values representing the presence of
;; column delimiters
(define (table-border-between-row-columns? row)
(let loop ((row row)
(res '())
(prev 0))
(if (null? row)
(reverse (cons (not (eq? (table-border-char-horizontal prev 0 #f)
#\space))
res))
(loop (cdr row)
(cons (not (eq? (table-border-char-horizontal prev
(car row) #f)
#\space))
res)
(car row)))))
;; Returns a list of booleans representing the fact that there is a
;; column separator at given position. The list contains one more
;; value than the number of columns.
(define (table-col-separators? borders)
(if (null? borders)
'()
(let loop ((rows (cdr borders))
(res (table-border-between-row-columns? (car borders))))
(if (null? rows)
res
(loop (cdr rows)
(let rloop ((res0 res)
(row0 (table-border-between-row-columns? (car rows)))
(res '()))
(if (null? res0)
(reverse res)
(rloop (cdr res0)
(cdr row0)
(cons (or (car res0)
(car row0))
res)))))))))
;; Merges row SGR blocks and intersperses them with separators when
;; appropriate.
(define (table-row-merge row col-separators borders unicode?)
(if (null? row)
'()
(let ((height (sgr-block-height (car row))))
(let loop ((row row)
(res '())
(seps col-separators) ;; always left first
(borders borders)
(prev-border 0)
)
(if (null? row)
(apply sgr-block-happend
(reverse (if (car seps)
(cons (table-border-vertical height
prev-border
0
unicode?)
res)
res)))
(loop (cdr row)
(cons (car row)
(if (car seps)
(cons (table-border-vertical height
prev-border
(car borders)
unicode?)
res)
res))
(cdr seps)
(cdr borders)
(car borders)))))))
)

208
src/table-processor.scm Normal file
View file

@ -0,0 +1,208 @@
;;
;; table-processor.scm
;;
;; Table data preprocessing (before rendering)
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit table-processor))
(module
table-processor
(
table-prepare
)
(import scheme
(chicken base)
(chicken format)
(chicken sort)
sgr-cell
template-list-expander)
;; Makes the list of lists rectangular, makes sure all cells are
;; sgr-cell (sgr-block actually).
(define (table-prepare-cells tbl)
(let ((width (apply max (map length tbl))))
(map (lambda (row)
(let loop ((row row)
(rrow '())
(rlen 0))
(if (= rlen width)
(let ()
(reverse rrow))
(loop (if (null? row)
row
(cdr row))
(let ((cell (if (null? row)
""
(car row))))
(cons (string->sgr-cell
(if (string? cell)
cell
(format "~A" cell)))
rrow))
(add1 rlen)))))
tbl)))
;; Max from both lists (must be the same length)
(define (combine-column-widths cwidths rwidths)
(let mloop ((cwidths cwidths)
(rwidths rwidths)
(res '()))
(if (null? cwidths)
(reverse res)
(mloop (cdr cwidths)
(cdr rwidths)
(cons (max (car cwidths)
(car rwidths))
res)))))
;; Returns maximum value for each column
(define (table-columns-max-query tbl cell-query)
(if (null? tbl)
'()
(let loop ((widths (map cell-query (car tbl)))
(tbl (cdr tbl)))
(if (null? tbl)
widths
(loop (combine-column-widths widths
(map sgr-cell-min-width (car tbl)))
(cdr tbl))))))
;; Minimal widths
(define (table-min-column-widths tbl)
(table-columns-max-query tbl sgr-cell-min-width))
;; Weights
(define (table-column-weights tbl)
(table-columns-max-query tbl sgr-cell-width))
;; Distribute width according to weights.
(define (compute-weighted-width-adds width weights)
(let ((sorted-weights
(sort
(let loop ((weights weights)
(idx 0)
(res '()))
(if (null? weights)
res
(loop (cdr weights)
(add1 idx)
(cons (cons (car weights)
idx)
res))))
(lambda (a b)
(< (car a)
(car b))))))
(let loop ((weights (map car sorted-weights))
(indexes (map cdr sorted-weights))
(remaining-width width)
(res '()))
(if (null? weights)
(map car
(sort res
(lambda (a b)
(< (cdr a)
(cdr b)))))
(let* ((total-weight (apply + weights))
(this-weight (car weights))
(this-width (quotient (* this-weight remaining-width) total-weight))
(this-index (car indexes)))
(loop (cdr weights)
(cdr indexes)
(- remaining-width this-width)
(cons (cons this-width this-index) res)))))))
;; Sums the two widths
(define (distribute-surplus widths adds)
(let loop ((widths widths)
(adds adds)
(res '()))
(if (null? widths)
(reverse res)
(loop (cdr widths)
(cdr adds)
(cons (+ (car widths)
(car adds))
res)))))
;; For all rows, performs 1st pass render (wrapping)
(define (render-cells-widths tbl widths)
(map (lambda (row)
(let loop ((row row)
(widths widths)
(res '()))
(if (null? row)
(reverse res)
(loop (cdr row)
(cdr widths)
(cons (sgr-cell-render (car row)
#:width (car widths))
res)))))
tbl))
;; Get maximum height, expand using last state and empty rows
(define (expand-row-height row)
(let ((height (apply max (map sgr-cell-height row))))
(map (lambda (cell)
(sgr-cell-vexpand cell height))
row)))
;; Get minimal column widths, combine to minimal wanted widths, get
;; column weights, distribute the surplus (if any). Render all cells
;; to get row heights. Second pass, expand vertically all cells,
;; return result. Widths must be expanded from template spec.
(define (table-prepare tbl width-arg widths-spec)
(if (or (null? tbl)
(null? (car tbl)))
'()
(let* ((ptbl (table-prepare-cells tbl))
;;(_ (print ptbl))
(num-columns (length (car ptbl)))
(widths (expand-template-list widths-spec num-columns))
;;(_ (print widths))
(min-widths0 (table-min-column-widths ptbl))
;;(_ (print min-widths0))
(min-widths (combine-column-widths min-widths0 widths))
;;(_ (print min-widths))
(col-weights (table-column-weights ptbl))
;;(_ (print col-weights))
(min-width (foldl + 0 min-widths))
;;(_ (print min-width))
(width (if (and width-arg
(> width-arg min-width))
width-arg
min-width))
;;(_ (print width))
(width-surplus (- width min-width))
;;(_ (print width-surplus))
(widths-adds (compute-weighted-width-adds width-surplus col-weights))
;;(_ (print widths-adds))
(col-widths (distribute-surplus widths-adds min-widths))
;;(_ (print col-widths))
(tbl1 (render-cells-widths ptbl col-widths))
;;(_ (print tbl1))
(tbl2 (map expand-row-height tbl1)))
;; Just return the result - both the table and cached column widths
(values tbl2
col-widths))))
)

170
src/table-style.scm Normal file
View file

@ -0,0 +1,170 @@
;;
;; table-style.scm
;;
;; Converts and expands table border styles.
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit table-style))
(module
table-style
(
expand-table-style
table-style-tests!
)
(import scheme
(chicken base)
(chicken keyword)
box-drawing
testing
template-list-expander)
;; Consumes single border specification from cell borders. Returns:
;; sides, line-style-spec and rest.
(define (table-border-style-consume lst)
(let loop ((sides '())
(sides-done? #f)
(line-style-spec '())
(lst lst))
(if (null? lst)
;; Last style in the list, just finish
(values (reverse sides)
(reverse line-style-spec)
lst)
(let ((tk (car lst)))
(cond ((and sides-done?
(keyword? tk))
;; Next side spec continues
(values (reverse sides)
(reverse line-style-spec)
lst))
((keyword? tk)
;; Still adding sides
(loop (cons tk sides)
sides-done? ;; Should be always #f
line-style-spec ;; Should always be '()
(cdr lst)))
(else
;; Line specification
(loop sides
#t
(cons tk line-style-spec)
(cdr lst))))))))
;; Compiles correct box drawing cell and merges it with given cell
(define (combine-cell-border cell sides line-style-spec)
(let ((line-style (spec->line-style line-style-spec)))
;; Overide given sides
(if (null? sides)
(make-line-cell line-style
line-style
line-style
line-style)
(let loop ((sides sides)
(cell cell))
(if (null? sides)
cell
(loop (cdr sides)
(case (car sides)
((#:north #:top #:up) (set-line-cell-north cell line-style))
((#:west #:left) (set-line-cell-west cell line-style))
((#:east #:right) (set-line-cell-east cell line-style))
((#:south #:bottom #:down) (set-line-cell-south cell line-style))
(else cell))))))))
;; Parses border style specification for single cell, returns
;; box-drawing cell with slightly different meaning of NWES sides.
(define (parse-table-cell-border-style spec-arg)
(let ((spec (if (list? spec-arg)
spec-arg
(list spec-arg))))
(let loop ((spec spec)
(cell line-cell-none))
(if (null? spec)
;; Finished, return, whatever we accumulated
cell
(let-values (((sides line-style-spec rest)
(table-border-style-consume spec)))
(loop rest
(combine-cell-border cell sides line-style-spec)))))))
;; Converts all "cells" using parse-table-cell-border-style
(define (compile-table-style-spec spec)
(map (lambda (row)
(if (template-expansion-token? row)
row
(map (lambda (cell)
(if (template-expansion-token? cell)
cell
(parse-table-cell-border-style cell)))
row)))
spec))
;; Converts the template skipping dots, expands the result
(define (expand-table-style spec width height)
(let ((cspec (compile-table-style-spec spec)))
(expand-template-list
(map (lambda (row)
(if (template-expansion-token? row)
row
(expand-template-list row width)))
cspec)
height)))
;; Module self-tests
(define (table-style-tests!)
(run-tests
table-style
(test-equal? parse-table-cell-border-style
(parse-table-cell-border-style 'light)
#b1001100110011001)
(test-equal? parse-table-cell-border-style
(parse-table-cell-border-style '(light dashed))
#b101010101010101)
(test-equal? parse-table-cell-border-style
(parse-table-cell-border-style '(light #:top none))
#b1001100110010000)
(test-equal? compile-table-style-spec
(compile-table-style-spec
'(((heavy dashed) ...)
((light #:left #:right none) ...)
...
(dashed ...)))
'((#b0110011001100110 ...)
(#b1001000000001001 ...)
...
(#b0101010101010101 ...)))
(test-equal? expand-table-style
(expand-table-style
'(((heavy dashed) ...)
((light #:left #:right none) ...)
...
(dashed ...))
4 4)
'((#b0110011001100110 #b0110011001100110 #b0110011001100110 #b0110011001100110)
(#b1001000000001001 #b1001000000001001 #b1001000000001001 #b1001000000001001)
(#b1001000000001001 #b1001000000001001 #b1001000000001001 #b1001000000001001)
(#b0101010101010101 #b0101010101010101 #b0101010101010101 #b0101010101010101)))
))
)

112
src/table.scm Normal file
View file

@ -0,0 +1,112 @@
;;
;; table.scm
;;
;; Table surface API.
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit table))
(module
table
(
print-table
table->string
table->string-list
table->sgr-lists
)
(import scheme
(chicken base)
(chicken string)
sgr-list
sgr-block
racket-kwargs
table-processor
table-border
table-style)
(define (print-table . args)
(print (apply table->string args)))
(define (table->string . args)
(string-intersperse
(apply table->string-list args)
"\n"))
(define (table->string-list . args)
(map sgr-list->string
(apply table->sgr-lists args)))
(define (merge-rows ptbl borders col-separators unicode?)
(let loop ((rows ptbl)
(borders borders)
(res '()))
(if (null? rows)
(reverse res)
(loop (cdr rows)
(cdr borders)
(cons (table-row-merge (car rows)
col-separators
(car borders)
unicode?)
res)))))
(define* (table->sgr-lists tbl
#:border (border-spec '((none ...) ...))
#:widths (widths-spec '(0 ...))
#:width (width #f)
#:unicode? (unicode? #t))
(let-values (((ptbl col-widths)
(table-prepare tbl width widths-spec)))
(let* ((num-columns (length (car tbl)))
(num-rows (length tbl))
(borders (expand-table-style border-spec num-columns num-rows))
(col-separators (table-col-separators? borders))
(rows (merge-rows ptbl borders col-separators unicode?)))
(let loop ((rows rows)
(borders borders)
(res '())
(prev-borders #f))
(if (null? rows)
(apply append
(reverse (if (table-border-between-rows? prev-borders #f)
(cons (table-rows-border col-widths
prev-borders
#f
col-separators
unicode?)
res)
res)))
(loop (cdr rows)
(cdr borders)
(if (table-border-between-rows? prev-borders (car borders))
(cons (car rows)
(cons (table-rows-border col-widths
prev-borders
(car borders)
col-separators
unicode?)
res))
(cons (car rows) res))
(car borders)
))))))
)

View file

@ -0,0 +1,153 @@
;;
;; template-list-expander.scm
;;
;; Dynamic length lists based on simple templates with head and tail
;; patterns.
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit template-list-expander))
(module
template-list-expander
(
template-expansion-token?
expand-template-list
template-list-expander-tests!
)
(import scheme
(chicken base)
testing)
(define (template-expansion-token? token)
(and (symbol? token)
(let* ((tokenstr (symbol->string token))
(tokenlen (string-length tokenstr)))
(and (>= tokenlen 3)
(string=? (substring tokenstr 0 3) "...")))))
(define (split-template-list temp-lst)
(let loop ((lst temp-lst)
(rhead '()))
(if (null? lst)
(values temp-lst '() '())
(let* ((token (car lst))
(tokenstr (if (symbol? token)
(symbol->string token)
""))
(tokenlen (if (symbol? token)
(string-length tokenstr)
0)))
(cond ((and (symbol? token)
(>= tokenlen 3)
(string=? (substring tokenstr 0 3) "..."))
(let rloop ((cnt (- tokenlen 2))
(rhead2 rhead)
(rrep '()))
(if (= cnt 0)
(values (reverse rhead2)
rrep
(cdr lst))
(rloop (sub1 cnt)
(cdr rhead2)
(cons (car rhead2) rrep)))))
(else
(loop (cdr lst)
(cons token rhead))))))))
(define (repeat-list-for lst len)
(let loop ((cnt len)
(rep lst)
(res '()))
(if (= cnt 0)
(reverse res)
(loop (sub1 cnt)
(if (null? (cdr rep)) lst (cdr rep))
(cons (car rep) res)))))
(define (take-from-list lst cnt)
(let loop ((lst lst)
(res '())
(cnt cnt))
(if (= cnt 0)
(reverse res)
(loop (cdr lst)
(cons (car lst) res)
(sub1 cnt)))))
(define (expand-template-list lst len)
(let-values (((head rep tail) (split-template-list lst)))
(let ((headlen (length head))
(taillen (length tail)))
;;(print "----------------")
;;(print "head = " head)
;;(print "rep = " rep)
;;(print "tail = " tail)
(cond
((= len headlen)
head)
((< len headlen)
(take-from-list head len))
(else
(let ((head+taillen (+ headlen taillen)))
(cond
((= len head+taillen)
(append head tail))
((< len head+taillen)
(append head (take-from-list tail (- len head+taillen))))
(else
(append head
(repeat-list-for rep (- len head+taillen))
tail)))))))))
(define (template-list-expander-tests!)
(run-tests
template-list-expander
(test-equal? expand-template-list
(expand-template-list '() 0)
'())
(test-equal? expand-template-list
(expand-template-list '((a) ... (b)) 5)
'((a) (a) (a) (a) (b)))
(test-equal? expand-template-list
(expand-template-list '((c) (a) (d) ... (b) (e)) 9)
'((c) (a) (d) (d) (d) (d) (d) (b) (e)))
(test-equal? expand-template-list
(expand-template-list '((c) (a) (d) .... (b) (e)) 9)
'((c) (a) (d) (a) (d) (a) (d) (b) (e)))
(test-true template-expansion-token?
(template-expansion-token? '...))
(test-true template-expansion-token?
(template-expansion-token? '....))
(test-false template-expansion-token?
(template-expansion-token? '..))
(test-false template-expansion-token?
(template-expansion-token? 'hello))
(test-false template-expansion-token?
(template-expansion-token? "hello"))
(test-equal? expand-template-list
(expand-template-list '(a b ... c) 2)
'(a c))
))
)

305
src/util-utf8.scm Normal file
View file

@ -0,0 +1,305 @@
;;
;; util-utf8.scm
;;
;; UTF-8 support
;;
;; ISC License
;;
;; Copyright 2023 Dominik Pantůček <dominik.pantucek@trustica.cz>
;;
;; Permission to use, copy, modify, and/or distribute this software
;; for any purpose with or without fee is hereby granted, provided
;; that the above copyright notice and this permission notice appear
;; in all copies.
;;
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
;;
(declare (unit util-utf8))
(module
util-utf8
(
utf8-char->string
make-utf8-string
string-append-utf8-char
utf8-string->lists
utf8-bytes->lists
utf8-string-length
utf8-string-next-char
utf8-string->list
list->utf8-string
util-utf8-tests!
)
(import scheme
(chicken base)
(chicken bitwise)
testing)
;; Encodes given character as UTF-8
(define (utf8-char->string ch)
(let ((n (char->integer ch)))
(cond ((>= n #x10000)
(let ((r (make-string 4))
(b0 (bitwise-and n #b111111))
(b1 (bitwise-and (arithmetic-shift n -6) #b111111))
(b2 (bitwise-and (arithmetic-shift n -12) #b111111))
(b3 (bitwise-and (arithmetic-shift n -18) #b111)))
(string-set! r 0 (integer->char (bitwise-ior b3 #b11110000)))
(string-set! r 1 (integer->char (bitwise-ior b2 #b10000000)))
(string-set! r 2 (integer->char (bitwise-ior b1 #b10000000)))
(string-set! r 3 (integer->char (bitwise-ior b0 #b10000000)))
r))
((>= n #x800)
(let ((r (make-string 3))
(b0 (bitwise-and n #b111111))
(b1 (bitwise-and (arithmetic-shift n -6) #b111111))
(b2 (bitwise-and (arithmetic-shift n -12) #b1111)))
(string-set! r 0 (integer->char (bitwise-ior b2 #b11100000)))
(string-set! r 1 (integer->char (bitwise-ior b1 #b10000000)))
(string-set! r 2 (integer->char (bitwise-ior b0 #b10000000)))
r))
((>= n #x80)
(let ((r (make-string 2))
(b0 (bitwise-and n #b111111))
(b1 (bitwise-and (arithmetic-shift n -6) #b11111)))
(string-set! r 0 (integer->char (bitwise-ior b1 #b11000000)))
(string-set! r 1 (integer->char (bitwise-ior b0 #b10000000)))
r))
(else
(make-string 1 ch)))))
;; UTF-8 version of make-string
(define (make-utf8-string n . chs)
(let* ((ch (if (null? chs) #\space (car chs)))
(s (utf8-char->string ch))
(sl (string-length s))
(rl (* n sl))
(r (make-string rl)))
(let loop ((ri 0)
(si 0))
(if (= ri rl)
r
(let ((nsi (add1 si)))
(string-set! r ri (string-ref s si))
(loop (add1 ri)
(if (= nsi sl) 0 nsi)))))))
;; UTF-8 character append
(define (string-append-utf8-char s ch)
(string-append s (utf8-char->string ch)))
;; Converts a UTF-8 string into two lists: list of UTF-8 characters
;; of the string and a list of remaining bytes (as integers).
(define (utf8-string->lists str)
(utf8-bytes->lists
(map char->integer
(string->list str))))
;; The same as above but accepts a list of bytes (as integers)
(define (utf8-bytes->lists chars)
(let loop ((bytes chars)
(rpending '())
(pending 0)
(expected #f)
(res '()))
(if (null? bytes)
(values (reverse res)
(reverse rpending))
(let ((byte (car bytes)))
(cond (expected
;; Decode UTF-8 sequence
(cond ((= expected 1)
;; Last byte
(let ((char (integer->char (bitwise-ior pending
(bitwise-and byte #b111111)))))
(loop (cdr bytes)
'()
0
#f
(cons char res))))
(else
;; Intermediate bytes
(loop (cdr bytes)
(cons byte rpending)
(arithmetic-shift (bitwise-ior pending
(bitwise-and byte #b111111)) 6)
(sub1 expected)
res))))
(else
;; ASCII or first of UTF-8 sequence
(cond ((= (bitwise-and byte #b10000000) 0)
;; ASCII
(loop (cdr bytes)
'()
0
#f
(cons (integer->char byte) res)))
(else
;; First byte of UTF-8 sequence
(let-values
(((first-byte char-bytes)
(cond ((= (bitwise-and byte #b11000000) #b11000000)
(values (bitwise-and byte #b11111)
2))
((= (bitwise-and byte #b11100000) #b11100000)
(values (bitwise-and byte #b1111)
3))
((= (bitwise-and byte #b11110000) #b11110000)
(values (bitwise-and byte #b111)
4)))))
(loop (cdr bytes)
(list byte)
(arithmetic-shift first-byte 6)
(sub1 char-bytes)
res))))))))))
;; Returns the position right after the character at specified
;; position.
(define (utf8-string-next-char str . sis)
(let ((len (string-length str))
(si0 (if (null? sis) 0 (car sis))))
(let loop ((si si0)
(pc 0))
(if (or (= si len)
(and (> si si0)
(eq? pc 0)))
si
(let ((b (char->integer (string-ref str si))))
(loop (add1 si)
(if (= pc 0)
(if (= (bitwise-and b #b11111000) #b11110000)
3
(if (= (bitwise-and b #b11110000) #b11100000)
2
(if (= (bitwise-and b 128) 128)
1
0)))
(if (= (bitwise-and b 128) 128)
(sub1 pc)
0))))))))
;; Calculates the length of given UTF-8 string
(define (utf8-string-length s)
(let ((l (string-length s)))
(let loop ((si 0)
(ci 0)
(pc 0))
(if (= si l)
ci
(let ((b (char->integer (string-ref s si))))
(loop (add1 si)
(if (or (= pc 0)
(= (bitwise-and b 128) 0))
(add1 ci)
ci)
(if (= pc 0)
(if (= (bitwise-and b #b11111000) #b11110000)
3
(if (= (bitwise-and b #b11110000) #b11100000)
2
(if (= (bitwise-and b 128) 128)
1
0)))
(if (= (bitwise-and b 128) 128)
(sub1 pc)
0))))))))
;; Converts utf8 string to list of unicode characters
(define (utf8-string->list s)
(let-values (((lst _) (utf8-string->lists s)))
lst))
;; Prepends 1-byte characters representing utf8 encoding of given
;; unicode character to the list
(define (prepend-unicode-char-to-utf8-list ch lst)
(let ((n (char->integer ch)))
(cond ((>= n #x10000)
(let ((r (make-string 4))
(b0 (bitwise-and n #b111111))
(b1 (bitwise-and (arithmetic-shift n -6) #b111111))
(b2 (bitwise-and (arithmetic-shift n -12) #b111111))
(b3 (bitwise-and (arithmetic-shift n -18) #b111)))
(cons (integer->char (bitwise-ior b0 #b10000000))
(cons (integer->char (bitwise-ior b1 #b10000000))
(cons (integer->char (bitwise-ior b2 #b10000000))
(cons (integer->char (bitwise-ior b3 #b11110000))
lst))))))
((>= n #x800)
(let ((r (make-string 3))
(b0 (bitwise-and n #b111111))
(b1 (bitwise-and (arithmetic-shift n -6) #b111111))
(b2 (bitwise-and (arithmetic-shift n -12) #b1111)))
(cons (integer->char (bitwise-ior b0 #b10000000))
(cons (integer->char (bitwise-ior b1 #b10000000))
(cons (integer->char (bitwise-ior b2 #b11100000))
lst)))))
((>= n #x80)
(let ((r (make-string 2))
(b0 (bitwise-and n #b111111))
(b1 (bitwise-and (arithmetic-shift n -6) #b11111)))
(cons (integer->char (bitwise-ior b0 #b10000000))
(cons (integer->char (bitwise-ior b1 #b11000000))
lst))))
(else
(cons ch lst)))))
;; Converts list of unicode characters into utf8 string
(define (list->utf8-string lst)
(let loop ((lst lst)
(res '()))
(if (null? lst)
(list->string (reverse res))
(loop (cdr lst)
(prepend-unicode-char-to-utf8-list (car lst) res)))))
;; Module self-tests
(define (util-utf8-tests!)
(run-tests
util-utf8
(test-equal? utf8-char->string
(utf8-char->string #\ř)
"ř")
(test-equal? make-utf8-string
(make-utf8-string 4 #\č)
"čččč")
(test-equal? string-append-utf8-char
(string-append-utf8-char "ččč" #\ř)
"čččř")
(test-equal? utf8-string->list
(utf8-string->list "ěščř")
'(#\ě #\š #\č #\ř))
(test-equal? list->utf8-string
(list->utf8-string '(#\ě #\š #\č #\ř))
"ěščř")
(test-equal? utf8-string-length
(utf8-string-length "ěščř")
4)
(test-equal? utf8-string-length
(utf8-string-length "ěxšy")
4)
(test-equal? utf8-string-next-char
(utf8-string-next-char "ěščř")
2)
(test-equal? utf8-string-next-char
(utf8-string-next-char "ěščř" 2)
4)
))
)