;; ;; sgr-block.scm ;; ;; Represents a block of sgr-list rows. ;; ;; ISC License ;; ;; Copyright 2023 Dominik Pantůček ;; ;; 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 util-utf8) ;; 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) #:strip-ansi? (strip-ansi? #f)) (map (lambda (sl) (sgr-list->string sl #:reset-state reset-state #:strip-ansi? strip-ansi?)) 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-utf8-string n #\xA0) 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? res)) (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-utf8-string (car gluelens) #\xA0) (car gluelens)) res)) (loop gluelens (cdr sl) (cons (car sl) res)))))) ;; Nothing to expand, fill-in remainder with spaces (reverse (cons (cons (make-utf8-string rem0 #\xA0) 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 ((sln (sgr-list-neutralize sl))) ;;(write sln)(newline) (values (list 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-utf8-string sbw #\xA0) 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) ("\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0" . 9) ("World!" . 6))) )) )