;;; fmt.el --- Macro-based string interpolation library -*- lexical-binding: t; -*-
;; Copyright (C) 2016 Clément Pit-Claudel
;; Author: Clément Pit-Claudel
;; Keywords: convenience, extensions
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see .
;;; Commentary:
;;
;;; Code:
(defconst fmt--sigil ?$)
;;; Printing utilities
(defun fmt--print (e &optional printer format-sequence)
"Print E with PRINTER according to optional FORMAT-SEQUENCE.
With no FORMAT-SEQUENCE, use princ. With no PRINTER, use FORMAT."
(let ((print-quoted t)
(print-length nil)
(print-level nil))
(if format-sequence
(funcall (or printer 'format) format-sequence e)
(prin1-to-string e t))))
;;; Specialized printers
(defun fmt--printers-str (_format-string v)
"Format V as a string, but return \"\" if V is nil."
(if v (format "%s" v) ""))
(defun fmt--printers-whereis (_format-string function)
"Show a keybinding for FUNCTION."
(substitute-command-keys (concat "\\[" (symbol-name function) "]")))
(defvar fmt--printers-alist '(("str" . fmt--printers-str)
("date" . format-time-string)
("where-is" . fmt--printers-whereis))
"An alist mapping printer names to formatting functions.
Each formatting function should take a value and a format
sequence, and return a string.")
(defun fmt--get-printer (name)
"Find printer for NAME."
(or (cdr (assoc name fmt--printers-alist))
(error "Unrecognized printer: %S" name)))
;;; Compiling format strings
(defun fmt--read-delimited ()
"Read a string enclosed in a pair of delimiters."
(let ((start (point)))
(forward-list)
(buffer-substring-no-properties (1+ start) (1- (point)))))
(defun fmt--read-format-sequence ()
"Read a []-delimited format sequence, such as `[.2f]'."
(when (eq (char-after) ?\[)
(let ((str (fmt--read-delimited)))
(if (string-match-p "^%" str)
(list ''format str)
(let* ((parts (split-string str "|")))
(list (list 'quote (fmt--get-printer (car parts)))
(mapconcat #'identity (cdr parts) "|")))))))
(defun fmt--read-braces ()
"Read a {}-delimited form, such as `{12}'."
(pcase-let* ((start (point))
(expr (fmt--read-delimited))
(`(,obj . ,count) (read-from-string expr)))
(prog1 obj
(unless (= count (length expr))
(error "Expecting \"}\", but got %S instead"
(buffer-substring (+ start count 1) (point-max)))))))
(defun fmt--read-var-name ()
"Read an alphanumeric+dashes variable name, such as `ab0-cd'."
(let ((start (point)))
(if (> (skip-chars-forward "-[:alnum:]") 0)
(intern (buffer-substring-no-properties start (point)))
(error "Invalid fmt string when reaching %S"
(buffer-substring (point) (point-max))))))
(defun fmt--read ()
"Read one form, a lone sigil, or a variable, plus an optional format.
Returns a list (FORM PRINTER FORMAT-SEQUENCE)."
(let* ((format-sequence (fmt--read-format-sequence))
(next-char (char-after)))
(apply #'list
(cond
((equal next-char ?\() (read (current-buffer)))
((equal next-char ?\{) (fmt--read-braces))
((equal next-char fmt--sigil) (forward-char) (char-to-string fmt--sigil))
(t (fmt--read-var-name)))
format-sequence)))
(defun fmt--compile (expr)
"Parse and compile a format expression EXPR."
(with-temp-buffer
(insert expr)
(goto-char (point-min))
(let ((chunks nil)
(str-start (point))
(sigil-str (char-to-string fmt--sigil)))
(while (search-forward sigil-str nil t)
(let ((str (buffer-substring-no-properties str-start (1- (point)))))
(push str chunks)
(push `(fmt--print ,@(fmt--read)) chunks)
(setq str-start (point))))
(push (buffer-substring-no-properties str-start (point-max)) chunks)
(nreverse (seq-remove (lambda (x) (equal x "")) chunks)))))
;;; Public API
(defun fmt--1 (expr)
"Compile a format expressions EXPR."
(unless (stringp expr)
(error "`fmt' requires a constant string"))
(fmt--compile expr))
(defmacro fmt (&rest exprs)
"Compile a sequence of format expressions EXPRS."
(cons 'concat (apply #'append (mapcar #'fmt--1 exprs))))
(defmacro fmt-message (exprs)
"Display one or more messages EXPRS.
EXPRS is compiled as a format expression."
`(message "%s" (fmt ,@exprs)))
(provide 'fmt)
;;; fmt.el ends here