figlet.lisp 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. ;;;; Copyright © 2023, Jaidyn Ann <jadedctrl@posteo.at>
  2. ;;;;
  3. ;;;; This program is free software: you can redistribute it and/or
  4. ;;;; modify it under the terms of the GNU General Public License as
  5. ;;;; published by the Free Software Foundation, either version 3 of
  6. ;;;; the License, or (at your option) any later version.
  7. ;;;;
  8. ;;;; This program is distributed in the hope that it will be useful,
  9. ;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. ;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. ;;;; GNU General Public License for more details.
  12. ;;;;
  13. ;;;; You should have received a copy of the GNU General Public License
  14. ;;;; along with this program. If not, see <https://www.gnu.org/licenses/>.
  15. ;;;; FIGLET
  16. ;;;; A package for parsing Figlet fonts into simple associative lists, for
  17. ;;;; devious text-rendering purposes.
  18. (defpackage :figlet
  19. (:export #:figlet-string #:figlet-font-plist #:save-font-to-file)
  20. (:use cl))
  21. (in-package :figlet)
  22. ;;; ———————————————————————————————————
  23. ;;; Misc. utilities
  24. ;;; ———————————————————————————————————
  25. (defun contains-char-p (character string)
  26. "Whether or not a STRING contains the given CHARACTER."
  27. (str:containsp (string character) string))
  28. (defun characters (string)
  29. "Return a list of a STRING’s characters."
  30. (loop for char across string
  31. collect char))
  32. (defun string->integer (string)
  33. "Convert a string to a number, potentially in 0x… hexadecimal form.
  34. If no number is parsed out, return NIL."
  35. (let ((radix (if (str:starts-with-p "0x" string)
  36. 16 10))
  37. (string (if (str:starts-with-p "0x" string)
  38. (subseq string 2) string)))
  39. (ignore-errors (parse-integer string :radix radix))))
  40. (defun unlines (strings)
  41. "Wrapper around STR:UNLINES that removes all non-strings from the STRINGS list."
  42. (str:unlines (remove-if-not #'stringp strings)))
  43. ;; TODO: Use MISMATCH in this function, instead of whatever I did!
  44. (defun count-in-a-row (item seq &key (count 0) (test #'eql) (from-end nil))
  45. "How many times the given ITEM is found in SEQ in-a-row. Starts from the head
  46. of the list, unless FROM-END is specified. The TEST defaults to #'eql."
  47. (if (> (length seq) 0)
  48. (let* ((target-element (elt seq
  49. (if from-end (- (length seq) 1)
  50. 0)))
  51. (remaining-seq (if from-end
  52. (subseq seq 0 (- (length seq) 1))
  53. (subseq seq 1))))
  54. (if (apply test (list target-element item))
  55. (count-in-a-row item remaining-seq
  56. :test test :from-end from-end
  57. :count (+ count 1))
  58. count))
  59. count))
  60. (defun most-in-a-row (item sequences &key (test #'eql) (from-end nil))
  61. "Sort a sequence by the longest amount of the given ITEM In a row. Checks for
  62. reptitions of ITEM from the head of the list, unless FROM-END is specified."
  63. (sort sequences (lambda (a b) (> (count-in-a-row item a :test test :from-end from-end)
  64. (count-in-a-row item b :test test :from-end from-end)))))
  65. (defun least-in-a-row (item sequences &key (test #'eql) (from-end nil))
  66. "Sort a sequence by the smallest amount of the given ITEM In a row. Checks for
  67. reptitions of ITEM from the head of the list, unless FROM-END is specified."
  68. (reverse (most-in-a-row item sequences :test test :from-end from-end)))
  69. (defun equalize-padding (string)
  70. "Equalize and minimize the padding between lines of a string."
  71. (let* ((lines (str:lines string))
  72. (least-padded-left (car (least-in-a-row #\space lines)))
  73. (least-padded-right (car (least-in-a-row #\space lines :from-end 't)))
  74. (left-padding (count-in-a-row #\space least-padded-left))
  75. (right-padding (count-in-a-row #\space least-padded-right :from-end 't)))
  76. (str:unlines
  77. (mapcar (lambda (line)
  78. (subseq line left-padding (- (length line) right-padding)))
  79. (str:lines string)))))
  80. ;;; ———————————————————————————————————
  81. ;;; Font-parsing
  82. ;;; ———————————————————————————————————
  83. (defun parse-lines (lines &optional (font-plist '()) (current-charcode 32))
  84. "Parse a list of lines from a Figlet font-file (.FLF) into a plist
  85. associating a character with its respective string in the font-file.
  86. (#\A \"TEXT-ART-A\" #\B \"TEXT-ART-B\" …)"
  87. (if lines
  88. (let* ((line (car lines))
  89. (sans-@ (string-trim "@" line)) ;; Lines are terminated by ‘@’
  90. (last-of-symbol-p (str:ends-with-p "@@" line)) ;; Character-art is terminated by ‘@@’
  91. (not-art-line-p (not (str:ends-with-p "@" line))) ;; If no @ at all, line’s a comment or header
  92. (first-word-num (string->integer (car (str:words line)))) ;; If header line, this’ll be a charcode
  93. (current-art (ignore-errors (getf font-plist (code-char current-charcode)))))
  94. (cond
  95. ;; This is a header for a new char-art of specific char-code.
  96. ((and not-art-line-p first-word-num)
  97. (parse-lines (cdr lines) font-plist first-word-num))
  98. ;; If a line of char-art, amass it!
  99. ((not not-art-line-p)
  100. (setf (getf font-plist (code-char current-charcode))
  101. (unlines (list current-art sans-@)))
  102. ;; We want to make sure unnecessary padding’s stripped!
  103. (when last-of-symbol-p
  104. (setf (getf font-plist (code-char current-charcode))
  105. (str:replace-all (getf font-plist :space-char) " "
  106. (equalize-padding (unlines (list current-art sans-@))))))
  107. (parse-lines (cdr lines) font-plist
  108. (if last-of-symbol-p
  109. (+ current-charcode 1)
  110. current-charcode)))
  111. ;; This is the first line of the file, the header line.
  112. ((str:starts-with-p "flf2a" line)
  113. (setf (getf font-plist :space-char)
  114. (subseq line 5 6)) ;; A char (often $) to substitute spaces.
  115. (parse-lines (cdr lines) font-plist current-charcode))
  116. ;; If none of the above, it’s a comment!
  117. ('t
  118. (setf (getf font-plist :comments)
  119. (unlines (list (getf font-plist :comments) line)))
  120. (parse-lines (cdr lines) font-plist current-charcode))))
  121. font-plist))
  122. (defun figlet-font-plist (font-path)
  123. "Parse a Figlet font-file (.FLF) into a plist associating a character
  124. with its respective string in the font-file.
  125. (#\A \"TEXT-ART-A\" #\B \"TEXT-ART-B\" …)"
  126. (parse-lines
  127. (str:lines
  128. (alexandria:read-file-into-string font-path))))
  129. ;;; ———————————————————————————————————
  130. ;;; Output of Figlet-style strings
  131. ;;; ———————————————————————————————————
  132. (defun figlet-string (string &key (font-path nil) (font-plist (figlet-font-plist font-path)))
  133. (if (contains-char-p #\newline string)
  134. (mapcar (lambda (line) (figlet-string line :font-path font-path :font-plist font-plist))
  135. (str:lines string))
  136. (let* ((char-lines
  137. (mapcar (lambda (char)
  138. (str:lines (getf font-plist char)))
  139. (characters string)))
  140. (lines’-parts
  141. (loop for i to (- (length (car char-lines)) 1)
  142. collect (mapcar (lambda (lines)
  143. (nth i lines))
  144. char-lines))))
  145. (str:unlines (mapcar (lambda (line-parts)
  146. (reduce #'str:concat line-parts))
  147. lines’-parts)))))
  148. ;;; ———————————————————————————————————
  149. ;;; Exporting
  150. ;;; ———————————————————————————————————
  151. (defun save-font-to-file (path font-plist &optional (package ":FIGLET") (variable "*font*"))
  152. "Given a parsed font plist, generate source-code that corresponds to it."
  153. (with-open-file (file-stream path :direction :output :if-exists :supersede)
  154. (format file-stream "(in-package ~A)~%(defparameter ~A~% (QUOTE ~S))"
  155. package variable font-plist)))