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