src/main.lisp @ 856fc364e2b0
default tip
Update URLs
author |
Steve Losh <steve@stevelosh.com> |
date |
Tue, 14 Jan 2020 20:10:12 -0500 |
parents |
3e34737c4a7e |
children |
(none) |
(in-package :bobbin)
(defun wrap-line (line width)
"Wrap the single-line string `line` to `width`, returning a multi-line string."
(with-output-to-string (*standard-output*)
(let ((pos 0)
(spaces 0)
(words (split-sequence:split-sequence #\space line))
(fresh t)
word
len)
(flet ((add (s)
(incf pos (length s))
(princ s))
(linebreak ()
(setf pos 0 spaces 0)
(terpri)))
(loop
:until (null words)
:do
(setf word (pop words)
len (length word))
(cond
;; chomp leading whitespace
((and (not fresh) (zerop pos) (zerop len))
nil)
;; if we have multiple spaces in a row, preserve them (maybe)
((zerop len)
(incf spaces))
;; if we're dealing with a single word that's too long, reluctantly
;; split it into pieces
((and (zerop pos) (> len width))
(setf fresh nil)
(add (subseq word 0 width))
(linebreak)
(push (subseq word width) words))
;; if this would send us beyond the limit, break
((> (+ spaces len pos) width)
(if fresh
(setf pos 0 spaces 0)
(linebreak))
(setf fresh nil)
(push word words))
;; otherwise concat
(t
(setf fresh nil)
(add (make-string spaces :initial-element #\space))
(add word)
(setf spaces 1))))))))
(defun wrap-lines (strings width)
"Wrap a list of `strings` to `width`, returning a list of strings."
;; This is mildly tricky because we want to correctly handle indented lines
;; inside of multiline strings inside the list.
(let ((lines (mapcan
;; Split and flatten any multiline strings in the list first.
(lambda (string)
(split-sequence:split-sequence #\newline string))
strings)))
(mapcan
;; Then wrap each string in the list and flatten the results.
(lambda (line)
(split-sequence:split-sequence #\newline (wrap-line line width)))
lines)))
(defun wrap-string (string width)
"Wrap a multi-line string, returning a multi-line string."
(format nil "~{~A~^~%~}"
(mapcar (lambda (line)
(wrap-line line width))
(split-sequence:split-sequence #\newline string))))
(defun wrap (string-or-strings width)
"Wrap `string-or-strings` to `width`.
`string-or-strings` can be a string or a list of strings. A list of strings
is treated as multiple lines. In either case the string(s) may also contain
newlines. All of these linebreaks will be included in the output — wrapping
will only add linebreaks, never remove them.
The result with be of the same type as the argument: either a single string
(containing newlines) or a list of strings (not containing newlines).
Examples:
(print (wrap (format nil \"foo bar baz\") 3))
foo
bar
baz
(print (wrap (format nil \"foo bar baz\") 7))
foo bar
baz
(print (wrap (format nil \"foo~%bar baz\") 7))
foo
bar baz
(print (wrap '(\"foo\" \"bar baz\") 7))
(\"foo\" \"bar baz\")
(print (wrap '(\"foo\" \"bar baz\") 3))
(\"foo\" \"bar\" \"baz\")
"
(check-type width (integer 1))
(etypecase string-or-strings
(string (wrap-string string-or-strings width))
(list (wrap-lines string-or-strings width))))