418 lines
16 KiB
Scheme
418 lines
16 KiB
Scheme
|
;;;; fmt-block.scm -- columnar formatting
|
||
|
;;
|
||
|
;; Copyright (c) 2006-2011 Alex Shinn. All rights reserved.
|
||
|
;; BSD-style license: http://synthcode.com/license.txt
|
||
|
|
||
|
|
||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||
|
;;; Columnar formatting
|
||
|
;;
|
||
|
;; A line-oriented formatter. Takes a list of
|
||
|
;; (line-fmt1 gen-fmt1 line-fmt2 gen-fmt2 ...)
|
||
|
;; and formats each of the gen-fmt1 formats as columns, printed
|
||
|
;; side-by-side, each line allowing post-processing done by line-fmt1
|
||
|
;; (just use dsp if you want to display the lines verbatim).
|
||
|
|
||
|
;; Continuations come to the rescue to make this work properly,
|
||
|
;; letting us weave the output between different columns without
|
||
|
;; needing to build up intermediate strings.
|
||
|
|
||
|
(define (fmt-columns . ls)
|
||
|
(lambda (orig-st)
|
||
|
(call-with-current-continuation
|
||
|
(lambda (return)
|
||
|
(define (infinite? x)
|
||
|
(and (pair? x) (pair? (cdr x)) (pair? (cddr x)) (caddr x)))
|
||
|
(let ((q1 '())
|
||
|
(q2 '())
|
||
|
(remaining (length (remove infinite? ls))))
|
||
|
(define (enq! proc) (set! q2 (cons proc q2)))
|
||
|
(define (deq!) (let ((proc (car q1))) (set! q1 (cdr q1)) proc))
|
||
|
(define (line-init!) (set! q1 (reverse q2)) (set! q2 '()))
|
||
|
(define (line-done?) (null? q1))
|
||
|
(define line-buf '())
|
||
|
(define line-non-empty? #f)
|
||
|
(define (write-column fmt str finite?)
|
||
|
(set! line-buf (cons (cons fmt str) line-buf))
|
||
|
(if finite? (set! line-non-empty? #t)))
|
||
|
(define (write-line)
|
||
|
(cond
|
||
|
(line-non-empty?
|
||
|
(for-each
|
||
|
(lambda (x) (set! orig-st (((car x) (cdr x)) orig-st)))
|
||
|
(reverse line-buf))
|
||
|
(set! orig-st (nl orig-st))))
|
||
|
(set! line-buf '())
|
||
|
(set! line-non-empty? #f)
|
||
|
(line-init!))
|
||
|
(define (next cont)
|
||
|
(enq! cont)
|
||
|
(cond
|
||
|
((line-done?)
|
||
|
(write-line)
|
||
|
(if (not (positive? remaining)) (finish) ((deq!) #f)))
|
||
|
(else ((deq!) #f))))
|
||
|
(define (finish)
|
||
|
(write-line)
|
||
|
(return orig-st))
|
||
|
(define (make-empty-col fmt)
|
||
|
(define (blank *ignored*)
|
||
|
(write-column fmt "" #f)
|
||
|
(next blank)) ; infinite loop, next terminates for us
|
||
|
blank)
|
||
|
(define (make-col st fmt gen finite?)
|
||
|
(let ((acc '())) ; buffer incomplete lines
|
||
|
(lambda (*ignored*)
|
||
|
(define (output* str st)
|
||
|
(let lp ((i 0))
|
||
|
(let ((nli (string-index str #\newline i)))
|
||
|
(cond
|
||
|
(nli
|
||
|
(let ((line
|
||
|
(string-concatenate-reverse
|
||
|
(cons (substring/shared str i nli) acc))))
|
||
|
(set! acc '())
|
||
|
(write-column fmt line finite?)
|
||
|
(call-with-current-continuation next)
|
||
|
(lp (+ nli 1))))
|
||
|
(else
|
||
|
(set! acc (cons (substring/shared str i) acc))))))
|
||
|
;; update - don't output or the string port will fill up
|
||
|
(fmt-update str st))
|
||
|
;; gen threads through it's own state, ignore result
|
||
|
(gen (fmt-set-writer! (copy-fmt-state st) output*))
|
||
|
;; reduce # of remaining finite columns
|
||
|
(set! remaining (- remaining 1))
|
||
|
;; write any remaining accumulated output
|
||
|
(if (pair? acc)
|
||
|
(let ((s (string-concatenate-reverse acc)))
|
||
|
(write-column fmt s (and finite? (not (equal? s ""))))))
|
||
|
;; (maybe) loop with an empty column in place
|
||
|
(if (not (positive? remaining))
|
||
|
(finish)
|
||
|
(next (make-empty-col fmt))))))
|
||
|
;; queue up the initial formatters
|
||
|
(for-each
|
||
|
(lambda (col)
|
||
|
(let ((st (fmt-set-port! (copy-fmt-state orig-st)
|
||
|
(open-output-string))))
|
||
|
(enq! (make-col st (car col) (dsp (cadr col))
|
||
|
(not (infinite? col))))))
|
||
|
ls)
|
||
|
(line-init!)
|
||
|
;; start
|
||
|
((deq!) #f))))))
|
||
|
|
||
|
(define (columnar . ls)
|
||
|
(define (proportional-width? w)
|
||
|
(and (number? w)
|
||
|
(or (< 0 w 1)
|
||
|
(and (inexact? w) (= w 1.0)))))
|
||
|
(define (whitespace-pad? st)
|
||
|
(char-whitespace? (or (fmt-pad-char st) #\space)))
|
||
|
(define (build-column ls)
|
||
|
(let-optionals* ls ((fixed-width #f)
|
||
|
(width #f)
|
||
|
(last? #t)
|
||
|
(tail '())
|
||
|
(gen #f)
|
||
|
(prefix '())
|
||
|
(align 'left)
|
||
|
(infinite? #f))
|
||
|
(define (scale-width st)
|
||
|
(max 1 (inexact->exact
|
||
|
(truncate (* width (- (fmt-width st) fixed-width))))))
|
||
|
(define (padder)
|
||
|
(if (proportional-width? width)
|
||
|
(case align
|
||
|
((right)
|
||
|
(lambda (str) (lambda (st) ((pad/left (scale-width st) str) st))))
|
||
|
((center)
|
||
|
(lambda (str) (lambda (st) ((pad/both (scale-width st) str) st))))
|
||
|
(else
|
||
|
(lambda (str) (lambda (st) ((pad/right (scale-width st) str) st)))))
|
||
|
(case align
|
||
|
((right) (lambda (str) (pad/left width str)))
|
||
|
((center) (lambda (str) (pad/both width str)))
|
||
|
(else (lambda (str) (pad/right width str))))))
|
||
|
(define (affix x)
|
||
|
(cond
|
||
|
((pair? tail)
|
||
|
(lambda (str)
|
||
|
(cat (string-concatenate prefix)
|
||
|
(x str)
|
||
|
(string-concatenate tail))))
|
||
|
((pair? prefix)
|
||
|
(lambda (str) (cat (string-concatenate prefix) (x str))))
|
||
|
(else x)))
|
||
|
(list
|
||
|
;; line formatter
|
||
|
(affix
|
||
|
(let ((pad (padder)))
|
||
|
(if (and last? (not (pair? tail)) (eq? align 'left))
|
||
|
(lambda (str)
|
||
|
(lambda (st)
|
||
|
(((if (whitespace-pad? st) dsp pad) str) st)))
|
||
|
pad)))
|
||
|
;; generator
|
||
|
(if (proportional-width? width)
|
||
|
(lambda (st) ((with-width (scale-width st) gen) st))
|
||
|
(with-width width gen))
|
||
|
infinite?
|
||
|
)))
|
||
|
(define (adjust-widths ls border-width)
|
||
|
(let* ((fixed-ls
|
||
|
(filter (lambda (x) (and (number? (car x)) (>= (car x) 1))) ls))
|
||
|
(fixed-total (fold + border-width (map car fixed-ls)))
|
||
|
(scaled-ls (filter (lambda (x) (proportional-width? (car x))) ls))
|
||
|
(denom (- (length ls) (+ (length fixed-ls) (length scaled-ls))))
|
||
|
(rest (if (zero? denom)
|
||
|
0
|
||
|
(exact->inexact
|
||
|
(/ (- 1 (fold + 0 (map car scaled-ls))) denom)))))
|
||
|
(if (negative? rest)
|
||
|
(error 'columnar "fractional widths must sum to less than 1"
|
||
|
(map car scaled-ls)))
|
||
|
(map
|
||
|
(lambda (col)
|
||
|
(cons fixed-total
|
||
|
(if (not (number? (car col))) (cons rest (cdr col)) col)))
|
||
|
ls)))
|
||
|
(define (finish ls border-width)
|
||
|
(apply fmt-columns
|
||
|
(map build-column (adjust-widths (reverse ls) border-width))))
|
||
|
(let lp ((ls ls) (strs '()) (align 'left) (infinite? #f)
|
||
|
(width #t) (border-width 0) (res '()))
|
||
|
(cond
|
||
|
((null? ls)
|
||
|
(if (pair? strs)
|
||
|
(finish (cons (cons (caar res)
|
||
|
(cons #t (cons (append (reverse strs)
|
||
|
(caddar res))
|
||
|
(cdddar res))))
|
||
|
(cdr res))
|
||
|
border-width)
|
||
|
(finish (cons (cons (caar res) (cons #t (cddar res))) (cdr res))
|
||
|
border-width)))
|
||
|
((string? (car ls))
|
||
|
(if (string-index (car ls) #\newline)
|
||
|
(error 'columnar "column string literals can't contain newlines")
|
||
|
(lp (cdr ls) (cons (car ls) strs) align infinite?
|
||
|
width (+ border-width (string-length (car ls))) res)))
|
||
|
((number? (car ls))
|
||
|
(lp (cdr ls) strs align infinite? (car ls) border-width res))
|
||
|
((eq? (car ls) 'infinite)
|
||
|
(lp (cdr ls) strs align #t width border-width res))
|
||
|
((symbol? (car ls))
|
||
|
(lp (cdr ls) strs (car ls) infinite? width border-width res))
|
||
|
((procedure? (car ls))
|
||
|
(lp (cdr ls) '() 'left #f #t border-width
|
||
|
(cons (list width #f '() (car ls) (reverse strs) align infinite?)
|
||
|
res)))
|
||
|
(else
|
||
|
(error 'columnar "invalid column" (car ls))))))
|
||
|
|
||
|
(define (max-line-width string-width str)
|
||
|
(let lp ((i 0) (hi 0))
|
||
|
(let ((j (string-index str #\newline i)))
|
||
|
(if j
|
||
|
(lp (+ j 1) (max hi (string-width (substring str i j))))
|
||
|
(max hi (string-width (substring str i (string-length str))))))))
|
||
|
|
||
|
(define (pad-finite st proc width)
|
||
|
(let* ((str ((fmt-to-string proc) (copy-fmt-state st)))
|
||
|
(w (max-line-width (or (fmt-string-width st) string-length) str)))
|
||
|
(list (cat str)
|
||
|
(if (and (integer? width) (exact? width))
|
||
|
(max width w)
|
||
|
w))))
|
||
|
|
||
|
(define (tabular . ls)
|
||
|
(lambda (st)
|
||
|
(let lp ((ls ls) (infinite? #f) (width #t) (res '()))
|
||
|
(cond
|
||
|
((null? ls)
|
||
|
((apply columnar (reverse res)) st))
|
||
|
((number? (car ls))
|
||
|
(lp (cdr ls) infinite? (car ls) res))
|
||
|
((eq? 'infinite (car ls))
|
||
|
(lp (cdr ls) #t width (cons (car ls) res)))
|
||
|
((procedure? (car ls))
|
||
|
(if infinite?
|
||
|
(if width
|
||
|
(lp (cdr ls) #f #t (cons (car ls) (cons width res)))
|
||
|
(lp (cdr ls) #f #t (cons (car ls) res)))
|
||
|
(let ((gen+width (pad-finite st (car ls) width)))
|
||
|
(lp (cdr ls) #f #t (append gen+width res)))))
|
||
|
(else
|
||
|
(lp (cdr ls) infinite? width (cons (car ls) res)))))))
|
||
|
|
||
|
;; break lines only, don't fmt-join short lines or justify
|
||
|
(define (fold-lines . ls)
|
||
|
(lambda (st)
|
||
|
(define output (fmt-writer st))
|
||
|
(define (kons-in-line str st)
|
||
|
(let ((len ((or (fmt-string-width st) string-length) str))
|
||
|
(space (- (fmt-width st) (fmt-col st))))
|
||
|
(cond
|
||
|
((or (<= len space) (not (positive? space)))
|
||
|
(output str st))
|
||
|
(else
|
||
|
(kons-in-line
|
||
|
(substring/shared str space len)
|
||
|
(output nl-str
|
||
|
(output (substring/shared str 0 space) st)))))))
|
||
|
((fmt-let
|
||
|
'writer
|
||
|
(lambda (str st)
|
||
|
(let lp ((str str) (st st))
|
||
|
(let ((nli (string-index str #\newline)))
|
||
|
(cond
|
||
|
((not nli)
|
||
|
(kons-in-line str st))
|
||
|
(else
|
||
|
(lp (substring/shared str (+ nli 1))
|
||
|
(output nl-str
|
||
|
(kons-in-line
|
||
|
(substring/shared str 0 nli)
|
||
|
st))))))))
|
||
|
(apply-cat ls))
|
||
|
st)))
|
||
|
|
||
|
(define (wrap-fold-words seq knil max-width get-width line . o)
|
||
|
(let* ((last-line (if (pair? o) (car o) line))
|
||
|
(vec (if (list? seq) (list->vector seq) seq))
|
||
|
(len (vector-length vec))
|
||
|
(len-1 (- len 1))
|
||
|
(breaks (make-vector len #f))
|
||
|
(penalties (make-vector len #f))
|
||
|
(widths
|
||
|
(list->vector
|
||
|
(map get-width (if (list? seq) seq (vector->list vec))))))
|
||
|
(define (largest-fit i)
|
||
|
(let lp ((j (+ i 1)) (width (vector-ref widths i)))
|
||
|
(let ((width (+ width 1 (vector-ref widths j))))
|
||
|
(cond
|
||
|
((>= width max-width) (- j 1))
|
||
|
((>= j len-1) len-1)
|
||
|
(else (lp (+ j 1) width))))))
|
||
|
(define (min-penalty! i)
|
||
|
(cond
|
||
|
((>= i len-1) 0)
|
||
|
((vector-ref penalties i))
|
||
|
(else
|
||
|
(vector-set! penalties i (expt (+ max-width 1) 3))
|
||
|
(vector-set! breaks i i)
|
||
|
(let ((k (largest-fit i)))
|
||
|
(let lp ((j i) (width 0))
|
||
|
(if (<= j k)
|
||
|
(let* ((width (+ width (vector-ref widths j)))
|
||
|
(break-penalty
|
||
|
(+ (max 0 (expt (- max-width (+ width (- j i))) 3))
|
||
|
(min-penalty! (+ j 1)))))
|
||
|
(cond
|
||
|
((< break-penalty (vector-ref penalties i))
|
||
|
(vector-set! breaks i j)
|
||
|
(vector-set! penalties i break-penalty)))
|
||
|
(lp (+ j 1) width)))))
|
||
|
(if (>= (vector-ref breaks i) len-1)
|
||
|
(vector-set! penalties i 0))
|
||
|
(vector-ref penalties i))))
|
||
|
(define (sub-list i j)
|
||
|
(let lp ((i i) (res '()))
|
||
|
(if (> i j)
|
||
|
(reverse res)
|
||
|
(lp (+ i 1) (cons (vector-ref vec i) res)))))
|
||
|
(cond
|
||
|
((zero? len)
|
||
|
;; degenerate case
|
||
|
(last-line '() knil))
|
||
|
(else
|
||
|
;; compute optimum breaks
|
||
|
(vector-set! breaks len-1 len-1)
|
||
|
(vector-set! penalties len-1 0)
|
||
|
(min-penalty! 0)
|
||
|
;; fold
|
||
|
(let lp ((i 0) (acc knil))
|
||
|
(let ((break (vector-ref breaks i)))
|
||
|
(if (>= break len-1)
|
||
|
(last-line (sub-list i len-1) acc)
|
||
|
(lp (+ break 1) (line (sub-list i break) acc)))))))))
|
||
|
|
||
|
;; XXXX don't split, traverse the string manually and keep track of
|
||
|
;; sentence endings so we can insert two spaces
|
||
|
(define (wrap-fold str . o)
|
||
|
(apply wrap-fold-words (string-tokenize str) o))
|
||
|
|
||
|
(define (wrap-lines . ls)
|
||
|
(define (print-line ls st)
|
||
|
(nl ((fmt-join dsp ls " ") st)))
|
||
|
(define buffer '())
|
||
|
(lambda (st)
|
||
|
((fmt-let
|
||
|
'writer
|
||
|
(lambda (str st) (set! buffer (cons str buffer)) st)
|
||
|
(apply-cat ls))
|
||
|
st)
|
||
|
(wrap-fold (string-concatenate-reverse buffer)
|
||
|
st (fmt-width st)
|
||
|
(or (fmt-string-width st) string-length)
|
||
|
print-line)))
|
||
|
|
||
|
(define (justify . ls)
|
||
|
(lambda (st)
|
||
|
(let ((width (fmt-width st))
|
||
|
(string-width (or (fmt-string-width st) string-length))
|
||
|
(output (fmt-writer st))
|
||
|
(buffer '()))
|
||
|
(define (justify-line ls st)
|
||
|
(if (null? ls)
|
||
|
(nl st)
|
||
|
(let* ((sum (fold (lambda (s n) (+ n (string-width s))) 0 ls))
|
||
|
(len (length ls))
|
||
|
(diff (max 0 (- width sum)))
|
||
|
(sep (make-string (if (= len 1)
|
||
|
0
|
||
|
(quotient diff (- len 1)))
|
||
|
#\space))
|
||
|
(rem (if (= len 1)
|
||
|
diff
|
||
|
(remainder diff (- len 1)))))
|
||
|
(output
|
||
|
(call-with-output-string
|
||
|
(lambda (p)
|
||
|
(display (car ls) p)
|
||
|
(let lp ((ls (cdr ls)) (i 1))
|
||
|
(cond
|
||
|
((pair? ls)
|
||
|
(display sep p)
|
||
|
(if (<= i rem) (write-char #\space p))
|
||
|
(display (car ls) p)
|
||
|
(lp (cdr ls) (+ i 1)))))
|
||
|
(newline p)))
|
||
|
st))))
|
||
|
(define (justify-last ls st)
|
||
|
(nl ((fmt-join dsp ls " ") st)))
|
||
|
((fmt-let
|
||
|
'writer
|
||
|
(lambda (str st) (set! buffer (cons str buffer)) st)
|
||
|
(apply-cat ls))
|
||
|
st)
|
||
|
(wrap-fold (string-concatenate-reverse buffer)
|
||
|
st width string-width justify-line justify-last))))
|
||
|
|
||
|
(define (fmt-file path)
|
||
|
(lambda (st)
|
||
|
(call-with-input-file path
|
||
|
(lambda (p)
|
||
|
(let lp ((st st))
|
||
|
(let ((line (read-line p)))
|
||
|
(if (eof-object? line)
|
||
|
st
|
||
|
(lp (nl ((dsp line) st))))))))))
|
||
|
|
||
|
(define (line-numbers . o)
|
||
|
(let ((start (if (pair? o) (car o) 1)))
|
||
|
(fmt-join/range dsp start #f nl-str)))
|
||
|
|