Browse code
Initial Commit
Ed L authored on 17/03/2016 07:32:01
Showing 4 changed files
Showing 4 changed files
1 | 1 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,13 @@ |
1 |
+A simple package implementing a DSL for generating format strings. |
|
2 |
+ |
|
3 |
+```(Common Lisp) |
|
4 |
+ |
|
5 |
+(make-format-string '(:str)) #| ==> "~a" |# |
|
6 |
+(make-format-string '((:map () :str))) #| ==> "~{~a~}" |# |
|
7 |
+ |
|
8 |
+(define-message hello (name) |
|
9 |
+ "Hello " :str) |
|
10 |
+(define-message print-comma-separated (values) |
|
11 |
+ (:map () :str)) |
|
12 |
+ |
|
13 |
+``` |
0 | 14 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,11 @@ |
1 |
+;;;; format-string-builder.asd |
|
2 |
+ |
|
3 |
+(asdf:defsystem #:format-string-builder |
|
4 |
+ :description "Describe format-string-builder here" |
|
5 |
+ :author "fiddlerwoaroof" |
|
6 |
+ :license "MIT" |
|
7 |
+ :depends-on (:alexandria) |
|
8 |
+ :serial t |
|
9 |
+ :components ((:file "package") |
|
10 |
+ (:file "format-string-builder"))) |
|
11 |
+ |
0 | 12 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,235 @@ |
1 |
+(in-package #:format-string-builder) |
|
2 |
+ |
|
3 |
+(defvar *format-command-stream*) |
|
4 |
+ |
|
5 |
+(defclass format-string-command () |
|
6 |
+ ((at-p :initarg :at-p :accessor at-p :initform nil) |
|
7 |
+ (colon-p :initarg :colon-p :accessor colon-p :initform nil))) |
|
8 |
+ |
|
9 |
+(defclass simple-format-string-command (format-string-command) |
|
10 |
+ ((format-char :initarg :format-char :accessor format-char) |
|
11 |
+ (modifiers :initarg :modifiers :accessor modifiers :initform nil))) |
|
12 |
+ |
|
13 |
+(defclass compound-format-string-command (format-string-command) |
|
14 |
+ ((start-char :initarg :start-char :accessor start-char) |
|
15 |
+ (contents :initarg :contents :accessor contents :initform nil) |
|
16 |
+ (end-char :initarg :end-char :accessor end-char) |
|
17 |
+ (modifiers :initarg :modifiers :accessor modifiers :initform nil))) |
|
18 |
+ |
|
19 |
+(defmethod print-object ((obj compound-format-string-command) s) |
|
20 |
+ (declare (optimize (debug 3))) |
|
21 |
+ (print-unreadable-object (obj s :type t :identity t) |
|
22 |
+ (format s "~:@{\"~:[~;:~]~:[~;@~]~a...~a\" modifiers: ~s~}" |
|
23 |
+ (mapcar (lambda (x) (funcall x obj)) |
|
24 |
+ '(colon-p at-p start-char end-char modifiers))))) |
|
25 |
+ |
|
26 |
+(defgeneric print-format-modifiers (command stream) |
|
27 |
+ (:method ((command format-string-command) s) |
|
28 |
+ ; TODO: format here causes inconsistent behavior :p |
|
29 |
+ (mapl (lambda (x) |
|
30 |
+ (princ (car x) s) |
|
31 |
+ (when (cdr x) (princ #\, s))) |
|
32 |
+ (modifiers command)))) |
|
33 |
+ |
|
34 |
+(defgeneric print-format-representation (command stream) |
|
35 |
+ (:method ((commands list) s) |
|
36 |
+ (dolist (command commands) |
|
37 |
+ (print-format-representation command s))) |
|
38 |
+ (:method ((literal character) s) |
|
39 |
+ (princ literal s)) |
|
40 |
+ (:method ((literal string) s) |
|
41 |
+ (princ literal s)) |
|
42 |
+ |
|
43 |
+ (:method :around ((command format-string-command) s) |
|
44 |
+ (princ #\~ s) |
|
45 |
+ (call-next-method)) |
|
46 |
+ (:method :before ((command format-string-command) s) |
|
47 |
+ (when (colon-p command) |
|
48 |
+ (princ #\: s)) |
|
49 |
+ (when (at-p command) |
|
50 |
+ (princ #\@ s))) |
|
51 |
+ |
|
52 |
+ (:method :before ((command simple-format-string-command) s) |
|
53 |
+ (print-format-modifiers command s)) |
|
54 |
+ (:method ((command simple-format-string-command) s) |
|
55 |
+ (princ (format-char command) s)) |
|
56 |
+ |
|
57 |
+ (:method :before ((command compound-format-string-command) s) |
|
58 |
+ (print-format-modifiers command s)) |
|
59 |
+ (:method ((command compound-format-string-command) s) |
|
60 |
+ (princ (start-char command) s) |
|
61 |
+ (print-format-representation (contents command) s) |
|
62 |
+ (princ #\~ s) |
|
63 |
+ (princ (end-char command) s)) |
|
64 |
+ |
|
65 |
+ (:documentation "Prints the appropriate control sequence to the stream passed in |
|
66 |
+ The :before method will print the Tilde.")) |
|
67 |
+ |
|
68 |
+; TODO: should this be a generic function? |
|
69 |
+(defun convert-modifier (modifier) |
|
70 |
+ (flet ((validate-modifier (modifier) |
|
71 |
+ (when (member (elt (string modifier) 0) '(#\: #\@ #\,)) |
|
72 |
+ (error "Invalid modifier: ~s" modifier)) |
|
73 |
+ modifier)) |
|
74 |
+ (typecase modifier |
|
75 |
+ (character (concatenate 'string "'" (string (validate-modifier modifier)))) |
|
76 |
+ (string (validate-modifier modifier)) |
|
77 |
+ (null "") |
|
78 |
+ (t modifier)))) |
|
79 |
+ |
|
80 |
+(#-dev defvar |
|
81 |
+ #+dev defparameter *format-string-registry* (make-hash-table)) |
|
82 |
+ |
|
83 |
+(defun generate-function-defs () |
|
84 |
+ (loop for name being the hash-keys in *format-string-registry* using (hash-value spec) |
|
85 |
+ collect `(,name (&key colon-p at-p) |
|
86 |
+ (make-instance ,@spec :colon-p colon-p :at-p at-p)))) |
|
87 |
+ |
|
88 |
+(defgeneric dispatch-command (command args) |
|
89 |
+ (:method ((command simple-format-string-command) args) |
|
90 |
+ (setf (modifiers command) (mapcar #'convert-modifier args)) |
|
91 |
+ command) |
|
92 |
+ (:method ((command compound-format-string-command) args) |
|
93 |
+ (destructuring-bind ((&rest modifiers) &rest contents) args |
|
94 |
+ (setf (modifiers command) (mapcar #'convert-modifier modifiers)) |
|
95 |
+ (setf (contents command) (mapcar #'form-step contents)) |
|
96 |
+ command))) |
|
97 |
+ |
|
98 |
+(defun form-step (form) |
|
99 |
+ (flet ((dispatch-keyword (keyword &optional args) |
|
100 |
+ (if-let ((spec (gethash keyword *format-string-registry*))) |
|
101 |
+ (let ((result (apply #'make-instance spec))) |
|
102 |
+ (when args |
|
103 |
+ (dispatch-command result args)) |
|
104 |
+ result)))) |
|
105 |
+ (etypecase form |
|
106 |
+ (list (cond ((null form) nil) |
|
107 |
+ ((keywordp (car form)) (dispatch-keyword (car form) (cdr form))))) |
|
108 |
+ ((or character string) form) |
|
109 |
+ (keyword (dispatch-keyword form))))) |
|
110 |
+ |
|
111 |
+(defun make-format-string (forms) |
|
112 |
+ (with-output-to-string (*format-stream*) |
|
113 |
+ (print-format-representation |
|
114 |
+ (mapcar #'form-step (macroexpand forms)) |
|
115 |
+ *format-stream*))) |
|
116 |
+ |
|
117 |
+(defun %define-format-char (name format-char closing-char &key at-p colon-p) |
|
118 |
+ ; TODO: implement closing-char when we implement the block constructs. |
|
119 |
+ (declare (ignore closing-char)) |
|
120 |
+ (setf (gethash (intern (string name) :keyword) |
|
121 |
+ *format-string-registry*) |
|
122 |
+ `(simple-format-string-command :format-char ,format-char |
|
123 |
+ :at-p ,at-p |
|
124 |
+ :colon-p ,colon-p))) |
|
125 |
+ |
|
126 |
+(defun define-simple-format-char (name format-char &key at-p colon-p) |
|
127 |
+ (%define-format-char name format-char nil :at-p at-p :colon-p colon-p)) |
|
128 |
+ |
|
129 |
+(defun define-compound-format-char (name start-char end-char &key at-p colon-p) |
|
130 |
+ (setf (gethash (intern (string name) :keyword) |
|
131 |
+ *format-string-registry*) |
|
132 |
+ `(compound-format-string-command :start-char ,start-char |
|
133 |
+ :end-char ,end-char |
|
134 |
+ :at-p ,at-p |
|
135 |
+ :colon-p ,colon-p))) |
|
136 |
+ |
|
137 |
+; ('compound-format-string-command :start-char #\{ :end-char #\}) |
|
138 |
+ |
|
139 |
+(defmacro define-compound-format-chars (&body specs) |
|
140 |
+ (list* |
|
141 |
+ 'progn |
|
142 |
+ (loop for spec in specs |
|
143 |
+ collect `(define-compound-format-char ,@spec)))) |
|
144 |
+ |
|
145 |
+(defmacro define-simple-format-chars (&body specs) |
|
146 |
+ (list* |
|
147 |
+ 'progn |
|
148 |
+ (loop for spec in specs |
|
149 |
+ collect `(define-simple-format-char ,@spec)))) |
|
150 |
+ |
|
151 |
+(defmacro define-format-chars (&body body) |
|
152 |
+ `(macrolet ((:simple (name (char) &key at-p colon-p) |
|
153 |
+ `(define-simple-format-char ,name ,char |
|
154 |
+ :at-p ,at-p :colon-p ,colon-p)) |
|
155 |
+ (:simples (&rest specs) |
|
156 |
+ (list* 'progn |
|
157 |
+ (mapcar (lambda (spec) |
|
158 |
+ `(:simple ,@spec)) |
|
159 |
+ specs))) |
|
160 |
+ (:compound (name (start-char end-char) &key at-p colon-p) |
|
161 |
+ `(define-compound-format-char ,name ,start-char ,end-char |
|
162 |
+ :at-p ,at-p :colon-p ,colon-p)) |
|
163 |
+ (:compounds (&rest specs) |
|
164 |
+ (list* 'progn |
|
165 |
+ (mapcar (lambda (spec) |
|
166 |
+ `(:compound ,@spec)) |
|
167 |
+ specs)))) |
|
168 |
+ ,@body)) |
|
169 |
+ |
|
170 |
+(defmacro &format (stream format-spec &rest args) |
|
171 |
+ `(format ,stream |
|
172 |
+ (make-format-string ',format-spec) |
|
173 |
+ ,@args)) |
|
174 |
+ |
|
175 |
+(defmacro define-message (name (&rest args) &body spec) |
|
176 |
+ (with-gensyms (stream) |
|
177 |
+ `(defun ,name (,stream ,@args) |
|
178 |
+ (&format ,stream ,spec ,@args)))) |
|
179 |
+ |
|
180 |
+(define-format-chars |
|
181 |
+ |
|
182 |
+ ; Iteration characters. |
|
183 |
+ (:compounds |
|
184 |
+ (:map (#\{ #\})) |
|
185 |
+ (:rest (#\{ #\}) :at-p t) |
|
186 |
+ |
|
187 |
+ (:ap (#\{ #\}) :colon-p t) |
|
188 |
+ (:apply (#\{ #\}) :colon-p t) |
|
189 |
+ |
|
190 |
+ (:aprest (#\{ #\}) :at-p t :colon-p t) |
|
191 |
+ (:apply-rest (#\{ #\}) :at-p t :colon-p t)) |
|
192 |
+ |
|
193 |
+ ; Alignment characters. |
|
194 |
+ (:compounds |
|
195 |
+ (:spread (#\< #\>)) |
|
196 |
+ |
|
197 |
+ (:ljust (#\< #\>) :at-p t) |
|
198 |
+ (:left (#\< #\>) :at-p t) |
|
199 |
+ |
|
200 |
+ (:rjust (#\< #\>) :colon-p t) |
|
201 |
+ (:right (#\< #\>) :colon-p t) |
|
202 |
+ |
|
203 |
+ (:cjust (#\< #\>) :at-p t :colon-p t) |
|
204 |
+ (:center (#\< #\>) :at-p t :colon-p t)) |
|
205 |
+ |
|
206 |
+ ; Case printing characters. |
|
207 |
+ (:compounds |
|
208 |
+ (:lowercase (#\( #\))) |
|
209 |
+ (:uppercase (#\( #\)) :at-p t :colon-p t) |
|
210 |
+ (:titlecase (#\( #\)) :colon-p t) |
|
211 |
+ (:initialcap (#\( #\)) :at-p t)) |
|
212 |
+ |
|
213 |
+ (:compounds |
|
214 |
+ (:own-line (#\& #\%))) |
|
215 |
+ |
|
216 |
+ (:simples |
|
217 |
+ (:str (#\a)) |
|
218 |
+ (:repr (#\s)) |
|
219 |
+ (:float (#\f)) |
|
220 |
+ (:dec (#\d)) |
|
221 |
+ (:decimal (#\d)) |
|
222 |
+ (:hex (#\x)) |
|
223 |
+ (:oct (#\o)) |
|
224 |
+ (:currency (#\$)) |
|
225 |
+ (:exit (#\^)) |
|
226 |
+ (:go-to (#\*)) |
|
227 |
+ (:end-section (#\;)) |
|
228 |
+ (:ensure-line (#\&)) |
|
229 |
+ (:new-line (#\%)))) |
|
230 |
+ |
|
231 |
+(define-message hello (name) |
|
232 |
+ "Hello " :str) |
|
233 |
+ |
|
234 |
+(define-message print-comma-separated (values) |
|
235 |
+ (:map () :str :exit ", ")) |