1;;; -*- mode:scheme; coding:utf-8; -*- 2;;; 3;;; srfi/%3a159/internal/base.scm - Combinator Formatting (util) 4;;; 5;;; Copyright (c) 2020 Takashi Kato <ktakashi@ymail.com> 6;;; 7;;; Redistribution and use in source and binary forms, with or without 8;;; modification, are permitted provided that the following conditions 9;;; are met: 10;;; 11;;; 1. Redistributions of source code must retain the above copyright 12;;; notice, this list of conditions and the following disclaimer. 13;;; 14;;; 2. Redistributions in binary form must reproduce the above copyright 15;;; notice, this list of conditions and the following disclaimer in the 16;;; documentation and/or other materials provided with the distribution. 17;;; 18;;; THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19;;; "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20;;; LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21;;; A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22;;; OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23;;; SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24;;; TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25;;; PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26;;; LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27;;; NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28;;; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29;;; 30 31#!nounboud 32(library (srfi :159 internal util) 33 (export downcased 34 fitted fitted/both fitted/left fitted/right fl 35 joined joined/dot joined/last joined/prefix 36 joined/range joined/suffix 37 nl 38 padded padded/both padded/left padded/right 39 space-to 40 tab-to trimmed trimmed/both trimmed/lazy trimmed/left trimmed/right 41 upcased) 42 (import (rnrs) 43 (only (rnrs r5rs) modulo quotient) 44 (srfi :159 internal base)) 45;;;;; util.scm 46;; show.scm -- additional combinator formatters 47;; Copyright (c) 2013-2017 Alex Shinn. All rights reserved. 48;; BSD-style license: http://synthcode.com/license.txt 49 50;;> A library of procedures for formatting Scheme objects to text in 51;;> various ways, and for easily concatenating, composing and 52;;> extending these formatters. 53 54;;> \section{Background} 55;;> 56;;> There are several approaches to text formatting. Building strings 57;;> to \scheme{display} is not acceptable, since it doesn't scale to 58;;> very large output. The simplest realistic idea, and what people 59;;> resort to in typical portable Scheme, is to interleave 60;;> \scheme{display} and \scheme{write} and manual loops, but this is 61;;> both extremely verbose and doesn't compose well. A simple concept 62;;> such as padding space can't be achieved directly without somehow 63;;> capturing intermediate output. 64;;> 65;;> The traditional approach is to use templates - typically strings, 66;;> though in theory any object could be used and indeed Emacs' 67;;> mode-line format templates allow arbitrary sexps. Templates can 68;;> use either escape sequences (as in C's \cfun{printf} and 69;;> \hyperlink["http://en.wikipedia.org/wiki/Format_(Common_Lisp)"]{CL's} 70;;> \scheme{format}) or pattern matching (as in Visual Basic's 71;;> \cfun{Format}, 72;;> \hyperlink["http://search.cpan.org/~dconway/Perl6-Form-0.04/Form.pm"}{Perl6's} 73;;> \cfun{form}, and SQL date formats). The primary disadvantage of 74;;> templates is the relative difficulty (usually impossibility) of 75;;> extending them, their opaqueness, and the unreadability that 76;;> arises with complex formats. Templates are not without their 77;;> advantages, but they are already addressed by other libraries such 78;;> as 79;;> \hyperlink["http://srfi.schemers.org/srfi-28/srfi-28.html"]{SRFI-28} 80;;> and 81;;> \hyperlink["http://srfi.schemers.org/srfi-48/srfi-48.html"]{SRFI-48}. 82;;> 83;;> This library takes a combinator approach. Formats are nested chains 84;;> of closures, which are called to produce their output as needed. 85;;> The primary goal of this library is to have, first and foremost, a 86;;> maximally expressive and extensible formatting library. The next 87;;> most important goal is scalability - to be able to handle 88;;> arbitrarily large output and not build intermediate results except 89;;> where necessary. The third goal is brevity and ease of use. 90 91;;> \section{Interface} 92 93;;> \procedure{(show out [args ...])} 94;;> 95;;> The primary interface. Analogous to CL's \scheme{format}, the first 96;;> argument is either an output port or a boolean, with \scheme{#t} 97;;> indicating \scheme{current-output-port} and \scheme{#f} indicating a 98;;> string port. The remaining arguments are formatters, combined as with 99;;> \scheme{each}, run with output to the given destination. If \var{out} 100;;> is \scheme{#f} then the accumulated output is returned, otherwise 101;;> the result is unspecified. 102 103;;> \section{Formatters} 104 105;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 106;; Spacing 107 108;;> Output a single newline. 109(define nl (displayed "\n")) 110 111;;> "Fresh line" - output a newline iff we're not at the start of a 112;;> fresh line. 113(define fl 114 (fn (col) (if (zero? col) nothing nl))) 115 116;;> Move to a given tab-stop (using spaces, not tabs). 117(define (tab-to . o) 118 (fn (col pad-char) 119 (let* ((tab-width (if (pair? o) (car o) 8)) 120 (rem (modulo col tab-width))) 121 (if (positive? rem) 122 (displayed (make-string (- tab-width rem) pad-char)) 123 nothing)))) 124 125;;> Move to an explicit column. 126(define (space-to where) 127 (fn (col pad-char) 128 (displayed (make-string (max 0 (- where col)) pad-char)))) 129 130;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 131;;; String transformations 132 133(define (with-string-transformer proc . ls) 134 (fn (output) 135 (let ((output* (lambda (str) (fn () (output (proc str)))))) 136 (with ((output output*)) (each-in-list ls))))) 137 138;;> Show each of \var{ls}, uppercasing all generated text. 139(define (upcased . ls) (apply with-string-transformer string-upcase ls)) 140 141;;> Show each of \var{ls}, lowercasing all generated text. 142(define (downcased . ls) (apply with-string-transformer string-downcase ls)) 143 144;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 145;; Padding and trimming 146 147;;> Pad the result of \scheme{(each-in-list ls)} to at least 148;;> \var{width} characters, equally applied to the left and right, 149;;> with any extra odd padding applied to the right. Uses the value 150;;> of \scheme{pad-char} for padding, defaulting to \scheme{#\\space}. 151(define (padded/both width . ls) 152 (call-with-output 153 (each-in-list ls) 154 (lambda (str) 155 (fn (string-width pad-char) 156 (let ((diff (- width (string-width str)))) 157 (if (positive? diff) 158 (let* ((diff/2 (quotient diff 2)) 159 (left (make-string diff/2 pad-char)) 160 (right (if (even? diff) 161 left 162 (make-string (+ 1 diff/2) pad-char)))) 163 (each left str right)) 164 (displayed str))))))) 165 166;;> As \scheme{padded/both} but only applies padding on the right. 167(define (padded/right width . ls) 168 (fn ((col1 col)) 169 (each (each-in-list ls) 170 (fn ((col2 col) pad-char) 171 (displayed (make-string (max 0 (- width (- col2 col1))) 172 pad-char)))))) 173 174;;> As \scheme{padded/both} but only applies padding on the left. 175(define (padded/left width . ls) 176 (call-with-output 177 (each-in-list ls) 178 (lambda (str) 179 (fn (string-width pad-char) 180 (let ((diff (- width (string-width str)))) 181 (each (make-string (max 0 diff) pad-char) str)))))) 182 183;;> An alias for \scheme{padded/left}. 184(define padded padded/left) 185 186;; General buffered trim - capture the output apply a trimmer. 187(define (trimmed/buffered width producer proc) 188 (call-with-output 189 producer 190 (lambda (str) 191 (fn (string-width) 192 (let* ((str-width (string-width str)) 193 (diff (- str-width width))) 194 (displayed (if (positive? diff) 195 (proc str str-width diff) 196 str))))))) 197 198;;> Trims the result of \scheme{(each-in-list ls)} to at most 199;;> \var{width} characters, removed from the right. If any characters 200;;> are removed, then the value of \scheme{ellipsis} (default empty) 201;;> is used in its place (trimming additional characters as needed to 202;;> be sure the final output doesn't exceed \var{width}). 203(define (trimmed/right width . ls) 204 (trimmed/buffered 205 width 206 (each-in-list ls) 207 (lambda (str str-width diff) 208 (fn (ellipsis string-width col) 209 (let* ((ell (if (char? ellipsis) (string ellipsis) (or ellipsis ""))) 210 (ell-len (string-width ell)) 211 (diff (- (+ str-width ell-len) width))) 212 (each (if (negative? diff) 213 nothing 214 (substring str 0 (- width ell-len))) 215 ell)))))) 216 217;;> As \scheme{trimmed} but removes from the left. 218(define (trimmed/left width . ls) 219 (trimmed/buffered 220 width 221 (each-in-list ls) 222 (lambda (str str-width diff) 223 (fn (ellipsis string-width) 224 (let* ((ell (if (char? ellipsis) (string ellipsis) (or ellipsis ""))) 225 (ell-len (string-width ell)) 226 (diff (- (+ str-width ell-len) width))) 227 (each ell 228 (if (negative? diff) 229 nothing 230 (string-copy str diff)))))))) 231 232;;> An alias for \scheme{trimmed/left}. 233(define trimmed trimmed/left) 234 235;;> As \scheme{trimmed} but removes equally from both the left and the 236;;> right, removing extra odd characters from the right, and inserting 237;;> \scheme{ellipsis} on both sides. 238(define (trimmed/both width . ls) 239 (trimmed/buffered 240 width 241 (each-in-list ls) 242 (lambda (str str-width diff) 243 (fn (ellipsis string-width) 244 (let* ((ell (if (char? ellipsis) (string ellipsis) (or ellipsis ""))) 245 (ell-len (string-width ell)) 246 (diff (- (+ str-width ell-len ell-len) width)) 247 (left (quotient diff 2)) 248 (right (- (string-width str) (quotient (+ diff 1) 2)))) 249 (if (negative? diff) 250 ell 251 (each ell (substring str left right) ell))))))) 252 253;;> A \scheme{trimmed}, but truncates and terminates immediately if 254;;> more than \var{width} characters are generated by \var{ls}. Thus 255;;> \var{ls} may lazily generate an infinite amount of output safely 256;;> (e.g. \scheme{write-simple} on an infinite list). The nature of 257;;> this procedure means only truncating on the right is meaningful. 258(define (trimmed/lazy width . ls) 259 (fn ((orig-output output) string-width) 260 (call-with-current-continuation 261 (lambda (return) 262 (let ((chars-written 0) 263 (output (or orig-output output-default))) 264 (define (output* str) 265 (let ((len (string-width str))) 266 (set! chars-written (+ chars-written len)) 267 (if (> chars-written width) 268 (let* ((end (max 0 (- len (- chars-written width)))) 269 (s (substring str 0 end))) 270 (each (output s) 271 (with! (output orig-output)) 272 (fn () (return nothing)))) 273 (output str)))) 274 (with ((output output*)) 275 (each-in-list ls))))))) 276 277;;> Fits the result of \scheme{(each-in-list ls)} to exactly 278;;> \var{width} characters, padding or trimming on the right as 279;;> needed. 280(define (fitted/right width . ls) 281 (padded/right width (trimmed/right width (each-in-list ls)))) 282 283;;> As \scheme{fitted} but pads/trims from the left. 284(define (fitted/left width . ls) 285 (padded/left width (trimmed/left width (each-in-list ls)))) 286 287;;> An alias for \scheme{fitted/left}. 288(define fitted fitted/left) 289 290;;> As \scheme{fitted} but pads/trims equally from both the left and 291;;> the right. 292(define (fitted/both width . ls) 293 (padded/both width (trimmed/both width (each-in-list ls)))) 294 295;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 296;; Joining and interspersing 297 298(define (joined/general elt-f last-f dot-f init-ls sep) 299 (fn () 300 (let lp ((ls init-ls)) 301 (cond 302 ((pair? ls) 303 (each (if (eq? ls init-ls) nothing sep) 304 ((if (and last-f (null? (cdr ls))) last-f elt-f) (car ls)) 305 (lp (cdr ls)))) 306 ((and dot-f (not (null? ls))) 307 (each (if (eq? ls init-ls) nothing sep) (dot-f ls))) 308 (else 309 nothing))))) 310 311;;> \procedure{(joined elt-f ls [sep])} 312;;> 313;;> Joins the result of applying \var{elt-f} to each element of the 314;;> list \var{ls} together with \var{sep}, which defaults to the empty 315;;> string. 316(define (joined elt-f ls . o) 317 (joined/general elt-f #f #f ls (if (pair? o) (car o) ""))) 318 319;;> As \scheme{joined} but treats the separator as a prefix, inserting 320;;> before every element instead of between. 321(define (joined/prefix elt-f ls . o) 322 (if (null? ls) 323 nothing 324 (let ((sep (if (pair? o) (car o) ""))) 325 (each sep (joined elt-f ls sep))))) 326 327;;> As \scheme{joined} but treats the separator as a suffix, inserting 328;;> after every element instead of between. 329(define (joined/suffix elt-f ls . o) 330 (if (null? ls) 331 nothing 332 (let ((sep (if (pair? o) (car o) ""))) 333 (each (joined elt-f ls sep) sep)))) 334 335;;> As \scheme{joined} but applies \var{last-f}, instead of 336;;> \var{elt-f}, to the last element of \var{ls}, useful for 337;;> e.g. commas separating a list with "and" before the final element. 338(define (joined/last elt-f last-f ls . o) 339 (joined/general elt-f last-f #f ls (if (pair? o) (car o) ""))) 340 341;;> As \scheme{joined} but if \var{ls} is a dotted list applies 342;;> \var{dot-f} to the dotted tail as a final element. 343(define (joined/dot elt-f dot-f ls . o) 344 (joined/general elt-f #f dot-f ls (if (pair? o) (car o) ""))) 345 346;;> As \scheme{joined} but counts from \var{start} to \var{end} 347;;> (exclusive), formatting each integer in the range. If \var{end} 348;;> is \scheme{#f} or unspecified, produces an infinite stream of 349;;> output. 350(define (joined/range elt-f start . o) 351 (let ((end (and (pair? o) (car o))) 352 (sep (if (and (pair? o) (pair? (cdr o))) (cadr o) ""))) 353 (let lp ((i start)) 354 (if (and end (>= i end)) 355 nothing 356 (each (if (> i start) sep nothing) 357 (elt-f i) 358 (fn () (lp (+ i 1)))))))) 359 360) 361