git.fiddlerwoaroof.com
format-string-builder.lisp
b33cd396
 (in-package #:format-string-builder)
 
 (defclass format-string-command ()
   ((at-p :initarg :at-p :accessor at-p :initform nil)
    (colon-p :initarg :colon-p :accessor colon-p :initform nil)))
 
 (defclass simple-format-string-command (format-string-command)
   ((format-char :initarg :format-char :accessor format-char)
    (modifiers :initarg :modifiers :accessor modifiers :initform nil)))
 
 (defclass compound-format-string-command (format-string-command)
   ((start-char :initarg :start-char :accessor start-char)
    (contents :initarg :contents :accessor contents :initform nil)
    (end-char :initarg :end-char :accessor end-char)
    (modifiers :initarg :modifiers :accessor modifiers :initform nil)))
 
851e82bb
 (defclass sectioned-format-string-command (compound-format-string-command)
   ())
 
b33cd396
 (defmethod print-object ((obj compound-format-string-command) s)
   (declare (optimize (debug 3)))
   (print-unreadable-object (obj s :type t :identity t)
     (format s "~:@{\"~:[~;:~]~:[~;@~]~a...~a\" modifiers: ~s~}"
             (mapcar (lambda (x) (funcall x obj))
                     '(colon-p at-p start-char end-char modifiers)))))
 
 (defgeneric print-format-modifiers (command stream)
   (:method ((command format-string-command) s)
35846370
      ;; TODO: format here causes inconsistent behavior :p
b33cd396
      (mapl (lambda (x)
              (princ (car x) s)
              (when (cdr x) (princ #\, s)))
            (modifiers command))))
 
 (defgeneric print-format-representation (command stream)
   (:documentation "Prints the appropriate control sequence to the stream passed in
                    The :before method will print the Tilde."))
 
4650fd48
 (defmethod print-format-representation ((commands list) s)
35846370
   (dolist (command commands)
     (print-format-representation command s)))
 
4650fd48
 (defmethod print-format-representation ((literal character) s)
35846370
   (princ literal s))
 
96de3510
 (defmethod print-format-representation ((literal integer) s)
   (princ literal s))
 
4650fd48
 (defmethod print-format-representation ((literal string) s)
35846370
   (princ literal s))
 
4650fd48
 (defmethod print-format-representation :around ((command format-string-command) s)
35846370
   (princ #\~ s)
851e82bb
   (print-format-modifiers command s)
35846370
   (when (colon-p command)
     (princ #\: s))
   (when (at-p command)
851e82bb
     (princ #\@ s))
   (call-next-method))
35846370
 
4650fd48
 (defmethod print-format-representation ((command simple-format-string-command) s)
35846370
   (princ (format-char command) s))
 
4650fd48
 (defmethod print-format-representation :before ((command compound-format-string-command) s)
851e82bb
   (princ (start-char command) s))
35846370
 
4650fd48
 (defmethod print-format-representation ((command compound-format-string-command) s)
851e82bb
   (print-format-representation (contents command) s))
 
 (defmethod print-format-representation :after ((command compound-format-string-command) s)
35846370
   (princ #\~ s)
   (princ (end-char command) s))
 
851e82bb
 (defmethod print-format-representation ((command sectioned-format-string-command) s)
   (mapcar (op (princ _ s))
 	  (intersperse "~;" (contents command))))
 
35846370
 ;;; TODO: should this be a generic function?
b33cd396
 (defun convert-modifier (modifier)
   (flet ((validate-modifier (modifier)
            (when (member (elt (string modifier) 0) '(#\: #\@ #\,))
              (error "Invalid modifier: ~s" modifier))
            modifier))
     (typecase modifier
       (character  (concatenate 'string "'" (string (validate-modifier modifier))))
       (string (validate-modifier modifier))
       (null "")
       (t modifier))))
 
 (defun generate-function-defs ()
   (loop for name being the hash-keys in *format-string-registry* using (hash-value spec)
         collect `(,name (&key colon-p at-p)
                         (make-instance ,@spec :colon-p colon-p :at-p at-p))))
 
 (defgeneric dispatch-command (command args)
   (:method ((command simple-format-string-command) args)
    (setf (modifiers command) (mapcar #'convert-modifier args))
    command)
   (:method ((command compound-format-string-command) args)
    (destructuring-bind ((&rest modifiers) &rest contents) args
      (setf (modifiers command) (mapcar #'convert-modifier modifiers))
      (setf (contents command) (mapcar #'form-step contents))
      command)))
 
4839fa21
 (eval-when (:compile-toplevel :load-toplevel :execute)
   (#-dev defvar #+dev defparameter
 	 *format-string-registry* (make-hash-table))
 
   (defun form-step (form)
     (flet ((dispatch-keyword (keyword &optional args)
 	     (if-let ((spec (gethash keyword *format-string-registry*)))
 	       (let ((result (apply #'make-instance spec)))
 		 (when args
 		   (dispatch-command result args))
 		 result))))
       (etypecase form
 	(list (cond ((null form) nil)
 		    ((keywordp (car form)) (dispatch-keyword (car form) (cdr form)))))
 	((or character string) form)
 	(keyword (dispatch-keyword form)))))
 
   (defun make-format-string (forms)
     (with-output-to-string (*format-stream*)
       (print-format-representation
        (mapcar #'form-step (macroexpand forms))
        *format-stream*))))
b33cd396
 
4650fd48
 (defun define-simple-format-char (name format-char &key at-p colon-p)
b33cd396
   (setf (gethash (intern (string name) :keyword)
                  *format-string-registry*)
         `(simple-format-string-command :format-char ,format-char
                                        :at-p ,at-p
                                        :colon-p ,colon-p)))
 
 (defun define-compound-format-char (name start-char end-char &key at-p colon-p)
   (setf (gethash (intern (string name) :keyword)
                  *format-string-registry*)
         `(compound-format-string-command :start-char ,start-char
                                          :end-char ,end-char
                                          :at-p ,at-p
                                          :colon-p ,colon-p)))
 
851e82bb
 (defun define-sectioned-format-char (name start-char end-char &key at-p colon-p)
   (setf (gethash (intern (string name) :keyword)
 		 *format-string-registry*)
 	`(sectioned-format-string-command :start-char ,start-char
 					  :end-char ,end-char
 					  :at-p ,at-p
 					  :colon-p ,colon-p)))
 
b33cd396
 (defmacro define-compound-format-chars (&body specs)
35846370
   `(progn
      ,@(loop for spec in specs
851e82bb
 	     collect `(define-compound-format-char ,@spec))))
b33cd396
 
 (defmacro define-simple-format-chars (&body specs)
35846370
   `(progn
      ,@(loop for spec in specs
              collect `(define-simple-format-char ,@spec))))
b33cd396
 
 (defmacro define-format-chars (&body body)
   `(macrolet ((:simple (name (char) &key at-p colon-p)
                 `(define-simple-format-char ,name ,char
                                             :at-p ,at-p :colon-p ,colon-p))
               (:simples (&rest specs)
                 (list* 'progn
                        (mapcar (lambda (spec)
                                  `(:simple ,@spec))
                                specs)))
               (:compound (name (start-char end-char) &key at-p colon-p)
                 `(define-compound-format-char ,name ,start-char ,end-char
                                               :at-p ,at-p :colon-p ,colon-p))
               (:compounds (&rest specs)
                 (list* 'progn
                        (mapcar (lambda (spec)
                                  `(:compound ,@spec))
851e82bb
                                specs)))
 	      (:sectioned (name (start-char end-char) &key at-p colon-p)
 		`(define-sectioned-format-char ,name ,start-char ,end-char
 					       :at-p ,at-p :colon-p ,colon-p)))
b33cd396
      ,@body))
 
851e82bb
 (defmacro format* (stream format-spec &rest args)
b33cd396
   `(format ,stream
851e82bb
            ,(make-format-string format-spec)
b33cd396
            ,@args))
 
851e82bb
 (defmacro define-message (name (stream-arg &rest args) &body spec)
96de3510
   "Define a function called NAME that takes a stream argument and a
 variable argument list that formats the arguments according to the
 spec passed as the body."
851e82bb
   (flet ((get-argument-names (arg-list)
            (loop for s in arg-list
                  when (and (symbolp s) (not (char= (elt (symbol-name s) 0) #\&))) collect s
                  when (consp s) collect (car s))))
     (with-gensyms (fs the-rest)
       `(let ((,fs ,(make-format-string spec)))
          (defun ,name (,stream-arg ,@args &rest ,the-rest)
            (apply #'format ,stream-arg ,fs ,@(get-argument-names args) ,the-rest))))))
b33cd396
 
 (define-format-chars
 
35846370
   ;; Iteration characters.
b33cd396
   (:compounds
     (:map (#\{ #\}))
     (:rest (#\{ #\}) :at-p t)
 
     (:ap (#\{ #\}) :colon-p t)
     (:apply (#\{ #\}) :colon-p t)
 
     (:aprest (#\{ #\}) :at-p t :colon-p t)
     (:apply-rest (#\{ #\}) :at-p t :colon-p t))
 
35846370
   ;; Alignment characters.
b33cd396
   (:compounds
     (:spread (#\< #\>))
 
     (:ljust (#\< #\>) :at-p t)
     (:left (#\< #\>) :at-p t)
 
     (:rjust (#\< #\>) :colon-p t)
     (:right (#\< #\>) :colon-p t)
 
     (:cjust (#\< #\>) :at-p t :colon-p t)
     (:center (#\< #\>) :at-p t :colon-p t))
 
851e82bb
   ;; Conditional output
   (:sectioned :y-or-n (#\[ #\]) :colon-p t)
 
35846370
   ;; Case printing characters.
b33cd396
   (:compounds
     (:lowercase (#\( #\)))
96de3510
     (:downcase (#\( #\)))
b33cd396
     (:uppercase (#\( #\)) :at-p t :colon-p t)
96de3510
     (:upcase (#\( #\)) :at-p t :colon-p t)
b33cd396
     (:titlecase (#\( #\)) :colon-p t)
96de3510
     (:capitalize (#\( #\)) :colon-p t)
b33cd396
     (:initialcap (#\( #\)) :at-p t))
 
   (:compounds
     (:own-line (#\& #\%)))
 
   (:simples
     (:str (#\a))
     (:repr (#\s))
     (:float (#\f))
     (:dec (#\d))
     (:decimal (#\d))
     (:hex (#\x))
96de3510
     (:hexadecimal (#\d))
b33cd396
     (:oct (#\o))
96de3510
     (:octal (#\o))
b33cd396
     (:currency (#\$))
     (:exit (#\^))
     (:go-to (#\*))
     (:end-section (#\;))
851e82bb
     (:fresh-line (#\&))
b33cd396
     (:ensure-line (#\&))
     (:new-line (#\%))))
 
4839fa21
 #+null
 (progn
   (define-message hello (stream name)
     (:titlecase () "hello" #\space :str))
b33cd396
 
4839fa21
   (define-message print-comma-separated (stream)
     (:own-line ()
 	       (:rest () :str :exit ", "))))