(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))) (defclass sectioned-format-string-command (compound-format-string-command) ()) (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) ;; TODO: format here causes inconsistent behavior :p (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.")) (defmethod print-format-representation ((commands list) s) (dolist (command commands) (print-format-representation command s))) (defmethod print-format-representation ((literal character) s) (princ literal s)) (defmethod print-format-representation ((literal integer) s) (princ literal s)) (defmethod print-format-representation ((literal string) s) (princ literal s)) (defmethod print-format-representation :around ((command format-string-command) s) (princ #\~ s) (print-format-modifiers command s) (when (colon-p command) (princ #\: s)) (when (at-p command) (princ #\@ s)) (call-next-method)) (defmethod print-format-representation ((command simple-format-string-command) s) (princ (format-char command) s)) (defmethod print-format-representation :before ((command compound-format-string-command) s) (princ (start-char command) s)) (defmethod print-format-representation ((command compound-format-string-command) s) (print-format-representation (contents command) s)) (defmethod print-format-representation :after ((command compound-format-string-command) s) (princ #\~ s) (princ (end-char command) s)) (defmethod print-format-representation ((command sectioned-format-string-command) s) (mapcar (op (princ _ s)) (intersperse "~;" (contents command)))) ;;; TODO: should this be a generic function? (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))) (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*)))) (defun define-simple-format-char (name format-char &key at-p colon-p) (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))) (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))) (defmacro define-compound-format-chars (&body specs) `(progn ,@(loop for spec in specs collect `(define-compound-format-char ,@spec)))) (defmacro define-simple-format-chars (&body specs) `(progn ,@(loop for spec in specs collect `(define-simple-format-char ,@spec)))) (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)) 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))) ,@body)) (defmacro format* (stream format-spec &rest args) `(format ,stream ,(make-format-string format-spec) ,@args)) (defmacro define-message (name (stream-arg &rest args) &body spec) "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." (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)))))) (define-format-chars ;; Iteration characters. (: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)) ;; Alignment characters. (: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)) ;; Conditional output (:sectioned :y-or-n (#\[ #\]) :colon-p t) ;; Case printing characters. (:compounds (:lowercase (#\( #\))) (:downcase (#\( #\))) (:uppercase (#\( #\)) :at-p t :colon-p t) (:upcase (#\( #\)) :at-p t :colon-p t) (:titlecase (#\( #\)) :colon-p t) (:capitalize (#\( #\)) :colon-p t) (:initialcap (#\( #\)) :at-p t)) (:compounds (:own-line (#\& #\%))) (:simples (:str (#\a)) (:repr (#\s)) (:float (#\f)) (:dec (#\d)) (:decimal (#\d)) (:hex (#\x)) (:hexadecimal (#\d)) (:oct (#\o)) (:octal (#\o)) (:currency (#\$)) (:exit (#\^)) (:go-to (#\*)) (:end-section (#\;)) (:fresh-line (#\&)) (:ensure-line (#\&)) (:new-line (#\%)))) #+null (progn (define-message hello (stream name) (:titlecase () "hello" #\space :str)) (define-message print-comma-separated (stream) (:own-line () (:rest () :str :exit ", "))))