* doc/lispref/text.texi (Interpolated Strings): New section.
* lisp/format-spec.el (format-spec--parse-modifiers)
(format-spec--pad): New functions.
(format-spec): Support more format modifiers (bug#32931).
of another buffer.
* Decompression:: Dealing with compressed data.
* Base 64:: Conversion to or from base 64 encoding.
+* Interpolated Strings:: Formatting Customizable Strings.
* Checksum/Hash:: Computing cryptographic hashes.
* GnuTLS Cryptography:: Cryptographic algorithms imported from GnuTLS.
* Parsing HTML/XML:: Parsing HTML and XML.
is optional, and the URL variant of base 64 encoding is used.
@end defun
+
+@node Interpolated Strings
+@section Formatting Customizable Strings
+
+It is, in some circumstances, useful to present users with a string to
+be customized that can then be expanded programmatically. For
+instance, @code{erc-header-line-format} is @code{"%n on %t (%m,%l)
+%o"}, and each of those characters after the percent signs are
+expanded when the header line is computed. To do this, the
+@code{format-spec} function is used:
+
+@defun format-spec format specification &optional only-present
+@var{format} is the format specification string as in the example
+above. @var{specification} is an alist that has elements where the
+@code{car} is a character and the @code{cdr} is the substitution.
+
+If @code{ONLY-PRESENT} is @code{nil}, errors will be signalled if a
+format character has been used that's not present in
+@var{specification}. If it's non-@code{nil}, that format
+specification is left verbatim in the result.
+@end defun
+
+Here's a trivial example:
+
+@example
+(format-spec "su - %u %l"
+ `((?u . ,(user-login-name))
+ (?l . "ls")))
+=> "su - foo ls"
+@end example
+
+In addition to allowing padding/limiting to a certain length, the
+following modifiers are can be used:
+
+@table @asis
+@item @samp{0}
+Use zero padding.
+
+@item @samp{@ }
+User space padding.
+
+@item @samp{-}
+Pad to the right.
+
+@item @samp{^}
+Use upper case.
+
+@item @samp{_}
+Use lower case.
+
+@item @samp{<}
+If the length needs to limited, remove characters from the left.
+
+@item @samp{>}
+Same as previous, but remove characters from the right.
+@end table
+
+If contradictory modifiers are used (for instance, both upper- and
+lower case), then what happens is undefined.
+
+As an example, @samp{"%<010b"} means ``insert the @samp{b} expansion,
+but pad with leading zeroes if it's less than ten characters, and if
+it's more than ten characters, shorten by removing characters from the
+left''.
+
+
@node Checksum/Hash
@section Checksum/Hash
@cindex MD5 checksum
space or non-breaking space as third argument, and "B" as fourth
argument, circumstances allowing.
++++
+** `format-spec' has been expanded with several modifiers to allow
+greater flexibility when customizing variables. The modifiers include
+zero-padding, upper- and lower-casing, and limiting the length of the
+interpolated strings. The function has now also been documented in
+the Emacs Lisp manual.
+
\f
* Changes in Emacs 27.1 on Non-Free Operating Systems
;;; Code:
+(require 'subr-x)
+
(defun format-spec (format specification &optional only-present)
"Return a string based on FORMAT and SPECIFICATION.
FORMAT is a string containing `format'-like specs like \"su - %u %k\",
For instance:
- (format-spec \"su - %u %k\"
+ (format-spec \"su - %u %l\"
`((?u . ,(user-login-name))
- (?k . \"ls\")))
+ (?l . \"ls\")))
+
+Each format spec can have modifiers, where \"%<010b\" means \"if
+the expansion is shorter than ten characters, zero-pad it, and if
+it's longer, chop off characters from the left size\".
+
+The following modifiers are allowed:
+
+* 0: Use zero-padding.
+* -: Pad to the right.
+* ^: Upper-case the expansion.
+* _: Lower-case the expansion.
+* <: Limit the length by removing chars from the left.
+* >: Limit the length by removing chars from the right.
Any text properties on a %-spec itself are propagated to the text
that it generates.
(unless only-present
(delete-char 1)))
;; Valid format spec.
- ((looking-at "\\([-0-9.]*\\)\\([a-zA-Z]\\)")
- (let* ((num (match-string 1))
- (spec (string-to-char (match-string 2)))
+ ((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))
+ (setq val (cdr val)
+ modifiers (format-spec--parse-modifiers modifiers))
;; Pad result to desired length.
- (let ((text (format (concat "%" num "s") val)))
+ (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.
(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)
+ (let ((elems nil))
+ (mapc (lambda (char)
+ (when-let ((modifier
+ (pcase char
+ (?0 :zero-pad)
+ (?\s :space-pad)
+ (?^ :uppercase)
+ (?_ :lowercase)
+ (?- :right-pad)
+ (?< :chop-left)
+ (?> :chop-right))))
+ (push modifier elems)))
+ modifiers)
+ elems))
+
(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,
(should (equal (format-spec "foo %b %z %% zot" '((?b . "bar")) t)
"foo bar %z %% zot")))
+(ert-deftest test-format-modifiers ()
+ (should (equal (format-spec "foo %10b zot" '((?b . "bar")))
+ "foo bar zot"))
+ (should (equal (format-spec "foo % 10b zot" '((?b . "bar")))
+ "foo bar zot"))
+ (should (equal (format-spec "foo %-010b zot" '((?b . "bar")))
+ "foo bar0000000 zot"))
+ (should (equal (format-spec "foo %0-10b zot" '((?b . "bar")))
+ "foo bar0000000 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"))
+ (should (equal (format-spec "foo %<4b zot" '((?b . "longbar")))
+ "foo gbar zot"))
+ (should (equal (format-spec "foo %>4b zot" '((?b . "longbar")))
+ "foo long zot")))
+
;;; format-spec-tests.el ends here