]> git.eshelyaron.com Git - emacs.git/commitdiff
Fix and extend format-spec (bug#41758)
authorBasil L. Contovounesios <contovob@tcd.ie>
Fri, 29 May 2020 18:56:14 +0000 (19:56 +0100)
committerBasil L. Contovounesios <contovob@tcd.ie>
Thu, 18 Jun 2020 11:46:21 +0000 (12:46 +0100)
* lisp/format-spec.el: Use lexical-binding.  Remove dependence on
subr-x.el.
(format-spec-make): Clarify docstring.
(format-spec--parse-modifiers): Rename to...
(format-spec--parse-flags): ...this and simplify.  In particular,
don't bother parsing :space-pad which is redundant and unused.
(format-spec--pad): Remove, replacing with...
(format-spec--do-flags): ...this new helper function which performs
more of format-spec's supported text manipulation.
(format-spec): Autoload.  Allow optional argument to take on special
values 'ignore' and 'delete' for more control over what happens when
a replacement for a format specification isn't provided.  Bring back
proper support for a precision modifier similar to that of 'format'.

* lisp/battery.el (battery-format): Rewrite in terms of format-spec.
(battery-echo-area-format, battery-mode-line-format): Mention
support of format-spec syntax in docstrings.

* doc/lispref/strings.texi (Custom Format Strings):
* etc/NEWS: Document and announce these changes.

* lisp/dired-aux.el (dired-do-compress-to):
* lisp/erc/erc-match.el (erc-log-matches):
* lisp/erc/erc.el (erc-update-mode-line-buffer):
* lisp/gnus/gnus-sieve.el (gnus-sieve-update):
* lisp/gnus/gssapi.el (open-gssapi-stream):
* lisp/gnus/mail-source.el (mail-source-fetch-file)
(mail-source-fetch-directory, mail-source-fetch-pop)
(mail-source-fetch-imap):
* lisp/gnus/message.el (message-insert-formatted-citation-line):
* lisp/image-dired.el:
* lisp/net/eww.el:
* lisp/net/imap.el (imap-kerberos4-open, imap-gssapi-open)
(imap-shell-open):
* lisp/net/network-stream.el (network-stream-open-shell):
* lisp/obsolete/tls.el (open-tls-stream):
* lisp/textmodes/tex-mode.el:
Remove extraneous loads and autoloads of format-spec now that it is
autoloaded and simplify its uses where possible.

* test/lisp/battery-tests.el (battery-format): Test new format-spec
support.
* test/lisp/format-spec-tests.el (test-format-spec): Rename to...
(format-spec) ...this, extending test cases.
(test-format-unknown): Rename to...
(format-spec-unknown): ...this, extending test cases.
(test-format-modifiers): Rename to...
(format-spec-flags): ...this.
(format-spec-make, format-spec-parse-flags, format-spec-do-flags)
(format-spec-do-flags-truncate, format-spec-do-flags-pad)
(format-spec-do-flags-chop, format-spec-do-flags-case): New tests.

19 files changed:
doc/lispref/strings.texi
etc/NEWS
lisp/battery.el
lisp/dired-aux.el
lisp/erc/erc-match.el
lisp/erc/erc.el
lisp/format-spec.el
lisp/gnus/gnus-sieve.el
lisp/gnus/gssapi.el
lisp/gnus/mail-source.el
lisp/gnus/message.el
lisp/image-dired.el
lisp/net/eww.el
lisp/net/imap.el
lisp/net/network-stream.el
lisp/obsolete/tls.el
lisp/textmodes/tex-mode.el
test/lisp/battery-tests.el
test/lisp/format-spec-tests.el

index 4a7bda57c4e0f9dc74d7f3940990b31dc737aade..2ef88b90254b4afdf3ee9d2d35320e22d56e11e8 100644 (file)
@@ -1152,7 +1152,7 @@ The function @code{format-spec} described in this section performs a
 similar function to @code{format}, except it operates on format
 control strings that use arbitrary specification characters.
 
-@defun format-spec template spec-alist &optional only-present
+@defun format-spec template spec-alist &optional ignore-missing
 This function returns a string produced from the format string
 @var{template} according to conversions specified in @var{spec-alist},
 which is an alist (@pxref{Association Lists}) of the form
@@ -1185,12 +1185,15 @@ The order of specifications in @var{template} need not correspond to
 the order of associations in @var{spec-alist}.
 @end itemize
 
-The optional argument @var{only-present} indicates how to handle
+The optional argument @var{ignore-missing} indicates how to handle
 specification characters in @var{template} that are not found in
 @var{spec-alist}.  If it is @code{nil} or omitted, the function
-signals an error.  Otherwise, those format specifications and any
-occurrences of @samp{%%} in @var{template} are left verbatim in the
-output, including their text properties, if any.
+signals an error; if it is @code{ignore}, those format specifications
+are left verbatim in the output, including their text properties, if
+any; if it is @code{delete}, those format specifications are removed
+from the output; any other non-@code{nil} value is handled like
+@code{ignore}, but any occurrences of @samp{%%} are also left verbatim
+in the output.
 @end defun
 
 The syntax of format specifications accepted by @code{format-spec} is
@@ -1238,7 +1241,7 @@ the right rather than the left.
 
 @item <
 This flag causes the substitution to be truncated on the left to the
-given width, if specified.
+given width and precision, if specified.
 
 @item >
 This flag causes the substitution to be truncated on the right to the
@@ -1257,9 +1260,12 @@ The result of using contradictory flags (for instance, both upper and
 lower case) is undefined.
 
 As is the case with @code{format}, a format specification can include
-a width, which is a decimal number that appears after any flags.  If a
-substitution contains fewer characters than its specified width, it is
-padded on the left:
+a width, which is a decimal number that appears after any flags, and a
+precision, which is a decimal-point @samp{.} followed by a decimal
+number that appears after any flags and width.
+
+If a substitution contains fewer characters than its specified width,
+it is padded on the left:
 
 @example
 @group
@@ -1269,6 +1275,17 @@ padded on the left:
 @end group
 @end example
 
+If a substitution contains more characters than its specified
+precision, it is truncated on the right:
+
+@example
+@group
+(format-spec "%.2a is truncated on the right"
+             '((?a . "alpha")))
+     @result{} "al is truncated on the right"
+@end group
+@end example
+
 Here is a more complicated example that combines several
 aforementioned features:
 
index d702f758f23a042161e69102608ccd030959b078..4d7302281394dae630c74cfadd192ee962017dd1 100644 (file)
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -461,6 +461,16 @@ In Emacs 24.3, the variable 'dbus-event-error-hooks' was renamed to
 The old names, which were kept as obsolete aliases of the new names,
 have now been removed.
 
+** Battery
+
+---
+*** A richer syntax can be used to format battery status information.
+The user options 'battery-mode-line-format' and
+'battery-echo-area-format' now support the full formatting syntax of
+the function 'format-spec' documented under '(elisp) Custom Format
+Strings'.  The new syntax includes specifiers for padding and
+truncation, amongst other things.
+
 \f
 * New Modes and Packages in Emacs 28.1
 
@@ -578,6 +588,13 @@ for encoding and decoding without having to bind
 It controls, whether 'process-file' returns a string when a remote
 process is interrupted by a signal.
 
++++
+** The behavior of 'format-spec' is now closer to that of 'format'.
+In order for the two functions to behave more consistently,
+'format-spec' now pads and truncates based on string width rather than
+length, and also supports format specifications that include a
+truncating precision field, such as '%.2a'.
+
 \f
 * Changes in Emacs 28.1 on Non-Free Operating Systems
 
index b8855a8ce371e582cd4340fb4ebc13626c078dc7..387281965070e531c42b7151dfd1f3430615432d 100644 (file)
@@ -121,7 +121,10 @@ string are substituted as defined by the current value of the variable
 %p Battery load percentage
 %m Remaining time (to charge or discharge) in minutes
 %h Remaining time (to charge or discharge) in hours
-%t Remaining time (to charge or discharge) in the form `h:min'"
+%t Remaining time (to charge or discharge) in the form `h:min'
+
+The full `format-spec' formatting syntax is supported."
+  :link '(info-link "(elisp) Custom Format Strings")
   :type '(choice string (const nil)))
 
 (defvar battery-mode-line-string nil
@@ -153,7 +156,10 @@ string are substituted as defined by the current value of the variable
 %p Battery load percentage
 %m Remaining time (to charge or discharge) in minutes
 %h Remaining time (to charge or discharge) in hours
-%t Remaining time (to charge or discharge) in the form `h:min'"
+%t Remaining time (to charge or discharge) in the form `h:min'
+
+The full `format-spec' formatting syntax is supported."
+  :link '(info-link "(elisp) Custom Format Strings")
   :type '(choice string (const nil)))
 
 (defcustom battery-update-interval 60
@@ -823,13 +829,7 @@ The following %-sequences are provided:
 
 (defun battery-format (format alist)
   "Substitute %-sequences in FORMAT."
-  (replace-regexp-in-string
-   "%."
-   (lambda (str)
-     (let ((char (aref str 1)))
-       (if (eq char ?%) "%"
-        (or (cdr (assoc char alist)) ""))))
-   format t t))
+  (format-spec format alist 'delete))
 
 (defun battery-search-for-one-match-in-files (files regexp match-num)
   "Search REGEXP in the content of the files listed in FILES.
index 0d481f4ac19c7c1977b691cc7679a819c389bf37..efb214088d8a773326ad615e7d8f69087b7175f4 100644 (file)
@@ -1064,8 +1064,6 @@ corresponding command.
 Within CMD, %i denotes the input file(s), and %o denotes the
 output file. %i path(s) are relative, while %o is absolute.")
 
-(declare-function format-spec "format-spec.el" (format specification))
-
 ;;;###autoload
 (defun dired-do-compress-to ()
   "Compress selected files and directories to an archive.
@@ -1073,7 +1071,6 @@ Prompt for the archive file name.
 Choose the archiving command based on the archive file-name extension
 and `dired-compress-files-alist'."
   (interactive)
-  (require 'format-spec)
   (let* ((in-files (dired-get-marked-files nil nil nil nil t))
          (out-file (expand-file-name (read-file-name "Compress to: ")))
          (rule (cl-find-if
@@ -1093,12 +1090,12 @@ and `dired-compress-files-alist'."
            (when (zerop
                   (dired-shell-command
                    (format-spec (cdr rule)
-                                `((?\o . ,(shell-quote-argument out-file))
-                                  (?\i . ,(mapconcat
-                                           (lambda (file-desc)
-                                             (shell-quote-argument (file-name-nondirectory
-                                                                    file-desc)))
-                                           in-files " "))))))
+                                `((?o . ,(shell-quote-argument out-file))
+                                  (?i . ,(mapconcat
+                                          (lambda (in-file)
+                                            (shell-quote-argument
+                                             (file-name-nondirectory in-file)))
+                                          in-files " "))))))
              (message (ngettext "Compressed %d file to %s"
                                "Compressed %d files to %s"
                                (length in-files))
index 3107ff2ccd1a2e1f27a4438cb385e99c572b61f8..0e98f2bc613e5257397180666d80495196555526 100644 (file)
@@ -555,16 +555,15 @@ See `erc-log-match-format'."
               (and (eq erc-log-matches-flag 'away)
                    (erc-away-time)))
           match-buffer-name)
-      (let ((line (format-spec erc-log-match-format
-                  (format-spec-make
-                   ?n nick
-                   ?t (format-time-string
-                       (or (and (boundp 'erc-timestamp-format)
-                                erc-timestamp-format)
-                           "[%Y-%m-%d %H:%M] "))
-                   ?c (or (erc-default-target) "")
-                   ?m message
-                   ?u nickuserhost))))
+      (let ((line (format-spec
+                   erc-log-match-format
+                   `((?n . ,nick)
+                     (?t . ,(format-time-string
+                             (or (bound-and-true-p erc-timestamp-format)
+                                 "[%Y-%m-%d %H:%M] ")))
+                     (?c . ,(or (erc-default-target) ""))
+                     (?m . ,message)
+                     (?u . ,nickuserhost)))))
        (with-current-buffer (erc-log-matches-make-buffer match-buffer-name)
          (let ((inhibit-read-only t))
            (goto-char (point-max))
index cfde84e19aafd35f0176225f37ffbd485001cb25..3880778794593761dd6cc8bbe8d1b68dd650d46f 100644 (file)
@@ -6391,17 +6391,16 @@ if `erc-away' is non-nil."
 (defun erc-update-mode-line-buffer (buffer)
   "Update the mode line in a single ERC buffer BUFFER."
   (with-current-buffer buffer
-    (let ((spec (format-spec-make
-                 ?a (erc-format-away-status)
-                 ?l (erc-format-lag-time)
-                 ?m (erc-format-channel-modes)
-                 ?n (or (erc-current-nick) "")
-                 ?N (erc-format-network)
-                 ?o (or (erc-controls-strip erc-channel-topic) "")
-                 ?p (erc-port-to-string erc-session-port)
-                 ?s (erc-format-target-and/or-server)
-                 ?S (erc-format-target-and/or-network)
-                 ?t (erc-format-target)))
+    (let ((spec `((?a . ,(erc-format-away-status))
+                  (?l . ,(erc-format-lag-time))
+                  (?m . ,(erc-format-channel-modes))
+                  (?n . ,(or (erc-current-nick) ""))
+                  (?N . ,(erc-format-network))
+                  (?o . ,(or (erc-controls-strip erc-channel-topic) ""))
+                  (?p . ,(erc-port-to-string erc-session-port))
+                  (?s . ,(erc-format-target-and/or-server))
+                  (?S . ,(erc-format-target-and/or-network))
+                  (?t . ,(erc-format-target))))
           (process-status (cond ((and (erc-server-process-alive)
                                       (not erc-server-connected))
                                  ":connecting")
index 9278bd74c42412967929cb5453060e9645fba3fe..6af79a44167aa3d43f977d9c7a4be22319ef2b8d 100644 (file)
@@ -1,4 +1,4 @@
-;;; format-spec.el --- functions for formatting arbitrary formatting strings
+;;; format-spec.el --- format arbitrary formatting strings -*- lexical-binding: t -*-
 
 ;; Copyright (C) 1999-2020 Free Software Foundation, Inc.
 
 
 ;;; Code:
 
-(eval-when-compile
-  (require 'subr-x))
-
-(defun format-spec (format specification &optional only-present)
+;;;###autoload
+(defun format-spec (format specification &optional ignore-missing)
   "Return a string based on FORMAT and SPECIFICATION.
 FORMAT is a string containing `format'-like specs like \"su - %u %k\".
 SPECIFICATION is an alist mapping format specification characters
@@ -39,22 +37,22 @@ For instance:
                \\=`((?u . ,(user-login-name))
                  (?l . \"ls\")))
 
-Each %-spec may contain optional flag and width modifiers, as
-follows:
+Each %-spec may contain optional flag, width, and precision
+modifiers, as follows:
 
-  %<flags><width>character
+  %<flags><width><precision>character
 
 The following flags are allowed:
 
 * 0: Pad to the width, if given, with zeros instead of spaces.
 * -: Pad to the width, if given, on the right instead of the left.
-* <: Truncate to the width, if given, on the left.
-* >: Truncate to the width, if given, on the right.
+* <: Truncate to the width and precision, if given, on the left.
+* >: Truncate to the width and precision, if given, on the right.
 * ^: Convert to upper case.
 * _: Convert to lower case.
 
-The width modifier behaves like the corresponding one in `format'
-when applied to %s.
+The width and truncation modifiers behave like the corresponding
+ones in `format' when applied to %s.
 
 For example, \"%<010b\" means \"substitute into the output the
 value associated with ?b in SPECIFICATION, either padding it with
@@ -64,89 +62,108 @@ characters wide\".
 Any text properties of FORMAT are copied to the result, with any
 text properties of a %-spec itself copied to its substitution.
 
-ONLY-PRESENT indicates how to handle %-spec characters not
+IGNORE-MISSING indicates how to handle %-spec characters not
 present in SPECIFICATION.  If it is nil or omitted, emit an
-error; otherwise leave those %-specs and any occurrences of
-\"%%\" in FORMAT verbatim in the result, including their text
-properties, if any."
+error; if it is the symbol `ignore', leave those %-specs verbatim
+in the result, including their text properties, if any; if it is
+the symbol `delete', remove those %-specs from the result;
+otherwise do the same as for the symbol `ignore', but also leave
+any occurrences of \"%%\" in FORMAT verbatim in the result."
   (with-temp-buffer
     (insert format)
     (goto-char (point-min))
     (while (search-forward "%" nil t)
       (cond
-        ;; Quoted percent sign.
-        ((eq (char-after) ?%)
-         (unless only-present
-          (delete-char 1)))
-        ;; Valid format spec.
-        ((looking-at "\\([-0 _^<>]*\\)\\([0-9.]*\\)\\([a-zA-Z]\\)")
-        (let* ((modifiers (match-string 1))
-                (num (match-string 2))
-               (spec (string-to-char (match-string 3)))
-               (val (assq spec specification)))
-          (if (not val)
-               (unless only-present
-                (error "Invalid format character: `%%%c'" spec))
-            (setq val (cdr val)
-                   modifiers (format-spec--parse-modifiers modifiers))
-            ;; Pad result to desired length.
-            (let ((text (format "%s" val)))
-               (when num
-                 (setq num (string-to-number num))
-                 (setq text (format-spec--pad text num modifiers))
-                 (when (> (length text) num)
-                   (cond
-                    ((memq :chop-left modifiers)
-                     (setq text (substring text (- (length text) num))))
-                    ((memq :chop-right modifiers)
-                     (setq text (substring text 0 num))))))
-               (when (memq :uppercase modifiers)
-                 (setq text (upcase text)))
-               (when (memq :lowercase modifiers)
-                 (setq text (downcase text)))
-              ;; Insert first, to preserve text properties.
-              (insert-and-inherit text)
-              ;; Delete the specifier body.
-               (delete-region (+ (match-beginning 0) (length text))
-                              (+ (match-end 0) (length text)))
-               ;; Delete the percent sign.
-               (delete-region (1- (match-beginning 0)) (match-beginning 0))))))
-        ;; Signal an error on bogus format strings.
-        (t
-          (unless only-present
-           (error "Invalid format string")))))
+       ;; Quoted percent sign.
+       ((= (following-char) ?%)
+        (when (memq ignore-missing '(nil ignore delete))
+          (delete-char 1)))
+       ;; Valid format spec.
+       ((looking-at (rx (? (group (+ (in " 0<>^_-"))))
+                        (? (group (+ digit)))
+                        (? (group ?. (+ digit)))
+                        (group alpha)))
+        (let* ((beg (point))
+               (end (match-end 0))
+               (flags (match-string 1))
+               (width (match-string 2))
+               (trunc (match-string 3))
+               (char (string-to-char (match-string 4)))
+               (text (assq char specification)))
+          (cond (text
+                 ;; Handle flags.
+                 (setq text (format-spec--do-flags
+                             (format "%s" (cdr text))
+                             (format-spec--parse-flags flags)
+                             (and width (string-to-number width))
+                             (and trunc (car (read-from-string trunc 1)))))
+                 ;; Insert first, to preserve text properties.
+                 (insert-and-inherit text)
+                 ;; Delete the specifier body.
+                 (delete-region (point) (+ end (length text)))
+                 ;; Delete the percent sign.
+                 (delete-region (1- beg) beg))
+                ((eq ignore-missing 'delete)
+                 ;; Delete the whole format spec.
+                 (delete-region (1- beg) end))
+                ((not ignore-missing)
+                 (error "Invalid format character: `%%%c'" char)))))
+       ;; Signal an error on bogus format strings.
+       ((not ignore-missing)
+        (error "Invalid format string"))))
     (buffer-string)))
 
-(defun format-spec--pad (text total-length modifiers)
-  (if (> (length text) total-length)
-      ;; The text is longer than the specified length; do nothing.
-      text
-    (let ((padding (make-string (- total-length (length text))
-                                (if (memq :zero-pad modifiers)
-                                    ?0
-                                  ?\s))))
-      (if (memq :right-pad modifiers)
-          (concat text padding)
-        (concat padding text)))))
-
-(defun format-spec--parse-modifiers (modifiers)
+(defun format-spec--do-flags (str flags width trunc)
+  "Return STR formatted according to FLAGS, WIDTH, and TRUNC.
+FLAGS is a list of keywords as returned by
+`format-spec--parse-flags'.  WIDTH and TRUNC are either nil or
+string widths corresponding to `format-spec' modifiers."
+  (let (diff str-width)
+    ;; Truncate original string first, like `format' does.
+    (when trunc
+      (setq str-width (string-width str))
+      (when (> (setq diff (- str-width trunc)) 0)
+        (setq str (if (memq :chop-left flags)
+                      (truncate-string-to-width str str-width diff)
+                    (format (format "%%.%ds" trunc) str))
+              ;; We know the new width so save it for later.
+              str-width trunc)))
+    ;; Pad or chop to width.
+    (when width
+      (setq str-width (or str-width (string-width str))
+            diff (- width str-width))
+      (cond ((zerop diff))
+            ((> diff 0)
+             (let ((pad (make-string diff (if (memq :pad-zero flags) ?0 ?\s))))
+               (setq str (if (memq :pad-right flags)
+                             (concat str pad)
+                           (concat pad str)))))
+            ((memq :chop-left flags)
+             (setq str (truncate-string-to-width str str-width (- diff))))
+            ((memq :chop-right flags)
+             (setq str (format (format "%%.%ds" width) str))))))
+  ;; Fiddle case.
+  (cond ((memq :upcase flags)
+         (upcase str))
+        ((memq :downcase flags)
+         (downcase str))
+        (str)))
+
+(defun format-spec--parse-flags (flags)
+  "Convert sequence of FLAGS to list of human-readable keywords."
   (mapcan (lambda (char)
-            (when-let ((modifier
-                        (pcase char
-                          (?0 :zero-pad)
-                          (?\s :space-pad)
-                          (?^ :uppercase)
-                          (?_ :lowercase)
-                          (?- :right-pad)
-                          (?< :chop-left)
-                          (?> :chop-right))))
-              (list modifier)))
-          modifiers))
+            (pcase char
+              (?0 (list :pad-zero))
+              (?- (list :pad-right))
+              (?< (list :chop-left))
+              (?> (list :chop-right))
+              (?^ (list :upcase))
+              (?_ (list :downcase))))
+          flags))
 
 (defun format-spec-make (&rest pairs)
   "Return an alist suitable for use in `format-spec' based on PAIRS.
-PAIRS is a list where every other element is a character and a value,
-starting with a character."
+PAIRS is a property list with characters as keys."
   (let (alist)
     (while pairs
       (unless (cdr pairs)
index 278e3a5d6f3c45379168df7254c6f4ce81a11e4d..5d8f9b55deb2db08094f2cb5661e048125ff5d6f 100644 (file)
@@ -29,8 +29,6 @@
 
 (require 'gnus)
 (require 'gnus-sum)
-(require 'format-spec)
-(autoload 'sieve-mode "sieve-mode")
 (eval-when-compile
   (require 'sieve))
 
@@ -88,10 +86,10 @@ See the documentation for these variables and functions for details."
   (save-buffer)
   (shell-command
    (format-spec gnus-sieve-update-shell-command
-               (format-spec-make ?f gnus-sieve-file
-                                 ?s (or (cadr (gnus-server-get-method
-                                               nil gnus-sieve-select-method))
-                                        "")))))
+                `((?f . ,gnus-sieve-file)
+                  (?s . ,(or (cadr (gnus-server-get-method
+                                    nil gnus-sieve-select-method))
+                             ""))))))
 
 ;;;###autoload
 (defun gnus-sieve-generate ()
index 218a1542e3ac7acfe77c4db5f3700bed073df04a..485d58ad94e22559d599b9a9c4aa533f1ad120ef 100644 (file)
@@ -25,8 +25,6 @@
 
 ;;; Code:
 
-(require 'format-spec)
-
 (defcustom gssapi-program (list
                           (concat "gsasl %s %p "
                                   "--mechanism GSSAPI "
@@ -53,12 +51,9 @@ tried until a successful connection is made."
               (coding-system-for-write 'binary)
               (process (start-process
                         name buffer shell-file-name shell-command-switch
-                        (format-spec
-                         cmd
-                         (format-spec-make
-                          ?s server
-                          ?p (number-to-string port)
-                          ?l user))))
+                         (format-spec cmd `((?s . ,server)
+                                            (?p . ,(number-to-string port))
+                                            (?l . ,user)))))
               response)
          (when process
            (while (and (memq (process-status process) '(open run))
index acf35a376a9bf6dd8926806823fed554f6c37c70..43180726c45d9fe4efa4ac82c727e7983af2c2c9 100644 (file)
@@ -24,7 +24,6 @@
 
 ;;; Code:
 
-(require 'format-spec)
 (eval-when-compile
   (require 'cl-lib)
   (require 'imap))
@@ -769,14 +768,14 @@ Deleting old (> %s day(s)) incoming mail file `%s'." diff bfile)
   "Fetcher for single-file sources."
   (mail-source-bind (file source)
     (mail-source-run-script
-     prescript (format-spec-make ?t mail-source-crash-box)
+     prescript `((?t . ,mail-source-crash-box))
      prescript-delay)
     (let ((mail-source-string (format "file:%s" path)))
       (if (mail-source-movemail path mail-source-crash-box)
          (prog1
              (mail-source-callback callback path)
            (mail-source-run-script
-            postscript (format-spec-make ?t mail-source-crash-box))
+             postscript `((?t . ,mail-source-crash-box)))
            (mail-source-delete-crash-box))
        0))))
 
@@ -784,7 +783,7 @@ Deleting old (> %s day(s)) incoming mail file `%s'." diff bfile)
   "Fetcher for directory sources."
   (mail-source-bind (directory source)
     (mail-source-run-script
-     prescript (format-spec-make ?t path) prescript-delay)
+     prescript `((?t . ,path)) prescript-delay)
     (let ((found 0)
          (mail-source-string (format "directory:%s" path)))
       (dolist (file (directory-files
@@ -793,7 +792,7 @@ Deleting old (> %s day(s)) incoming mail file `%s'." diff bfile)
                   (funcall predicate file)
                   (mail-source-movemail file mail-source-crash-box))
          (cl-incf found (mail-source-callback callback file))
-         (mail-source-run-script postscript (format-spec-make ?t path))
+          (mail-source-run-script postscript `((?t . ,path)))
          (mail-source-delete-crash-box)))
       found)))
 
@@ -803,8 +802,8 @@ Deleting old (> %s day(s)) incoming mail file `%s'." diff bfile)
     ;; fixme: deal with stream type in format specs
     (mail-source-run-script
      prescript
-     (format-spec-make ?p password ?t mail-source-crash-box
-                      ?s server ?P port ?u user)
+     `((?p . ,password) (?t . ,mail-source-crash-box)
+       (?s . ,server) (?P . ,port) (?u . ,user))
      prescript-delay)
     (let ((from (format "%s:%s:%s" server user port))
          (mail-source-string (format "pop:%s@%s" user server))
@@ -825,8 +824,8 @@ Deleting old (> %s day(s)) incoming mail file `%s'." diff bfile)
              (mail-source-fetch-with-program
               (format-spec
                program
-               (format-spec-make ?p password ?t mail-source-crash-box
-                                 ?s server ?P port ?u user))))
+                `((?p . ,password) (?t . ,mail-source-crash-box)
+                  (?s . ,server) (?P . ,port) (?u . ,user)))))
             (function
              (funcall function mail-source-crash-box))
             ;; The default is to use pop3.el.
@@ -863,8 +862,8 @@ Deleting old (> %s day(s)) incoming mail file `%s'." diff bfile)
                  (setq mail-source-new-mail-available nil))
              (mail-source-run-script
               postscript
-              (format-spec-make ?p password ?t mail-source-crash-box
-                                ?s server ?P port ?u user))
+               `((?p . ,password) (?t . ,mail-source-crash-box)
+                 (?s . ,server) (?P . ,port) (?u . ,user)))
              (mail-source-delete-crash-box)))
        ;; We nix out the password in case the error
        ;; was because of a wrong password being given.
@@ -1077,8 +1076,9 @@ This only works when `display-time' is enabled."
   "Fetcher for imap sources."
   (mail-source-bind (imap source)
     (mail-source-run-script
-     prescript (format-spec-make ?p password ?t mail-source-crash-box
-                                ?s server ?P port ?u user)
+     prescript
+     `((?p . ,password) (?t . ,mail-source-crash-box)
+       (?s . ,server) (?P . ,port) (?u . ,user))
      prescript-delay)
     (let ((from (format "%s:%s:%s" server user port))
          (found 0)
@@ -1143,8 +1143,8 @@ This only works when `display-time' is enabled."
       (kill-buffer buf)
       (mail-source-run-script
        postscript
-       (format-spec-make ?p password ?t mail-source-crash-box
-                        ?s server ?P port ?u user))
+       `((?p . ,password) (?t . ,mail-source-crash-box)
+         (?s . ,server) (?P . ,port) (?u . ,user)))
       found)))
 
 (provide 'mail-source)
index 5a6827af762ea6557576bdff99286be87d030f9a..fb560f0eab82b8e3d3c36c12ad066d3abcd3fe44 100644 (file)
 (require 'mail-parse)
 (require 'mml)
 (require 'rfc822)
-(require 'format-spec)
 (require 'dired)
 (require 'mm-util)
 (require 'rfc2047)
 (require 'puny)
-(require 'rmc)                 ; read-multiple-choice
-(eval-when-compile (require 'subr-x))  ; when-let*
+(require 'rmc)                          ; read-multiple-choice
+(eval-when-compile (require 'subr-x))
 
 (autoload 'mailclient-send-it "mailclient")
 
@@ -440,8 +439,8 @@ whitespace)."
 
 (defcustom message-elide-ellipsis "\n[...]\n\n"
   "The string which is inserted for elided text.
-This is a format-spec string, and you can use %l to say how many
-lines were removed, and %c to say how many characters were
+This is a `format-spec' string, and you can use %l to say how
+many lines were removed, and %c to say how many characters were
 removed."
   :type 'string
   :link '(custom-manual "(message)Various Commands")
@@ -3977,7 +3976,6 @@ This function uses `mail-citation-hook' if that is non-nil."
   "Cite function in the standard Message manner."
   (message-cite-original-1 nil))
 
-(autoload 'format-spec "format-spec")
 (autoload 'gnus-date-get-time "gnus-util")
 
 (defun message-insert-formatted-citation-line (&optional from date tz)
@@ -4002,20 +4000,18 @@ See `message-citation-line-format'."
   (when (or message-reply-headers (and from date))
     (unless from
       (setq from (mail-header-from message-reply-headers)))
-    (let* ((data (condition-case ()
-                    (funcall (if (boundp 'gnus-extract-address-components)
-                                 gnus-extract-address-components
-                               'mail-extract-address-components)
-                             from)
-                  (error nil)))
+    (let* ((data (ignore-errors
+                   (funcall (or (bound-and-true-p
+                                 gnus-extract-address-components)
+                                #'mail-extract-address-components)
+                            from)))
           (name (car data))
           (fname name)
           (lname name)
-          (net (car (cdr data)))
-          (name-or-net (or (car data)
-                           (car (cdr data)) from))
+           (net (cadr data))
+           (name-or-net (or name net from))
           (time
-           (when (string-match "%[^fnNFL]" message-citation-line-format)
+            (when (string-match-p "%[^FLNfn]" message-citation-line-format)
              (cond ((numberp (car-safe date)) date) ;; backward compatibility
                    (date (gnus-date-get-time date))
                    (t
@@ -4024,68 +4020,53 @@ See `message-citation-line-format'."
           (tz (or tz
                   (when (stringp date)
                     (nth 8 (parse-time-string date)))))
-          (flist
-           (let ((i ?A) lst)
-             (when (stringp name)
-               ;; Guess first name and last name:
-               (let* ((names (delq
-                              nil
-                              (mapcar
-                               (lambda (x)
-                                 (if (string-match "\\`\\(\\w\\|[-.]\\)+\\'"
-                                                   x)
-                                     x
-                                   nil))
-                               (split-string name "[ \t]+"))))
-                      (count (length names)))
-                 (cond ((= count 1)
-                        (setq fname (car names)
-                              lname ""))
-                       ((or (= count 2) (= count 3))
-                        (setq fname (car names)
-                              lname (mapconcat 'identity (cdr names) " ")))
-                       ((> count 3)
-                        (setq fname (mapconcat 'identity
-                                               (butlast names (- count 2))
-                                               " ")
-                              lname (mapconcat 'identity
-                                               (nthcdr 2 names)
-                                               " "))))
-                  (when (string-match "\\(.*\\),\\'" fname)
-                    (let ((newlname (match-string 1 fname)))
-                      (setq fname lname lname newlname)))))
-             ;; The following letters are not used in `format-time-string':
-             (push ?E lst) (push "<E>" lst)
-             (push ?F lst) (push (or fname name-or-net) lst)
-             ;; We might want to use "" instead of "<X>" later.
-             (push ?J lst) (push "<J>" lst)
-             (push ?K lst) (push "<K>" lst)
-             (push ?L lst) (push lname lst)
-             (push ?N lst) (push name-or-net lst)
-             (push ?O lst) (push "<O>" lst)
-             (push ?P lst) (push "<P>" lst)
-             (push ?Q lst) (push "<Q>" lst)
-             (push ?f lst) (push from lst)
-             (push ?i lst) (push "<i>" lst)
-             (push ?n lst) (push net lst)
-             (push ?o lst) (push "<o>" lst)
-             (push ?q lst) (push "<q>" lst)
-             (push ?t lst) (push "<t>" lst)
-             (push ?v lst) (push "<v>" lst)
-             ;; Delegate the rest to `format-time-string':
-             (while (<= i ?z)
-               (when (and (not (memq i lst))
-                          ;; Skip (Z,a)
-                          (or (<= i ?Z)
-                              (>= i ?a)))
-                 (push i lst)
-                 (push (condition-case nil
-                           (format-time-string (format "%%%c" i) time tz)
-                         (error (format ">%c<" i)))
-                       lst))
-               (setq i (1+ i)))
-             (reverse lst)))
-          (spec (apply 'format-spec-make flist)))
+           spec)
+      (when (stringp name)
+        ;; Guess first name and last name:
+        (let* ((names (seq-filter
+                       (lambda (s)
+                         (string-match-p (rx bos (+ (in word ?. ?-)) eos) s))
+                       (split-string name "[ \t]+")))
+               (count (length names)))
+          (cond ((= count 1)
+                 (setq fname (car names)
+                       lname ""))
+                ((or (= count 2) (= count 3))
+                 (setq fname (car names)
+                       lname (string-join (cdr names) " ")))
+                ((> count 3)
+                 (setq fname (string-join (butlast names (- count 2))
+                                          " ")
+                       lname (string-join (nthcdr 2 names) " "))))
+          (when (string-match "\\(.*\\),\\'" fname)
+            (let ((newlname (match-string 1 fname)))
+              (setq fname lname lname newlname)))))
+      ;; The following letters are not used in `format-time-string':
+      (push (cons ?E "<E>") spec)
+      (push (cons ?F (or fname name-or-net)) spec)
+      ;; We might want to use "" instead of "<X>" later.
+      (push (cons ?J "<J>") spec)
+      (push (cons ?K "<K>") spec)
+      (push (cons ?L lname) spec)
+      (push (cons ?N name-or-net) spec)
+      (push (cons ?O "<O>") spec)
+      (push (cons ?P "<P>") spec)
+      (push (cons ?Q "<Q>") spec)
+      (push (cons ?f from) spec)
+      (push (cons ?i "<i>") spec)
+      (push (cons ?n net) spec)
+      (push (cons ?o "<o>") spec)
+      (push (cons ?q "<q>") spec)
+      (push (cons ?t "<t>") spec)
+      (push (cons ?v "<v>") spec)
+      ;; Delegate the rest to `format-time-string':
+      (dolist (c (nconc (number-sequence ?A ?Z)
+                        (number-sequence ?a ?z)))
+        (unless (assq c spec)
+          (push (cons c (condition-case nil
+                            (format-time-string (format "%%%c" c) time tz)
+                          (error (format ">%c<" c))))
+                spec)))
       (insert (format-spec message-citation-line-format spec)))
     (newline)))
 
index 1cc38ba714bf573c691d0c36c9de347ff89a2542..6f297672caf3e910d5851d4dc945c4b03567c410 100644 (file)
 ;;; Code:
 
 (require 'dired)
-(require 'format-spec)
 (require 'image-mode)
 (require 'widget)
 
index 2a70560ca7bb8a16d0dc96d709ce97300d160b65..cf31d37f07258bcb9c0d954ebd2af83160b9dd14 100644 (file)
@@ -25,7 +25,6 @@
 ;;; Code:
 
 (require 'cl-lib)
-(require 'format-spec)
 (require 'shr)
 (require 'url)
 (require 'url-queue)
index aa10f0291fd8d4538ed80ebcd06d5c14fe85cf1d..a492dc8c7986fc7c51f36c830edf44d60021bd4e 100644 (file)
 ;;; Code:
 
 (eval-when-compile (require 'cl-lib))
-(require 'format-spec)
 (require 'utf7)
 (require 'rfc2104)
 ;; Hmm... digest-md5 is not part of Emacs.
@@ -517,12 +516,9 @@ sure of changing the value of `foo'."
             (process-connection-type imap-process-connection-type)
             (process (start-process
                       name buffer shell-file-name shell-command-switch
-                      (format-spec
-                       cmd
-                       (format-spec-make
-                        ?s server
-                        ?p (number-to-string port)
-                        ?l imap-default-user))))
+                       (format-spec cmd `((?s . ,server)
+                                          (?p . ,(number-to-string port))
+                                          (?l . ,imap-default-user)))))
             response)
        (when process
          (with-current-buffer buffer
@@ -583,12 +579,9 @@ sure of changing the value of `foo'."
             (process-connection-type imap-process-connection-type)
             (process (start-process
                       name buffer shell-file-name shell-command-switch
-                      (format-spec
-                       cmd
-                       (format-spec-make
-                        ?s server
-                        ?p (number-to-string port)
-                        ?l imap-default-user))))
+                       (format-spec cmd `((?s . ,server)
+                                          (?p . ,(number-to-string port))
+                                          (?l . ,imap-default-user)))))
             response)
        (when process
          (with-current-buffer buffer
@@ -701,13 +694,10 @@ sure of changing the value of `foo'."
              (process-connection-type imap-process-connection-type)
             (process (start-process
                       name buffer shell-file-name shell-command-switch
-                      (format-spec
-                       cmd
-                       (format-spec-make
-                        ?s server
-                        ?g imap-shell-host
-                        ?p (number-to-string port)
-                        ?l imap-default-user)))))
+                       (format-spec cmd `((?s . ,server)
+                                          (?g . ,imap-shell-host)
+                                          (?p . ,(number-to-string port))
+                                          (?l . ,imap-default-user))))))
        (when process
          (while (and (memq (process-status process) '(open run))
                      (set-buffer buffer) ;; XXX "blue moon" nntp.el bug
index 1d5cf382a84e143392a80af2f16383836b1c7441..1c371f598706e4d4f1798a52e609019e15ea33dc 100644 (file)
@@ -170,8 +170,8 @@ a greeting from the server.
 :nowait, if non-nil, says the connection should be made
 asynchronously, if possible.
 
-:shell-command is a format-spec string that can be used if :type
-is `shell'.  It has two specs, %s for host and %p for port
+:shell-command is a `format-spec' string that can be used if
+:type is `shell'.  It has two specs, %s for host and %p for port
 number.  Example: \"ssh gateway nc %s %p\".
 
 :tls-parameters is a list that should be supplied if you're
@@ -453,11 +453,7 @@ gnutls-boot (as returned by `gnutls-boot-parameters')."
                   (network-stream-command stream capability-command eo-capa)
                   'tls)))))))
 
-(declare-function format-spec "format-spec" (format spec))
-(declare-function format-spec-make "format-spec" (&rest pairs))
-
 (defun network-stream-open-shell (name buffer host service parameters)
-  (require 'format-spec)
   (let* ((capability-command (plist-get parameters :capability-command))
         (eoc                (plist-get parameters :end-of-command))
         (start (with-current-buffer buffer (point)))
@@ -467,9 +463,8 @@ gnutls-boot (as returned by `gnutls-boot-parameters')."
                                  shell-command-switch
                                  (format-spec
                                   (plist-get parameters :shell-command)
-                                  (format-spec-make
-                                   ?s host
-                                   ?p service))))))
+                                   `((?s . ,host)
+                                     (?p . ,service)))))))
     (when coding (if (consp coding)
                        (set-process-coding-system stream
                                                   (car coding)
index cd091c0108e1ffe0276584d80a296d5038234783..d1b215cbfb8859295221ab265539062723137d09 100644 (file)
@@ -47,9 +47,6 @@
 
 (require 'gnutls)
 
-(autoload 'format-spec "format-spec")
-(autoload 'format-spec-make "format-spec")
-
 (defgroup tls nil
   "Transport Layer Security (TLS) parameters."
   :group 'comm)
@@ -224,14 +221,11 @@ Fourth arg PORT is an integer specifying a port to connect to."
       (while (and (not done) (setq cmd (pop cmds)))
        (let ((process-connection-type tls-process-connection-type)
              (formatted-cmd
-              (format-spec
-               cmd
-               (format-spec-make
-                 ?t (car (gnutls-trustfiles))
-                ?h host
-                ?p (if (integerp port)
-                       (int-to-string port)
-                     port)))))
+               (format-spec cmd `((?t . ,(car (gnutls-trustfiles)))
+                                  (?h . ,host)
+                                  (?p . ,(if (integerp port)
+                                             (number-to-string port)
+                                           port))))))
          (message "Opening TLS connection with `%s'..." formatted-cmd)
          (setq process (start-process
                         name buffer shell-file-name shell-command-switch
index 1b302e34a73cb6b8952c34c09184fbc8d0fa88ea..e3d5759579a8cce67f8379bb1247719a92fbdd55 100644 (file)
@@ -2295,9 +2295,6 @@ FILE is typically the output DVI or PDF file."
             (setq uptodate nil)))))
      uptodate)))
 
-
-(autoload 'format-spec "format-spec")
-
 (defvar tex-executable-cache nil)
 (defun tex-executable-exists-p (name)
   "Like `executable-find' but with a cache."
index 052ae49a8001aa00795d2fb71e86a2ee9bfba895..4cb7470d884a9d3553cef51d6dfcfa4a3ac42291 100644 (file)
@@ -52,7 +52,7 @@
   "Test `battery-format'."
   (should (equal (battery-format "" ()) ""))
   (should (equal (battery-format "" '((?b . "-"))) ""))
-  (should (equal (battery-format "%a%b%p%%" '((?b . "-") (?p . "99")))
-                 "-99%")))
+  (should (equal (battery-format "%2a%-3b%.1p%%" '((?b . "-") (?p . "99")))
+                 "-  9%")))
 
 ;;; battery-tests.el ends here
index 23ee88c526979cd8e9b1008cdee35734cf8b28dc..11882217afb54a80627fab67fc7a4dd2270855aa 100644 (file)
 (require 'ert)
 (require 'format-spec)
 
-(ert-deftest test-format-spec ()
+(ert-deftest format-spec-make ()
+  "Test `format-spec-make'."
+  (should-not (format-spec-make))
+  (should-error (format-spec-make ?b))
+  (should (equal (format-spec-make ?b "b") '((?b . "b"))))
+  (should-error (format-spec-make ?b "b" ?a))
+  (should (equal (format-spec-make ?b "b" ?a 'a)
+                 '((?b . "b")
+                   (?a . a)))))
+
+(ert-deftest format-spec-parse-flags ()
+  "Test `format-spec--parse-flags'."
+  (should-not (format-spec--parse-flags nil))
+  (should-not (format-spec--parse-flags ""))
+  (should (equal (format-spec--parse-flags "-") '(:pad-right)))
+  (should (equal (format-spec--parse-flags " 0") '(:pad-zero)))
+  (should (equal (format-spec--parse-flags " -x0y< >^_z ")
+                 '(:pad-right :pad-zero :chop-left :chop-right
+                              :upcase :downcase))))
+
+(ert-deftest format-spec-do-flags ()
+  "Test `format-spec--do-flags'."
+  (should (equal (format-spec--do-flags "" () nil nil) ""))
+  (dolist (flag '(:pad-zero :pad-right :upcase :downcase
+                            :chop-left :chop-right))
+    (should (equal (format-spec--do-flags "" (list flag) nil nil) "")))
+  (should (equal (format-spec--do-flags "FOOBAR" '(:downcase :chop-right) 5 2)
+                 "   fo"))
+  (should (equal (format-spec--do-flags
+                  "foobar" '(:pad-zero :pad-right :upcase :chop-left) 5 2)
+                 "AR000")))
+
+(ert-deftest format-spec-do-flags-truncate ()
+  "Test `format-spec--do-flags' truncation."
+  (let (flags)
+    (should (equal (format-spec--do-flags "" flags nil 0) ""))
+    (should (equal (format-spec--do-flags "" flags nil 1) ""))
+    (should (equal (format-spec--do-flags "a" flags nil 0) ""))
+    (should (equal (format-spec--do-flags "a" flags nil 1) "a"))
+    (should (equal (format-spec--do-flags "a" flags nil 2) "a"))
+    (should (equal (format-spec--do-flags "asd" flags nil 0) ""))
+    (should (equal (format-spec--do-flags "asd" flags nil 1) "a")))
+  (let ((flags '(:chop-left)))
+    (should (equal (format-spec--do-flags "" flags nil 0) ""))
+    (should (equal (format-spec--do-flags "" flags nil 1) ""))
+    (should (equal (format-spec--do-flags "a" flags nil 0) ""))
+    (should (equal (format-spec--do-flags "a" flags nil 1) "a"))
+    (should (equal (format-spec--do-flags "a" flags nil 2) "a"))
+    (should (equal (format-spec--do-flags "asd" flags nil 0) ""))
+    (should (equal (format-spec--do-flags "asd" flags nil 1) "d"))))
+
+(ert-deftest format-spec-do-flags-pad ()
+  "Test `format-spec--do-flags' padding."
+  (let (flags)
+    (should (equal (format-spec--do-flags "" flags 0 nil) ""))
+    (should (equal (format-spec--do-flags "" flags 1 nil) " "))
+    (should (equal (format-spec--do-flags "a" flags 0 nil) "a"))
+    (should (equal (format-spec--do-flags "a" flags 1 nil) "a"))
+    (should (equal (format-spec--do-flags "a" flags 2 nil) " a")))
+  (let ((flags '(:pad-zero)))
+    (should (equal (format-spec--do-flags "" flags 0 nil) ""))
+    (should (equal (format-spec--do-flags "" flags 1 nil) "0"))
+    (should (equal (format-spec--do-flags "a" flags 0 nil) "a"))
+    (should (equal (format-spec--do-flags "a" flags 1 nil) "a"))
+    (should (equal (format-spec--do-flags "a" flags 2 nil) "0a")))
+  (let ((flags '(:pad-right)))
+    (should (equal (format-spec--do-flags "" flags 0 nil) ""))
+    (should (equal (format-spec--do-flags "" flags 1 nil) " "))
+    (should (equal (format-spec--do-flags "a" flags 0 nil) "a"))
+    (should (equal (format-spec--do-flags "a" flags 1 nil) "a"))
+    (should (equal (format-spec--do-flags "a" flags 2 nil) "a ")))
+  (let ((flags '(:pad-right :pad-zero)))
+    (should (equal (format-spec--do-flags "" flags 0 nil) ""))
+    (should (equal (format-spec--do-flags "" flags 1 nil) "0"))
+    (should (equal (format-spec--do-flags "a" flags 0 nil) "a"))
+    (should (equal (format-spec--do-flags "a" flags 1 nil) "a"))
+    (should (equal (format-spec--do-flags "a" flags 2 nil) "a0"))))
+
+(ert-deftest format-spec-do-flags-chop ()
+  "Test `format-spec--do-flags' chopping."
+  (let ((flags '(:chop-left)))
+    (should (equal (format-spec--do-flags "a" flags 0 nil) ""))
+    (should (equal (format-spec--do-flags "a" flags 1 nil) "a"))
+    (should (equal (format-spec--do-flags "asd" flags 0 nil) ""))
+    (should (equal (format-spec--do-flags "asd" flags 1 nil) "d")))
+  (let ((flags '(:chop-right)))
+    (should (equal (format-spec--do-flags "a" flags 0 nil) ""))
+    (should (equal (format-spec--do-flags "a" flags 1 nil) "a"))
+    (should (equal (format-spec--do-flags "asd" flags 0 nil) ""))
+    (should (equal (format-spec--do-flags "asd" flags 1 nil) "a"))))
+
+(ert-deftest format-spec-do-flags-case ()
+  "Test `format-spec--do-flags' case fiddling."
+  (dolist (flag '(:pad-zero :pad-right :chop-left :chop-right))
+    (let ((flags (list flag)))
+      (should (equal (format-spec--do-flags "a" flags nil nil) "a"))
+      (should (equal (format-spec--do-flags "A" flags nil nil) "A")))
+    (let ((flags (list flag :downcase)))
+      (should (equal (format-spec--do-flags "a" flags nil nil) "a"))
+      (should (equal (format-spec--do-flags "A" flags nil nil) "a")))
+    (let ((flags (list flag :upcase)))
+      (should (equal (format-spec--do-flags "a" flags nil nil) "A"))
+      (should (equal (format-spec--do-flags "A" flags nil nil) "A")))))
+
+(ert-deftest format-spec ()
+  (should (equal (format-spec "" ()) ""))
+  (should (equal (format-spec "a" ()) "a"))
+  (should (equal (format-spec "b" '((?b . "bar"))) "b"))
+  (should (equal (format-spec "%%%b%%b%b%%" '((?b . "bar"))) "%bar%bbar%"))
   (should (equal (format-spec "foo %b zot" `((?b . "bar")))
                  "foo bar zot"))
   (should (equal (format-spec "foo %-10b zot" '((?b . "bar")))
                  "foo bar        zot"))
   (should (equal (format-spec "foo %10b zot" '((?b . "bar")))
-                 "foo        bar zot")))
+                 "foo        bar zot"))
+  (should (equal-including-properties
+           (format-spec (propertize "a" 'a 'b) '((?a . "foo")))
+           #("a" 0 1 (a b))))
+  (let ((fmt (concat (propertize "%a" 'a 'b)
+                     (propertize "%%" 'c 'd)
+                     "%b"
+                     (propertize "%b" 'e 'f))))
+    (should (equal-including-properties
+             (format-spec fmt '((?b . "asd") (?a . "fgh")))
+             #("fgh%asdasd" 0 3 (a b) 3 4 (c d) 7 10 (e f))))))
 
-(ert-deftest test-format-unknown ()
+(ert-deftest format-spec-unknown ()
   (should-error (format-spec "foo %b %z zot" '((?b . "bar"))))
+  (should-error (format-spec "foo %b %%%z zot" '((?b . "bar"))))
   (should (equal (format-spec "foo %b %z zot" '((?b . "bar")) t)
                  "foo bar %z zot"))
-  (should (equal (format-spec "foo %b %z %% zot" '((?b . "bar")) t)
-                 "foo bar %z %% zot")))
+  (should (equal (format-spec "foo %4b %%%4z %%4 zot" '((?b . "bar")) t)
+                 "foo  bar %%%4z %%4 zot"))
+  (should (equal (format-spec "foo %4b %%%4z %%4 zot" '((?b . "bar")) 'ignore)
+                 "foo  bar %%4z %4 zot"))
+  (should (equal (format-spec "foo %4b %%%4z %%4 zot" '((?b . "bar")) 'delete)
+                 "foo  bar % %4 zot")))
 
-(ert-deftest test-format-modifiers ()
+(ert-deftest format-spec-flags ()
   (should (equal (format-spec "foo %10b zot" '((?b . "bar")))
                  "foo        bar zot"))
   (should (equal (format-spec "foo % 10b zot" '((?b . "bar")))