Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API

"J.P." <[email protected]> Fri, 01 May 2026 21:23:49 -0700
Newsgroups gmane.emacs.erc.general
Message-ID <[email protected]>
--=-=-=
Content-Type: text/plain

v9. Remove erc--offset marker. Nail down fallback procedure for finding
prefix-body boundary.

Unfortunately, a key benefit of this feature was based on a faulty
assumption: that a marker deposited during formatting to preserve the
boundary between the prefix and body portions of a message could endure
being exposed to user code. Of course, _new_ third-party code could be
asked to take special care when inserting at the boundary by appealing
to a dedicated utility to do so. But any _existing_ user code that
happened to insert text at the boundary would surely do the wrong thing
much of the time, e.g., use `insert-before-markers' on the body side.

This updated version instead borrows some heuristics from the
`fill-wrap' module and "crystallizes" them into a more durable means of
finding the start of the semantic body.

Once bug#68861 "Introduce a modern message-insertion API" is ready, we
can add another visitation opportunity during piecemeal insertion. That
feature's entire reason for existing is to do away with this horrible
boundary guessing game for good.


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment; filename=0000-v8-v9.diff
Content-Transfer-Encoding: quoted-printable

From 562186769eef1cde225548ff940ce20fb6fb4197 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Fri, 1 May 2026 20:21:51 -0700
Subject: [PATCH 0/6] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (6):
  [5.7] ; Remove some forward declarations from ERC tests
  [5.7] Preserve order of local ERC modules for activation
  [5.7] Split ERC module documentation into subnodes
  [5.7] Make prefix-body boundary more detectable in ERC
  [5.7] Introduce lower level erc-match API
  [5.7] Use erc-match-type API for erc-desktop-notifications

 doc/misc/erc.texi                             | 398 +++++++++++++++++-
 etc/ERC-NEWS                                  |  38 ++
 lisp/erc/erc-desktop-notifications.el         | 102 ++++-
 lisp/erc/erc-fill.el                          |  20 +-
 lisp/erc/erc-goodies.el                       |   6 +
 lisp/erc/erc-match.el                         | 384 +++++++++++++++--
 lisp/erc/erc.el                               |  47 ++-
 .../erc/erc-desktop-notifications-tests.el    | 140 ++++++
 test/lisp/erc/erc-match-tests.el              | 250 ++++++++++-
 .../erc/erc-scenarios-join-display-context.el |   2 +
 test/lisp/erc/erc-scenarios-log.el            |   6 +-
 test/lisp/erc/erc-scenarios-match-api.el      | 127 ++++++
 test/lisp/erc/erc-scenarios-match.el          |   5 +-
 test/lisp/erc/erc-scenarios-services-misc.el  |   3 +-
 test/lisp/erc/erc-tests.el                    | 104 ++++-
 .../erc/resources/erc-scenarios-common.el     | 103 ++---
 test/lisp/erc/resources/erc-tests-common.el   |   2 +-
 .../fill/snapshots/merge-01-start.eld         |   2 +-
 .../fill/snapshots/merge-02-right.eld         |   2 +-
 .../fill/snapshots/merge-wrap-01.eld          |   2 +-
 .../snapshots/merge-wrap-indicator-pre-01.eld |   2 +-
 .../fill/snapshots/monospace-01-start.eld     |   2 +-
 .../fill/snapshots/monospace-02-right.eld     |   2 +-
 .../fill/snapshots/monospace-03-left.eld      |   2 +-
 .../fill/snapshots/monospace-04-reset.eld     |   2 +-
 .../fill/snapshots/spacing-01-mono.eld        |   2 +-
 .../fill/snapshots/stamps-left-01.eld         |   2 +-
 .../match/functions/custom-match-log          |  39 ++
 .../erc/resources/match/functions/custom.eld  |  49 +++
 29 files changed, 1708 insertions(+), 137 deletions(-)
 create mode 100644 test/lisp/erc/erc-desktop-notifications-tests.el
 create mode 100644 test/lisp/erc/erc-scenarios-match-api.el
 create mode 100644 test/lisp/erc/resources/match/functions/custom-match-log
 create mode 100644 test/lisp/erc/resources/match/functions/custom.eld

Interdiff:
diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index 118e8e02fda..cc6820480cc 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -2115,11 +2115,11 @@ Match API
 @cindex low-level match
=20
 This section describes the low-level @samp{match} @acronym{API}
-introduced in ERC 5.7.  For basic, options-oriented usage, please see
-the doc strings for option @code{erc-pal-highlight-type} and friends in
-the @code{erc-match} group.  Unfortunately, those options often prove
+introduced in ERC 5.7.  For basic configuration help, please see the doc
+strings for option @code{erc-pal-highlight-type} and friends in the
+@code{erc-match} group.  Unfortunately, those options often prove
 insufficient for more granular filtering and highlighting needs, and
-advanced users eventually outgrow them.  However, under the hood, those
+advanced users may come to outgrow them.  However, under the hood, those
 options all use the same foundational @code{erc-match} API, which
 centers around a @code{cl-defstruct} @dfn{type} of the same name:
=20
@@ -2127,19 +2127,20 @@ Match API
   predicate spkr-beg spkr-end body-beg sender nick command handler
=20
   This is a @code{cl-struct} type that contains some handy facts about
-  the message being processed.  That message's formatted body occupies
-  the narrowed buffer when ERC creates and provides access to each
+  the message being processed.  That formatted message occupies the
+  narrowed buffer when ERC creates and provides access to each
   @code{erc-match} instance.  To use this interface, you add a
   @dfn{constructor}-like function to the hook
   @code{erc-match-functions}:
=20
   @defopt erc-match-functions
=20
-  An abnormal hook for which each member accepts the parameters named
-  above as an @samp{&rest}-style plist and returns a new
-  @code{erc-match} instance.  A function can also be a traditional
-  @code{cl-defstruct}-provided constructor belonging to a @dfn{subtype}
-  you've defined.
+  An abnormal hook for which each member function accepts the parameters
+  named above as an @samp{&rest}-style plist and returns a new
+  @code{erc-match} instance.  Conforming to this interface are, of
+  course, traditional constructors automatically provided by a
+  @code{cl-defstruct} definition derived from @code{erc-match}, such as
+  @samp{make-my-match} for some @dfn{subtype} @samp{my-match}.
=20
   @end defopt
=20
@@ -2150,34 +2151,36 @@ Match API
   ignores, should run.
=20
   A few slots, like @samp{spkr-beg}, @samp{spkr-end}, and @samp{nick},
-  may surprise you.  The first two are @code{nil} for non-chat messages,
-  like those displayed for @samp{JOIN} events.  The @samp{nick} slot can
+  may surprise you.  The first two are null for non-chat messages, like
+  those displayed for @samp{JOIN} events.  The @samp{nick} slot can
   likewise be @code{nil} if the sender of the message is a domain-style
   host name, such as @samp{irc.example.org}, which it often is for
   informational messages, like @samp{*** #chan was created on 2023-12-26
-  00:36:42}.
+  00:36:42}.  This includes, for example, server-sent @samp{NOTICE}s and
+  @samp{221} user-mode changes.
=20
-  To locate the start of the just-inserted message, use @samp{body-beg},
-  a marker indicating the beginning of the message proper.  Don't
-  forget: all inserted messages include a trailing newline.  If you want
-  to extract just the message body's text, use the function
-  @code{erc-match-get-message-body}:
+  To locate the start of the just-inserted message, use slot
+  @samp{body-beg}, a marker indicating the beginning of the message
+  proper.  Don't forget: all inserted messages include a trailing
+  newline.  If you want to extract just the message body's text, use the
+  function @code{erc-match-get-message-body}:
=20
   @defun erc-match-get-message-body match
=20
   Takes an @code{erc-match} instance and returns a string containing the
   message body, sans trailing newline and any leading speaker or
-  decorative component, such as @code{erc-notice-prefix}.
+  decorative component, such as @code{erc-notice-prefix}.  The match
+  buffer must be current.
=20
   @end defun
=20
 @end deftp
=20
 @noindent
-Although module authors may want to subclass this struct, everyday users
-can just instantiate it directly (it's @dfn{concrete}).  This is
-especially handy for one-off tasks or simple customizations in your
-@file{init.el}.  To do this, define a function that invokes its
+Unless you're writing a module, there's often no need to subclass
+@code{erc-match}.  For everyday tasks or simple customizations in your
+@file{init.el}, you can often just instantiate it directly (it's
+@dfn{concrete}).  To do this, define a function that invokes its
 constructor:
=20
 @lisp
@@ -2210,9 +2213,10 @@ Match API
 @defun erc-match-get-match constructor
=20
 When called from a @samp{handler} or a @samp{predicate} body, this
-utility returns instances of prior @code{erc-match-functions} that have
-already successfully matched the current message.  Use this for
-deduplication and to share data between match instances.
+utility returns the @code{erc-match} instance from the last successful
+match by @code{erc-match-functions} member @var{constructor} for the
+current message.  Use this for deduplication and to share data between
+match instances.
=20
 @end defun
=20
@@ -2221,24 +2225,23 @@ Match API
 the @samp{jabbycat} demo module, available on ERC's dev-oriented package
 archive: @uref{https://emacs-erc.gitlab.io/bugs/archive/jabbycat.html}.
 If you're in a hurry, check out @file{erc-desktop-notifications.el},
-which ships with ERC, but be aware that it's full of potentially
-confusing noise you'll want to ignore, such as everything that deals
-with adapting the global setup and teardown business to a buffer-local
-context.  Since your module is declared @code{local}, as per the modern
-convention, you won't be needing such code, so feel free to do things
-like add local members to @code{erc-match-functions} in your module's
-definition.
+which ships with ERC.  Just be aware that it's full of potentially
+confusing compatibility-related fuss you'll want to ignore, such as
+adapting the global setup and teardown business to a buffer-local
+context.  (New modules most likely adhere to the modern convention of
+being defined as @code{local} in order to allow things like adding local
+members to @code{erc-match-functions} in their @dfn{enable body}.)
=20
 @anchor{highlighting}
 @subsection Highlighting
 @cindex highlighting
=20
-End users and third-party modules likely want to manage and apply faces
-themselves.  If that's you, feel free to skip to the more extensive
-examples further below.  However, for the sake of completeness, it's
-worth mentioning that in a pinch, you can likely piggyback atop the
-highlighting functionality already provided by @samp{match} to support
-its many high-level options.
+Module authors and normal users alike will often want to manage and
+apply faces themselves.  If that's you, feel free to skip to the more
+extensive examples in the subsection below.  However, for the sake of
+completeness, it's worth mentioning that (especially in a pinch) you can
+likely piggyback atop the highlighting functionality already provided by
+@samp{match} to support its many high-level options.
=20
 @lisp
 (require 'erc-match)
@@ -2252,8 +2255,8 @@ Match API
=20
 (defun my-match (&rest plist)
   (apply #'erc-match-opt-keyword
-         :data (and-let* ((chans (alist-get (erc-network) my-keywords))
-                          ((cdr (assoc (erc-target) chans)))))
+         :data (and-let* ((chans (alist-get (erc-network) my-keywords)))
+                 (cdr (assoc (erc-target) chans)))
          :face 'my-face
          plist))
=20
@@ -2268,21 +2271,21 @@ Match API
 @deftp {Struct} erc-match-traditional category face data part
=20
 Use this type or one of its descendants (see below) if you want
-@code{erc-text-matched-hook} to run alongside (after) the @samp{handler}
+@code{erc-text-matched-hook} to run right after the @samp{handler}
 slot's default highlighter, @code{erc-match-highlight}, on every match
 for which the @samp{category} slot's value is non-@code{nil} (it becomes
 the argument provided for the hook's @var{match-type} parameter).
=20
-Much more important, however, is @samp{part}.  This slot determines what
-portion of the message is being highlighted or otherwise operated on.
-It can be any symbol, but the ones with predefined methods are
-@code{nick}, @code{message}, @code{all}, @code{keyword},
-@code{nick-or-keyword}, and @code{nick-or-mention}.
+The @samp{part} slot determines what portion of the message is being
+highlighted or otherwise operated on.  It can be any symbol, but the
+ones with predefined methods are @code{nick}, @code{message},
+@code{all}, @code{keyword}, @code{nick-or-keyword}, and
+@code{nick-or-mention}.
=20
-The complement to the @samp{part} slot is @samp{data}, which holds the
-value of the module's option corresponding to the specific type.  For
-example, ERC initializes the @samp{data} slot for the
-@code{erc-match-opt-pal} type with the value of @code{erc-pals}.
+Accompanying the @samp{part} slot is @samp{data}, which holds the value
+of the module's option corresponding to the specific type.  For example,
+ERC initializes the @samp{data} slot for the @code{erc-match-opt-pal}
+type with the value of @code{erc-pals}.
=20
 The default handler, @code{erc-match-highlight}, does its work by
 deferring to a purpose-built @dfn{method} meant to handle
@@ -2327,7 +2330,7 @@ Match API
   "Highlight messages written in Org markup."
   :group 'erc)
=20
-(defcustom erc-org-markup-targets '("#org")
+(defcustom erc-org-markup-targets '("#erc-org-markup")
   "List of buffers in which to highlight messages."
   :type '(repeat string))
=20
@@ -2352,6 +2355,7 @@ Match API
=20
 (defun erc-org-markup--should-p (match)
   "Return non-nil if MATCH describes an Org-markup worthy message."
+  ;; For demo purposes, treat all spoken chat messages as markup.
   (and erc-org-markup-mode (erc-match-nick match)))
=20
 (defun erc-org-markup-ensure-buffer ()
@@ -2449,23 +2453,23 @@ Match API
 Prefer an existing entry in RING.  If there isn't one, pick the first
 unused face in `erc-colorize-faces'.  Otherwise, pick the least used
 face."
-  (cond
-   ((and-let* ((i (catch 'found
-                    (dotimes (i (ring-length ring))
-                      (when (equal (car (ring-ref ring i)) nick)
-                        (throw 'found i))))))
-      (ring-insert ring (ring-remove ring i))
-      (cdr (ring-ref ring 0))))
-   ((let ((used (mapcar #'cdr (ring-elements ring))))
-      (and-let* ((face (catch 'found
-                         (dolist (face erc-colorize-faces)
-                           (unless (member face used)
-                             (throw 'found face))))))
-        (prog1 face
-          (ring-insert ring (cons nick face))))))
-   ((let ((older (ring-remove ring)))
-      (ring-insert ring (cons nick (cdr older)))
-      (cdr older)))))
+  (or
+   (and-let* ((i (catch 'found
+                   (dotimes (i (ring-length ring))
+                     (when (equal (car (ring-ref ring i)) nick)
+                       (throw 'found i))))))
+     (ring-insert ring (ring-remove ring i))
+     (cdr (ring-ref ring 0)))
+   (let ((used (mapcar #'cdr (ring-elements ring))))
+     (and-let* ((face (catch 'found
+                        (dolist (face erc-colorize-faces)
+                          (unless (member face used)
+                            (throw 'found face))))))
+       (prog1 face
+         (ring-insert ring (cons nick face)))))
+   (let ((older (ring-remove ring)))
+     (ring-insert ring (cons nick (cdr older)))
+     (cdr older))))
=20
 (cl-defstruct (erc-colorize ( :include erc-match
                               (predicate #'erc-colorize-nick)
diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index 4706ef90fe3..db7df2d50ae 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -706,19 +706,17 @@ erc-fill-wrap
     (let ((len (or (and erc-fill--wrap-length-function
                         (funcall erc-fill--wrap-length-function))
                    (and-let* ((msg-prop (erc--check-msg-prop 'erc--msg))
-                              ((not (eq msg-prop 'unknown))))
-                     (let ((dedentp (or erc-fill--wrap-action-dedent-p
-                                        (not (erc--check-msg-prop 'erc--ct=
cp
-                                                                  'ACTION)=
))))
-                       (if (and dedentp erc--offset-marker)
-                           (goto-char erc--offset-marker)
-                         ;; No marker means `datestamp' or refilling via
-                         ;; `erc-fill--wrap-unmerge-on-date-stamp', etc.
-                         (when-let* ((dedentp)
-                                     (bounds (erc--get-speaker-bounds)))
-                           (goto-char (cdr bounds)))
-                         (skip-syntax-forward "^-")
-                         (forward-char)))
+                              (_ (not (eq msg-prop 'unknown))))
+                     (if-let* ((fn (erc--check-msg-prop 'erc--pfx)))
+                         (let ((erc--ctcp-action-speaker-in-prefix-p
+                                erc-fill--wrap-action-dedent-p))
+                           (funcall fn))
+                       ;; FIXME remove this before merging.
+                       (unless (eq msg-prop 'datestamp)
+                         (erc--lwarn 'erc-fill :error
+                                     "Missing `erc--pfx' skip-fwd function=
: %S"
+                                     (list :msg-prop msg-prop
+                                           :buffer-string (buffer-string))=
)))
                      (cond ((eq msg-prop 'datestamp)
                             (when erc-fill--wrap-rejigger-last-message
                               (set-marker erc-fill--wrap-last-msg (point-m=
in)))
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index f1f92923ea2..cd6096f5133 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -631,6 +631,9 @@ erc--command-indicator-permit-insertion
           (erc--input-split-substxt state) #'erc--command-indicator-displa=
y)
     (erc-send-distinguish-noncommands state)))
=20
+(defun erc--command-indicator-body-find ()
+  (search-forward (erc--check-msg-prop 'erc--command-indicator) (pos-eol) =
t))
+
 ;; This function used to be called `erc-display-command'.  It was
 ;; neutered in ERC 5.3.x (Emacs 24.5), commented out in 5.4, removed
 ;; in 5.5, and restored in 5.6.
@@ -649,6 +652,9 @@ erc--command-indicator-display
                                             'hash-table)))))
         (when-let* ((string (erc-command-indicator))
                     (erc-input-marker (copy-marker erc-input-marker)))
+          (puthash 'erc--command-indicator string erc--msg-props)
+          (puthash 'erc--pfx #'erc--command-indicator-body-find
+                   erc--msg-props)
           (erc-display-prompt nil nil string 'erc-command-indicator-face)
           (remove-text-properties insert-position (point)
                                   '(field nil erc-prompt nil))
diff --git a/lisp/erc/erc-match.el b/lisp/erc/erc-match.el
index 9faed08c762..46184844a27 100644
--- a/lisp/erc/erc-match.el
+++ b/lisp/erc/erc-match.el
@@ -63,14 +63,28 @@ match
    (erc-buffer-do #'erc-match--setup)
    (erc--modify-local-map nil "C-c C-k" #'erc-go-to-log-matches-buffer)))
=20
+;; This caches the result of applying `regexp-opt' analogs to the
+;; regexp-based user options, mainly for history playback bursts.
+(defvar erc-match--opt-pat-cache nil
+  "Alist of (COMPUTE-FN . PAIRS) where PAIRS is an alist of (IN . OUT).")
+
+(defun erc-match--opt-pat-custom-set (sym val &optional _)
+  "Assign VAL to SYM via `set'."
+  (when erc-match--opt-pat-cache
+    (setq erc-match--opt-pat-cache nil))
+  (set sym val))
+
+
 ;; Remaining customizations
=20
 (defcustom erc-pals nil
   "List of pals on IRC."
+  :set #'erc-match--opt-pat-custom-set
   :type '(repeat regexp))
=20
 (defcustom erc-fools nil
   "List of fools on IRC."
+  :set #'erc-match--opt-pat-custom-set
   :type '(repeat regexp))
=20
 (defcustom erc-keywords nil
@@ -78,12 +92,14 @@ erc-keywords
 Each entry in the list is either a regexp, or a cons cell with the
 regexp in the car and the face to use in the cdr.  If no face is
 specified, `erc-keyword-face' is used."
+  :set #'erc-match--opt-pat-custom-set
   :type '(repeat (choice regexp
 			 (list regexp face))))
=20
 (defcustom erc-dangerous-hosts nil
   "List of regexps for hosts to highlight.
 Useful to mark nicks from dangerous hosts."
+  :set #'erc-match--opt-pat-custom-set
   :type '(repeat regexp))
=20
 (defcustom erc-current-nick-highlight-type 'keyword
@@ -345,7 +361,8 @@ erc-add-entry-to-list
 LIST must be passed as a symbol
 The query happens using PROMPT.
 Completion is performed on the optional alist COMPLETIONS."
-  (erc-match--opt-pat-cache-clear-all list)
+  (when erc-match--opt-pat-cache
+    (setq erc-match--opt-pat-cache nil))
   (let ((entry (completing-read
 		prompt
 		completions
@@ -369,7 +386,8 @@ erc-remove-entry-from-list
 LIST must be passed as a symbol.
 The elements of LIST can be strings, or cons cells where the
 car is the string."
-  (erc-match--opt-pat-cache-clear-all list)
+  (when erc-match--opt-pat-cache
+    (setq erc-match--opt-pat-cache nil))
   (let* ((alist (mapcar (lambda (x)
 			  (if (listp x)
 			      x
@@ -590,60 +608,12 @@ erc-match-directed-at-fool-p
                (:constructor erc-match-opt-dangerous-host))
   "An options-based type for the `dangerous-host' category.")
=20
-(defvar erc-match--opt-pat-cache nil
-  "Hash table of computed `regexp-opt' patterns from match-list options.
-Keys are cons cells of (CATEGORY . COMPUTE-FN).  Values are
-`erc-match--opt-pat' objects.  The table also contains an auxiliary item
-whose key is CATEGORY and whose value is a list of (COMPUTE-FN-1
-COMPUTE-FN-2 ... COMPUTE-FN-N).  ERC uses this when clearing the cache
-for CATEGORY.")
-
-(defvar erc-match--opt-pat-ttl 300.0
-  "Seconds to retain cached `regexp-opt' patterns between hits.")
-
-(cl-defstruct erc-match--opt-pat ts in out)
-
-(defun erc-match--opt-pat-cache-clear (base-key)
-  "Remove items for BASE-KEY from `erc-match--opt-pat-cache'."
-  (when-let* ((table erc-match--opt-pat-cache)
-              (keys (gethash base-key table)))
-    (remhash base-key table)
-    (dolist (key keys)
-      (remhash (cons base-key key) table))))
-
-;; FIXME have :set functions of user options also break cache.
-(defun erc-match--opt-pat-cache-clear-all (list-option)
-  "Remove items for LIST-OPTION from `erc-match--opt-pat-cache'."
-  (let ((base-key (pcase-exhaustive list-option
-                    ('erc-fools 'fool)
-                    ('erc-pals 'pal)
-                    ('erc-keywords 'keyword)
-                    ('erc-dangerous-hosts 'dangerous-host))))
-    (erc-match--opt-pat-cache-clear base-key)))
-
-(defun erc-match--opt-pat-get (base-key compute-fn input)
-  "Retrieve cached results for computing INPUT with COMPUTE-FN.
-Use BASE-KEY for `erc-match--opt-pat-cache' transactions."
-  (unless erc-match--opt-pat-cache
-    (setq erc-match--opt-pat-cache
-          (make-hash-table :test #'equal)))
-  (if-let* ((key (cons base-key compute-fn))
-            (entry (gethash key erc-match--opt-pat-cache))
-            (ct (erc-current-time))
-            ((> ct (+ (erc-match--opt-pat-ts entry)
-                      erc-match--opt-pat-ttl)))
-            ((equal (erc-match--opt-pat-in entry) input)))
-      (progn
-        (setf (erc-match--opt-pat-ts entry) ct)
-        (erc-match--opt-pat-out entry))
-    (let ((output (funcall compute-fn input)))
-      (prog1 output
-        (cl-pushnew compute-fn (gethash base-key erc-match--opt-pat-cache))
-        (puthash key
-                 (make-erc-match--opt-pat :ts (or ct (erc-current-time))
-                                          :in input
-                                          :out output)
-                 erc-match--opt-pat-cache)))))
+(defun erc-match--opt-pat-get (compute-fn input)
+  "Retrieve cached results for computing INPUT with COMPUTE-FN."
+  (with-memoization (alist-get input (alist-get compute-fn
+                                                erc-match--opt-pat-cache n=
il t)
+                               nil t)
+    (funcall compute-fn input)))
=20
 (defun erc-match--opt-pat-make (patterns)
   (string-join patterns "\\|"))
@@ -657,96 +627,89 @@ erc-match--opt-pat-addr-beg-make
 (defun erc-match--opt-pat-addr-end-make (patterns)
   (concat "\\s. \\(" (erc-match--opt-pat-make patterns) "\\)\\s."))
=20
-(defun erc-match--current-nick-p (instance)
-  (re-search-forward (car (erc-match-traditional-data instance)) nil t))
+(defun erc-match--current-nick-p (match)
+  (re-search-forward (car (erc-match-traditional-data match)) nil t))
=20
-(defun erc-match--keyword-p (instance)
-  (and-let* ((patterns (erc-match-traditional-data instance))
-             (regexp (erc-match--opt-pat-get
-                      (erc-match-traditional-category instance)
-                      #'erc-match--opt-pat-kw-make patterns)))
-    (goto-char (erc-match-body-beg instance))
-    (re-search-forward regexp nil t)))
+(defun erc-match--keyword-p (match)
+  "Return non-nil if the pattern given for MATCH's user option matches."
+  (and-let* ((patterns (erc-match-traditional-data match)))
+    (goto-char (erc-match-body-beg match))
+    (re-search-forward (erc-match--opt-pat-get #'erc-match--opt-pat-kw-make
+                                               patterns)
+                       nil t)))
=20
-(defun erc-match--user-nuh-or-mention-p (instance)
-  "Return non-nil on NUH match for `erc-match' INSTANCE.
+(defun erc-match--user-nuh-or-mention-p (match)
+  "Return non-nil on matching \"NUH\" for MATCH object.
 Also do so on mentions if the category is `fool' or the corresponding
 \"part\" option is `nick-or-mention'."
-  (and-let* ((patterns (erc-match-traditional-data instance))
-             (category (erc-match-traditional-category instance)))
-    (or (string-match (erc-match--opt-pat-get
-                       category #'erc-match--opt-pat-make patterns)
-                      (erc-match-sender instance))
-        (and (or (eq category 'fool)
-                 (eq (erc-match-traditional-part instance) 'nick-or-mentio=
n))
+  (and-let* ((patterns (erc-match-traditional-data match)))
+    (or (string-match (erc-match--opt-pat-get #'erc-match--opt-pat-make
+                                              patterns)
+                      (erc-match-sender match))
+        (and (or (eq (erc-match-traditional-category match) 'fool)
+                 (eq (erc-match-traditional-part match) 'nick-or-mention))
              ;; Mimic `erc-match-directed-at-fool-p', but search
              ;; the narrowed buffer instead of a string argument.
-             (goto-char (erc-match-body-beg instance))
+             (goto-char (erc-match-body-beg match))
              (or (looking-at (erc-match--opt-pat-get
-                              category #'erc-match--opt-pat-addr-beg-make
+                              #'erc-match--opt-pat-addr-beg-make
                               patterns))
                  (search-forward-regexp
-                  (erc-match--opt-pat-get
-                   category #'erc-match--opt-pat-addr-end-make patterns)
+                  (erc-match--opt-pat-get #'erc-match--opt-pat-addr-end-ma=
ke
+                                          patterns)
                   nil t))))))
=20
-(cl-defgeneric erc-match-highlight-by-part (instance part)
-  "Highlight PART of narrowed buffer for `erc-match' INSTANCE.")
+(cl-defgeneric erc-match-highlight-by-part (match part)
+  "Highlight PART of narrowed buffer for `erc-match' object MATCH.")
=20
-(cl-defmethod erc-match-highlight-by-part ((instance erc-match-traditional)
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
                                            (_ (eql nick)))
-  "Highlight nick in the bracketed speaker portion of the message."
-  (when (erc-match-spkr-beg instance)
-    (erc-put-text-property (erc-match-spkr-beg instance)
-                           (erc-match-spkr-end instance)
+  "Highlight MATCH's nick in the bracketed speaker portion of the message."
+  (when (erc-match-spkr-beg match)
+    (erc-put-text-property (erc-match-spkr-beg match)
+                           (erc-match-spkr-end match)
                            'font-lock-face
-                           (erc-match-traditional-face instance))))
+                           (erc-match-traditional-face match))))
=20
-(cl-defmethod erc-match-highlight-by-part ((instance erc-match-traditional)
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
                                            (_ (eql message)))
-  "Highlight the message body, not including the leading speaker tag."
-  (erc-put-text-property (erc-match-body-beg instance) (point-max)
-                         'font-lock-face
-                         (erc-match-traditional-face instance)))
+  "Highlight MATCH's message body, not including the leading speaker tag."
+  (erc-put-text-property (erc-match-body-beg match) (point-max)
+                         'font-lock-face (erc-match-traditional-face match=
)))
=20
-(cl-defmethod erc-match-highlight-by-part ((instance erc-match-traditional)
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
                                            (_ (eql all)))
-  "Highlight the whole message, including the speaker tag."
+  "Highlight MATCH's whole message, including the speaker tag."
   (erc-put-text-property (point-min) (point-max)
-                         'font-lock-face
-                         (erc-match-traditional-face instance)))
+                         'font-lock-face (erc-match-traditional-face match=
)))
=20
-(cl-defmethod erc-match-highlight-by-part ((instance erc-match-traditional)
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
                                            (_ (eql keyword)))
-  "Highlight all occurrences of all keyword patterns."
-  (dolist (pat (erc-match-traditional-data instance))
+  "Highlight all occurrences of all keyword patterns for MATCH."
+  (dolist (pat (erc-match-traditional-data match))
     (let ((regex (if (consp pat) (car pat) pat))
-          (face (if (consp pat)
-                    (cdr pat)
-                  (erc-match-traditional-face instance))))
-      (goto-char (erc-match-body-beg instance))
+          (face (if (consp pat) (cdr pat) (erc-match-traditional-face matc=
h))))
+      (goto-char (erc-match-body-beg match))
       (while (re-search-forward regex nil t)
         (erc-put-text-property (match-beginning 0) (match-end 0)
                                'font-lock-face face)))))
=20
-(cl-defmethod erc-match-highlight-by-part ((instance erc-match-traditional)
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
                                            (_ (eql nick-or-keyword)))
-  "Highlight speaker-tag nick of matching users, otherwise all mentions."
-  (if (erc-match-spkr-end instance)
-      (erc-put-text-property (erc-match-spkr-beg instance)
-                             (erc-match-spkr-end instance)
+  "Highlight MATCH's speaker-tag nick if applicable, otherwise all mention=
s."
+  (if (erc-match-spkr-end match)
+      (erc-put-text-property (erc-match-spkr-beg match)
+                             (erc-match-spkr-end match)
                              'font-lock-face
-                             (erc-match-traditional-face instance))
-    (erc-match-highlight-by-part instance 'keyword)))
+                             (erc-match-traditional-face match))
+    (erc-match-highlight-by-part match 'keyword)))
=20
-(cl-defmethod erc-match-highlight-by-part ((instance erc-match-traditional)
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
                                            (_ (eql nick-or-mention)))
-  "Highlight speaker-tag nick of matching users or all mentions."
-  (let ((body-beg (erc-match-body-beg instance)))
-    (setf (erc-match-body-beg instance)
-          (or (erc-match-spkr-beg instance) (point-min)))
-    (erc-match-highlight-by-part instance 'keyword)
-    (setf (erc-match-body-beg instance) body-beg)))
+  "Highlight MATCH's speaker tag nick of matching users or all mentions."
+  (cl-letf (((erc-match-body-beg match)
+             (or (erc-match-spkr-beg match) (point-min))))
+    (erc-match-highlight-by-part match 'keyword)))
=20
 (defvar erc-match-highlight-matched nil
   "Matched `erc-match' instance in `erc-text-matched-hook'.")
@@ -754,29 +717,28 @@ erc-match-highlight-matched
 (defvar erc-match--instances nil
   "Alist mapping constructors to successful `erc-match' instances.")
=20
-(defun erc-match-highlight (instance)
-  "Dispatch `erc-match-highlight-by-part' on INSTANCE's `:part' slot.
-Run `erc-text-matched-hook' when INSTANCE's `category' slot is non-nil."
-  (unless (erc-match-traditional-p instance)
-    (signal 'wrong-type-argument (list 'erc-match-traditional instance)))
-  (erc-match-highlight-by-part instance (erc-match-traditional-part instan=
ce))
-  (when (erc-match-traditional-category instance)
-    (let ((user-nuh (and (erc-match-nick instance)
-                         (erc-match-sender instance)))
-          (erc-match-highlight-matched instance))
+(defun erc-match-highlight (match)
+  "Dispatch `erc-match-highlight-by-part' on MATCH's `:part' slot.
+Run `erc-text-matched-hook' when MATCH's `category' slot is non-nil."
+  (unless (erc-match-traditional-p match)
+    (signal 'wrong-type-argument (list 'erc-match-traditional match)))
+  (erc-match-highlight-by-part match (erc-match-traditional-part match))
+  (when (erc-match-traditional-category match)
+    (let ((user-nuh (and (erc-match-nick match) (erc-match-sender match)))
+          (erc-match-highlight-matched match))
       (run-hook-with-args 'erc-text-matched-hook
-                          (erc-match-traditional-category instance)
+                          (erc-match-traditional-category match)
                           (or user-nuh (format "Server:%s"
-                                               (erc-match-command instance=
)))
+                                               (erc-match-command match)))
                           ;; For compatibility, include a leading "*** ".
                           (buffer-substring (if user-nuh
-                                                (erc-match-body-beg instan=
ce)
+                                                (erc-match-body-beg match)
                                               (point-min))
                                             (point-max))))))
=20
-(defun erc-match-get-message-body (instance)
-  "Return the message body in the narrowed buffer for match INSTANCE."
-  (buffer-substring (erc-match-body-beg instance) (1- (point-max))))
+(defun erc-match-get-message-body (match)
+  "Return the message body for MATCH in the current narrowed buffer."
+  (buffer-substring (erc-match-body-beg match) (1- (point-max))))
=20
 (defun erc-match-get-match (constructor)
   "Return successful `erc-match' instance for CONSTRUCTOR, if any.
@@ -796,9 +758,9 @@ erc-match--run-match
                                  :nick nick
                                  :sender sender
                                  :command command))
-              ((or nick (not (erc-match-user-p instance))))
-              ((goto-char (point-min)))
-              ((funcall (erc-match-predicate instance) instance)))
+              (_ (or nick (not (erc-match-user-p instance))))
+              (_ (goto-char (point-min)))
+              (_ (funcall (erc-match-predicate instance) instance)))
     (funcall (erc-match-handler instance) instance)
     (push (cons constructor instance) erc-match--instances)
     nil))
@@ -811,25 +773,20 @@ erc-match--message
          ;; Nick of sender's NUH if they are not the server itself.
          (nick (and user-nuh (or (erc--check-msg-prop 'erc--spkr)
                                  (erc-extract-nick user-nuh))))
-         (spkr-end (erc--get-speaker-bounds))
+         (unknownp (erc--check-msg-prop 'erc--msg 'unknown))
+         (spkr-end (and (not unknownp) (erc--get-speaker-bounds)))
          (spkr-beg (and spkr-end (pop spkr-end)))
-         (body-beg (copy-marker
-                    (cond (erc--offset-marker
-                           (marker-position erc--offset-marker))
-                          (spkr-end
-                           (save-excursion (goto-char spkr-end)
-                                           (skip-syntax-forward "^-")
-                                           (skip-syntax-forward "-")
-                                           (point)))
-                          ((point-min)))))
+         (body-beg (save-excursion
+                     (unless unknownp
+                       (when-let* ((fn (erc--check-msg-prop 'erc--pfx)))
+                         (funcall fn)))
+                     (point-marker)))
          (command (erc--check-msg-prop 'erc--cmd))
          (erc-match--instances ()))
     (with-syntax-table erc-match-syntax-table
       (run-hook-wrapped 'erc-match-functions #'erc-match--run-match
                         spkr-beg spkr-end body-beg nick
-                        (erc-response.sender response) command))
-    (when (and erc--offset-marker (/=3D body-beg erc--offset-marker))
-      (setq erc--offset-marker body-beg))))
+                        (erc-response.sender response) command))))
=20
 (defvar erc-match-use-legacy-logic-p nil
   "When non-nil, use the non-`erc-match' variant of `erc-match-message'.")
@@ -970,7 +927,7 @@ erc-log-matches
 See `erc-log-match-format'."
   (when-let*
       ((erc-log-matches-flag)
-       ((or (eq erc-log-matches-flag t) (erc-away-time)))
+       (_ (or (eq erc-log-matches-flag t) (erc-away-time)))
        (match-buffer-name (cdr (assq match-type erc-log-matches-types-alis=
t)))
        (line (format-spec
               erc-log-match-format
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 5a123441ce4..42fb41610c1 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -187,6 +187,9 @@ erc--msg-props
  - `erc--skip': list of symbols known to modules that indicate an
     intent to skip or simplify module-specific processing
=20
+ - `erc--pfx': function taking no args that advances point to the
+    start of the semantic body
+
  - `erc--ephemeral': a symbol prefixed by or matching a module
     name; indicates to other modules and members of modification
     hooks that the current message should not affect stateful
@@ -3220,8 +3223,7 @@ erc--send-action-display
       (let ((erc-current-message-catalog erc--message-speaker-catalog))
         (erc-display-message nil nil (current-buffer) 'ctcp-action-input
                              ?p (erc-get-channel-membership-prefix nick)
-                             ?n (erc--speakerize-nick nick)
-                             ?m (erc--ensure-offset-prop string))))))
+                             ?n (erc--speakerize-nick nick) ?m string)))))
=20
 (defun erc--send-action (target string force)
   "Display STRING, then send to TARGET as a \"CTCP ACTION\" message."
@@ -3238,18 +3240,15 @@ erc--ensure-spkr-prop
 and return it."
   (cond (erc--msg-props
          (puthash 'erc--spkr nick erc--msg-props)
+         (puthash 'erc--pfx #'erc--pfx-skip-spkr-fwd erc--msg-props)
          (dolist (entry overrides)
            (puthash (car entry) (cdr entry) erc--msg-props)))
         (erc--msg-prop-overrides
          (setq erc--msg-prop-overrides
-               `((erc--spkr . ,nick) ,@overrides ,@erc--msg-prop-overrides=
))))
+               `((erc--spkr . ,nick) (erc--pfx . ,#'erc--pfx-skip-spkr-fwd)
+                 ,@overrides ,@erc--msg-prop-overrides))))
   nick)
=20
-(defun erc--ensure-offset-prop (message)
-  "Add `erc--offset' msg prop for string MESSAGE."
-  (erc--add-msg-prop 'erc--offset (- (length message)))
-  message)
-
 (defun erc-string-invisible-p (string)
   "Check whether STRING is invisible or not.
 I.e. any char in it has the `invisible' property set."
@@ -3364,13 +3363,6 @@ erc--memq-msg-prop
              ((consp haystack)))
     (memq needle haystack)))
=20
-(defun erc--add-msg-prop (prop val)
-  "Add PROP and VAL to `erc--msg-props' or `erc--msg-prop-overrides'."
-  (cond (erc--msg-props
-         (puthash prop val erc--msg-props))
-        (erc--msg-prop-overrides
-         (setf (alist-get prop erc--msg-prop-overrides) val))))
-
 (defmacro erc--get-inserted-msg-beg-at (point at-start-p)
   (macroexp-let2* nil ((point point)
                        (at-start-p at-start-p))
@@ -3495,20 +3487,6 @@ erc--insert-line-function
 (defvar erc--insert-marker nil
   "Internal override for `erc-insert-marker'.")
=20
-(defvar erc--offset-marker nil
-  "Demarcates the header/body partition in a message.")
-
-(defmacro erc--with-offset-marker (&rest body)
-  "Run BODY in insertion-narrowed buffer with `erc--offset-marker' present=
."
-  `(let ((erc--offset-marker
-          (and-let* ((offset (erc--check-msg-prop 'erc--offset))
-                     (side (if (natnump offset) (point-min) (1- (point-max=
)))))
-            (remhash 'erc--offset erc--msg-props)
-            (copy-marker (+ side offset)))))
-     ,@body
-     (when erc--offset-marker
-       (set-marker erc--offset-marker nil))))
-
 (define-obsolete-function-alias 'erc-display-line-1 'erc-insert-line "30.1=
")
 (defun erc-insert-line (string buffer)
   "Insert STRING in an `erc-mode' BUFFER.
@@ -3566,9 +3544,8 @@ erc-insert-line
                 ;; run insertion hook, with point at restored location
                 (save-restriction
                   (narrow-to-region insert-position (point))
-                  (erc--with-offset-marker
-                   (run-hooks 'erc-insert-modify-hook)
-                   (run-hooks 'erc-insert-post-hook))
+                  (run-hooks 'erc-insert-modify-hook)
+                  (run-hooks 'erc-insert-post-hook)
                   (when erc-remove-parsed-property
                     (remove-text-properties (point-min) (point-max)
                                             '(erc-parsed nil tags nil)))
@@ -3776,7 +3753,8 @@ erc--insert-before-markers-transplanting-hidden
          (new (and before (erc--solo (cl-intersection b a)))))
     (when new
       (erc--remove-from-prop-value-list (1- (point)) (point) 'invisible a))
-    (prog1 (insert-before-markers string)
+    (progn
+      (insert-before-markers string)
       (when new
         (erc--merge-prop (1- (point)) (point) 'invisible new)))))
=20
@@ -3863,8 +3841,14 @@ erc-display-message-highlight
=20
 See also `erc-make-notice'."
   (cond ((eq type 'notice)
+         (when erc--msg-props
+           (puthash 'erc--pfx #'erc--pfx-skip-notice-fwd erc--msg-props))
          (erc-make-notice string))
         (t
+         (when (and erc--msg-props ; prefer notice if combined
+                    (not (erc--check-msg-prop 'erc--pfx
+                                              #'erc--pfx-skip-notice-fwd)))
+           (puthash 'erc--pfx #'erc--pfx-skip-template-fwd erc--msg-props))
          (erc-put-text-property
           0 (length string)
           'font-lock-face
@@ -6278,6 +6262,35 @@ erc--get-speaker-bounds
                                          'erc--speaker nil)))
     (cons beg (next-single-property-change beg 'erc--speaker))))
=20
+(defvar erc--ctcp-action-speaker-in-prefix-p nil
+  ;; This variable replaces `erc-fill--wrap-action-dedent-p' in ERC 5.6.
+  "Whether the speaker of a /ME is part of the prefix rather than the body=
.")
+
+;; Insertion hook members defer to the function value of the `erc--pfx'
+;; msg prop to find the "body" portion of a message after a prefix, such
+;; as a "<speaker> " tag, assuming the `erc--msg' prop isn't `unknown'.
+(defun erc--pfx-skip-word-fwd ()
+  "Go to a space following an initial run of non-space chars."
+  (unless (and (bobp) (eq ?\s (char-after (point))))
+    (search-forward " " (pos-eol) t)))
+
+(defun erc--pfx-skip-spkr-fwd ()
+  "Move point after the first space following the speaker tag.
+That's one char beyond the last with a `erc--speaker' text property."
+  (let ((bounds (erc--get-speaker-bounds)))
+    (when (or erc--ctcp-action-speaker-in-prefix-p
+              (not (erc--check-msg-prop 'erc--ctcp 'ACTION)))
+      (goto-char (cdr bounds)))
+    (search-forward " " (pos-eol) t)))
+
+(defun erc--pfx-skip-notice-fwd ()
+  "Move point past `erc-notice-prefix'."
+  (search-forward erc-notice-prefix (+ (point) (length erc-notice-prefix))=
 t))
+
+(defun erc--pfx-skip-template-fwd ()
+  "Skip common prefixes from the English format-template catalog."
+  (search-forward-regexp (rx bol (| "\n\n*** " "=3D=3D> ")) (+ (point) 6) =
t))
+
 (defvar erc--cmem-from-nick-function #'erc--cmem-get-existing
   "Function maybe returning a \"channel member\" cons from a nick.
 Must return either nil or a cons of an `erc-server-user' and an
@@ -6495,7 +6508,7 @@ erc--determine-speaker-message-format-args
                      (if inputp 'input-query-notice 'query-notice)
                    (if inputp 'input-chan-notice 'chan-notice))))
         ?p (or prefix "") ?n (erc--speakerize-nick nick disp-nick)
-        ?s (or statusmsg "") ?m (erc--ensure-offset-prop message)))
+        ?s (or statusmsg "") ?m message))
=20
 (defcustom erc-show-speaker-membership-status nil
   "Whether to prefix speakers with their channel status.
@@ -6629,7 +6642,7 @@ erc--format-speaker-input-message
               (erc--msg-prop-overrides (push (cons 'erc--msg key)
                                              erc--msg-prop-overrides)))
         (erc-format-message key ?p pfx ?n (erc--speakerize-nick nick)
-                            ?m (erc--ensure-offset-prop message)))
+                            ?m message))
     (propertize (concat "> " message) 'font-lock-face 'erc-input-face)))
=20
 (defun erc-echo-notice-in-default-buffer (s parsed buffer _sender)
@@ -6939,12 +6952,12 @@ erc-ctcp-query-ACTION
            (if selfp
                (if stsmsg 'ctcp-action-statusmsg-input 'ctcp-action-input)
              (if stsmsg 'ctcp-action-statusmsg 'ctcp-action))
-           ?s (or stsmsg "")
+           ?s (or stsmsg to)
            ?p (or (and (erc-channel-user-p prefix)
                        (erc-get-channel-membership-prefix prefix))
                   "")
            ?n (erc--speakerize-nick nick dispnm)
-           ?m (erc--ensure-offset-prop s)))))))
+           ?m s))))))
=20
 (defvar erc-ctcp-query-CLIENTINFO-hook '(erc-ctcp-query-CLIENTINFO))
=20
@@ -7927,7 +7940,6 @@ erc-make-notice
   "Notify the user of MESSAGE."
   (when erc-minibuffer-notice
     (message "%s" message))
-  (erc--add-msg-prop 'erc--offset (length erc-notice-prefix))
   (erc-highlight-notice (concat erc-notice-prefix message)))
=20
 (defun erc-highlight-error (s)
@@ -8427,9 +8439,8 @@ erc-display-msg
         (insert (erc--format-speaker-input-message line) "\n")
         (save-restriction
           (narrow-to-region insert-position (point))
-          (erc--with-offset-marker
-           (run-hooks 'erc-send-modify-hook)
-           (run-hooks 'erc-send-post-hook))
+          (run-hooks 'erc-send-modify-hook)
+          (run-hooks 'erc-send-post-hook)
           (cl-assert (> (- (point-max) (point-min)) 1))
           (add-text-properties (point-min) (1+ (point-min))
                                (erc--order-text-properties-from-hash
diff --git a/test/lisp/erc/erc-match-tests.el b/test/lisp/erc/erc-match-tes=
ts.el
index 4fdf5d3fadb..6691f5506aa 100644
--- a/test/lisp/erc/erc-match-tests.el
+++ b/test/lisp/erc/erc-match-tests.el
@@ -733,6 +733,30 @@ erc-log-matches/legacy
     (let ((erc-match-use-legacy-logic-p t))
       (erc-match-tests--log-matches))))
=20
+(ert-deftest erc-match--opt-pat-cache ()
+  (let ((erc-match--opt-pat-cache ()))
+    (let ((erc-keywords '("foo")))
+      (erc-match--keyword-p (erc-match-opt-keyword :body-beg 1
+                                                   :sender ""
+                                                   :command 'fake)))
+    (let ((erc-keywords '("bar")))
+      (erc-match--keyword-p (erc-match-opt-keyword :body-beg 1
+                                                   :sender ""
+                                                   :command 'fake)))
+    (let ((erc-fools '("baz")))
+      (erc-match--user-nuh-or-mention-p (erc-match-opt-fool :body-beg 1
+                                                            :sender ""
+                                                            :command 'fake=
)))
+    (should (equal erc-match--opt-pat-cache
+                   '((erc-match--opt-pat-addr-end-make
+                      (("baz") . "\\s. \\(baz\\)\\s."))
+                     (erc-match--opt-pat-addr-beg-make
+                      (("baz") . "\\<\\(baz\\)[:,] "))
+                     (erc-match--opt-pat-make
+                      (("baz") . "baz"))
+                     (erc-match--opt-pat-kw-make
+                      (("bar") . "bar") (("foo") . "foo")))))))
+
 ;; This demos bare-bones usage of the `erc-match' API that implicitly
 ;; opts out of the traditional options and "parts"-based mechanism.  The
 ;; user does not have to provide a `:part' keyword because they've
@@ -771,7 +795,9 @@ erc-match-functions/api/non-parts-based
 ;; `match' interface.
 (ert-deftest erc-match-functions/api/parts-based ()
   (let* ((results ())
-         (erc-text-matched-hook (lambda (&rest r) (push r results)))
+         (bodies ())
+         (erc-text-matched-hook (lambda (&rest r)
+                                  (push r results)))
          (erc-match-functions ()))
=20
     (erc-match-tests--perform
@@ -787,6 +813,11 @@ erc-match-functions/api/parts-based
                           :face 'error
                           :predicate (lambda (_)
                                        (search-forward "alice" nil t))
+                          ;; Override `erc-match-highlight'.
+                          :handler (lambda (m)
+                                     (push (erc-match-get-message-body m)
+                                           bodies)
+                                     (erc-match-highlight m))
                           plist))
                  0 t)
=20
@@ -804,6 +835,11 @@ erc-match-functions/api/parts-based
        (erc-match-tests--assert-face-present 'error " bob")
        (erc-match-tests--assert-face-absent 'error (pos-eol))
=20
+       ;; Prefixes detected for notices and spoken messages.
+       (should (equal bodies
+                      '("hi ALICE"
+                        "Users on #chan: Alice bob tester")))
+
        (should (equal results
                       '(( keyword "[email protected]" "hi ALICE\n")
                         ( keyword "Server:353"
diff --git a/test/lisp/erc/erc-scenarios-join-display-context.el b/test/lis=
p/erc/erc-scenarios-join-display-context.el
index 07d7ff1519e..e525eb59f82 100644
--- a/test/lisp/erc/erc-scenarios-join-display-context.el
+++ b/test/lisp/erc/erc-scenarios-join-display-context.el
@@ -26,6 +26,8 @@
   (let ((load-path (cons (ert-resource-directory) load-path)))
     (require 'erc-scenarios-common)))
=20
+(require 'erc-join)
+
 ;; This module uses the list `erc-join--requested-channels' to detect
 ;; whether a JOIN response was likely triggered by an outgoing JOIN
 ;; emitted on behalf of `erc-autojoin-channels-alist'.  When a related
diff --git a/test/lisp/erc/erc-scenarios-log.el b/test/lisp/erc/erc-scenari=
os-log.el
index 7452062e3c5..4f190ae898e 100644
--- a/test/lisp/erc/erc-scenarios-log.el
+++ b/test/lisp/erc/erc-scenarios-log.el
@@ -28,8 +28,7 @@
=20
 (require 'erc-log)
 (require 'erc-truncate)
-
-(defvar erc-timestamp-format-left)
+(require 'erc-stamp)
=20
 (ert-deftest erc-scenarios-log--kill-hook ()
   :tags '(:expensive-test)
@@ -327,9 +326,6 @@ erc-scenarios-log--truncate/left-stamps
        (funcall expect 1 "loathed enemy")
        (funcall expect -0.001 "please your lordship")))))
=20
-(defvar erc-insert-timestamp-function)
-(declare-function erc-insert-timestamp-left "erc-stamp" (string))
-
 (ert-deftest erc-scenarios-log--save-buffer-in-logs/truncate-on-save ()
   :tags '(:expensive-test)
   (with-suppressed-warnings ((obsolete erc-truncate-buffer-on-save))
diff --git a/test/lisp/erc/erc-scenarios-match-api.el b/test/lisp/erc/erc-s=
cenarios-match-api.el
new file mode 100644
index 00000000000..0a8afbf33f2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-match-api.el
@@ -0,0 +1,127 @@
+;;; erc-scenarios-match-api.el --- `erc-match-functions' scenarios -*- lex=
ical-binding: t -*-
+
+;; Copyright (C) 2026 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-match)
+(require 'erc-stamp)
+
+(defun erc-scenarios-match-api--test-fn (&rest plist)
+  (apply #'erc-match
+         :predicate #'always
+         :handler (lambda (m)
+                    (let ((body (erc-match-get-message-body m))
+                          (cmd (format (if (numberp (erc-match-command m))
+                                           "%03i"
+                                         "%s")
+                                       (erc-match-command m)))
+                          (nick (or (erc-match-nick m) "?")))
+                      (with-current-buffer "*erc-match test matches*"
+                        (save-excursion
+                          (goto-char (point-max))
+                          (insert cmd " " nick ": " body "\n")))))
+         plist))
+
+(defun erc-scenarios-match-api--custom-match-functions ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "match/functions")
+       (dumb-server (erc-d-run "localhost" t 'custom))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-autojoin-channels-alist '((foonet "#chan")))
+       (match-buffer (get-buffer-create "*erc-match test matches*"))
+       (erc-match-functions
+        (cons #'erc-scenarios-match-api--test-fn erc-match-functions))
+       ;; Shadow this date stamp instead of mocking the time function.
+       ;; The version numbers in the QUIT message don't need this
+       ;; because they're printed verbatim from the server response.
+       (erc-message-english-s329 "%c was created on @@DATESTAMP@@"))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :full-name "tester"
+                                :user "tester"
+                                :nick "tester")
+        (funcall expect 5 "debug")))
+
+    (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+      (funcall expect 10 "<alice> There were none principal")
+      (erc-scenarios-common-say "/query bob"))
+
+    (with-current-buffer "bob"
+      (erc-scenarios-common-say "hi")
+      (funcall expect 10 "<bob> As much as ever Coriolanus did"))
+
+    (with-current-buffer "foonet"
+      (erc-scenarios-common-say "/msg NickServ help identify")
+      (funcall expect 10 "End of NickServ HELP"))
+
+    (with-current-buffer "#chan"
+      (funcall expect 10 "<alice> bob: Thus men may grow wiser")
+      (erc-scenarios-common-say "/quit")
+      (funcall expect 10 "quit: Quit:"))
+
+    (with-current-buffer "foonet"
+      (funcall expect 10 "=3D=3D> ERROR"))
+
+    (with-temp-buffer
+      (insert-file-contents
+       (expand-file-name "match/functions/custom-match-log"
+                         (ert-resource-directory)))
+      (let ((expected (buffer-string)))
+        (with-current-buffer "*erc-match test matches*"
+          (should (equal expected (buffer-string))))))))
+
+;; These tests primarily ensure that the various fields of the
+;; `erc-match' object, like `spkr-beg', as well as associated utilities,
+;; like `erc-match-get-message-body', work as expected.  It defines a
+;; custom `erc-match-functions' member that prints a summary of every
+;; displayed message to its own buffer.  That buffer's contents appear
+;; in the file resources/match/functions/custom-match-log.
+
+(ert-deftest erc-scenarios-match-api--custom-functions/basic ()
+  :tags '(:expensive-test)
+  (erc-scenarios-match-api--custom-match-functions))
+
+(ert-deftest erc-scenarios-match-api--custom-functions/fill-wrap ()
+  :tags '(:expensive-test)
+  (let ((erc-modules (cons 'fill-wrap erc-modules)))
+    (erc-scenarios-match-api--custom-match-functions)))
+
+(ert-deftest erc-scenarios-match-api--custom-functions/left-stamps ()
+  :tags '(:expensive-test)
+  (let ((erc-insert-timestamp-function #'erc-insert-timestamp-left)
+        (erc-timestamp-only-if-changed-flag nil))
+    (erc-scenarios-match-api--custom-match-functions)))
+
+(ert-deftest erc-scenarios-match-api--custom-functions/left-stamps/fill-wr=
ap ()
+  :tags '(:expensive-test)
+  (let ((erc-insert-timestamp-function #'erc-insert-timestamp-left)
+        (erc-timestamp-only-if-changed-flag nil)
+        (erc-modules (cons 'fill-wrap erc-modules)))
+    (erc-scenarios-match-api--custom-match-functions)))
+
+;;; erc-scenarios-match-api.el ends here
diff --git a/test/lisp/erc/erc-scenarios-match.el b/test/lisp/erc/erc-scena=
rios-match.el
index bc5ebe27ce9..b22cff18c46 100644
--- a/test/lisp/erc/erc-scenarios-match.el
+++ b/test/lisp/erc/erc-scenarios-match.el
@@ -24,10 +24,7 @@
   (let ((load-path (cons (ert-resource-directory) load-path)))
     (require 'erc-scenarios-common)))
=20
-(eval-when-compile
-  (require 'erc-join)
-  (require 'erc-match))
-
+(require 'erc-match)
 (require 'erc-stamp)
 (require 'erc-fill)
=20
diff --git a/test/lisp/erc/erc-scenarios-services-misc.el b/test/lisp/erc/e=
rc-scenarios-services-misc.el
index bc6522ae110..6a7d3c2f882 100644
--- a/test/lisp/erc/erc-scenarios-services-misc.el
+++ b/test/lisp/erc/erc-scenarios-services-misc.el
@@ -24,8 +24,7 @@
   (let ((load-path (cons (ert-resource-directory) load-path)))
     (require 'erc-scenarios-common)))
=20
-(eval-when-compile (require 'erc-join)
-                   (require 'erc-services))
+(require 'erc-services)
=20
 (ert-deftest erc-scenarios-services-password ()
   :tags '(:expensive-test)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 45df1b87719..515a0716549 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -3030,6 +3030,63 @@ erc--determine-speaker-message-format-args/queries
=20
     (when noninteractive (kill-buffer))))
=20
+(ert-deftest erc--pfx-skip-notice-fwd ()
+  (erc-tests-common-make-server-buf)
+  ;; Inhibit login I/O.
+  (setq erc-logged-in t)
+
+  ;; Moves past leading `erc-notice-prefix'.
+  (let* ((calledp nil)
+         (erc-insert-modify-hook
+          (lambda ()
+            (goto-char (point-min))
+            (erc--pfx-skip-notice-fwd)
+            (should (looking-at (rx "One!" eol)))
+            (setq calledp t))))
+    (erc-tests-common-simulate-line ":irc.foonet.net 375 tester :One!")
+    (should calledp))
+
+  ;; Respects customized option.
+  (let* ((erc-notice-prefix "~~> ")
+         (calledp nil)
+         (erc-insert-modify-hook
+          (lambda ()
+            (goto-char (point-min))
+            (erc--pfx-skip-notice-fwd)
+            (should (looking-at (rx "Two!" eol)))
+            (should (looking-back (rx bol "~~> ")))
+            (setq calledp t))))
+    (erc-tests-common-simulate-line ":irc.foonet.net 372 tester :Two!")
+    (should calledp)))
+
+(ert-deftest erc--pfx-skip-template-fwd ()
+  (erc-tests-common-make-server-buf)
+
+  ;; Skips over two leading newlines.
+  (let* ((calledp nil)
+         (erc-insert-modify-hook
+          (lambda ()
+            (goto-char (point-min))
+            (should (looking-at (rx eol bol)))
+            (erc--pfx-skip-template-fwd)
+            (should (equal (buffer-substring (point) (pos-eol))
+                           "ERC finished ***"))
+            (setq calledp t))))
+    (erc-display-message nil 'error (current-buffer) 'finished)
+    (should calledp))
+
+  ;; Does the same for arbitrary non-whitespace prefix.
+  (let* ((calledp nil)
+         (erc-insert-modify-hook
+          (lambda ()
+            (goto-char (point-min))
+            (erc--pfx-skip-template-fwd)
+            (should (looking-at (rx "ERROR from : Quit 123" eol)))
+            (should (looking-back (rx bol "=3D=3D> ")))
+            (setq calledp t))))
+    (erc-tests-common-simulate-line "ERROR :Quit 123")
+    (should calledp)))
+
 (defun erc-tests--format-my-nick (message)
   (concat (erc-format-my-nick)
           (propertize message 'font-lock-face 'erc-input-face)))
@@ -3685,12 +3742,45 @@ erc-channel-user
       (should (=3D 0 (erc-channel-user-status u))))))
=20
 (defconst erc-tests--modules
-  '( autoaway autojoin bufbar button capab-identify
-     command-indicator completion dcc fill identd
-     imenu irccontrols keep-place list log match menu move-to-prompt netsp=
lit
-     networks nickbar nicks noncommands notifications notify page readonly
-     replace ring sasl scrolltobottom services smiley sound
-     spelling stamp track truncate unmorse xdcc))
+  '(autoaway
+    autojoin
+    bufbar
+    button
+    capab-identify
+    command-indicator
+    completion
+    dcc
+    fill
+    identd
+    imenu
+    irccontrols
+    keep-place
+    list log
+    match
+    menu
+    move-to-prompt
+    netsplit
+    networks
+    nickbar
+    nicks
+    noncommands
+    notifications
+    notify
+    page
+    readonly
+    replace
+    ring
+    sasl
+    scrolltobottom
+    services
+    smiley
+    sound
+    spelling
+    stamp
+    track
+    truncate
+    unmorse
+    xdcc))
=20
 ;; Ensure that `:initialize' doesn't change the ordering of the
 ;; members because otherwise the widget's state is "edited".
diff --git a/test/lisp/erc/resources/erc-scenarios-common.el b/test/lisp/er=
c/resources/erc-scenarios-common.el
index fa187cc59f7..5e1686ab7b7 100644
--- a/test/lisp/erc/resources/erc-scenarios-common.el
+++ b/test/lisp/erc/resources/erc-scenarios-common.el
@@ -93,13 +93,6 @@
=20
 (require 'erc)
=20
-(eval-when-compile (require 'erc-join)
-                   (require 'erc-services)
-                   (require 'erc-fill))
-
-(declare-function erc-network "erc-networks")
-(defvar erc-network)
-
 (defvar erc-scenarios-common--resources-dir
   (expand-file-name "../" (ert-resource-directory)))
=20
@@ -149,7 +142,8 @@ erc-scenarios-common--print-trace
       (auth-source-do-cache nil)
       (timer-list (copy-sequence timer-list))
       (timer-idle-list (copy-sequence timer-idle-list))
-      (erc-auth-source-parameters-join-function nil)
+      ;; This binding exists to protect the default value because ERC
+      ;; adds all joined channels automatically.
       (erc-autojoin-channels-alist nil)
       (erc-server-auto-reconnect nil)
       (erc-after-connect nil)
@@ -178,47 +172,56 @@ erc-scenarios-common-with-cleanup
=20
   (let* ((orig-autojoin-mode (make-symbol "orig-autojoin-mode"))
          (combined `((,orig-autojoin-mode (bound-and-true-p erc-autojoin-m=
ode))
-                     ,@(erc-scenarios-common--make-bindings bindings))))
-
-    `(erc-d-t-with-cleanup (,@combined)
-
-         (ert-info ("Restore autojoin, etc., kill ERC buffers")
+                     ,@(erc-scenarios-common--make-bindings bindings)))
+         (dynvars ()))
+
+    ;; Declare "erc-" variables dynamic in test scope.
+    (dolist (binder combined)
+      (setq binder (ensure-list binder))
+      (when (and (string-prefix-p "erc-" (symbol-name (car binder)))
+                 (not (special-variable-p (car binder))))
+        (push `(defvar ,(car binder)) dynvars)))
+    `(progn
+       ,@dynvars
+       (erc-d-t-with-cleanup (,@combined)
+
+           (ert-info ("Restore autojoin, etc., kill ERC buffers")
+             (dolist (buf (buffer-list))
+               (when-let* ((erc-d-u--process-buffer)
+                           (proc (get-buffer-process buf)))
+                 (delete-process proc)))
+
+             (erc-scenarios-common--remove-silence)
+
+             (when erc-scenarios-common-extra-teardown
+               (ert-info ("Running extra teardown")
+                 (funcall erc-scenarios-common-extra-teardown)))
+
+             (erc-buffer-do #'erc-scenarios-common--assert-date-stamps)
+             (when (and (boundp 'erc-autojoin-mode)
+                        (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
+               (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
+
+             (when (or noninteractive erc-scenarios-common--graphical-p)
+               (when noninteractive
+                 (erc-scenarios-common--print-trace))
+               (erc-d-t-kill-related-buffers)
+               (delete-other-windows)))
+
+         (erc-scenarios-common--add-silence)
+
+         (ert-info ("Wait for dumb server")
            (dolist (buf (buffer-list))
-             (when-let* ((erc-d-u--process-buffer)
-                         (proc (get-buffer-process buf)))
-               (delete-process proc)))
-
-           (erc-scenarios-common--remove-silence)
-
-           (when erc-scenarios-common-extra-teardown
-             (ert-info ("Running extra teardown")
-               (funcall erc-scenarios-common-extra-teardown)))
-
-           (erc-buffer-do #'erc-scenarios-common--assert-date-stamps)
-           (when (and (boundp 'erc-autojoin-mode)
-                      (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
-             (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
-
-           (when (or noninteractive erc-scenarios-common--graphical-p)
-             (when noninteractive
-               (erc-scenarios-common--print-trace))
-             (erc-d-t-kill-related-buffers)
-             (delete-other-windows)))
-
-       (erc-scenarios-common--add-silence)
-
-       (ert-info ("Wait for dumb server")
-         (dolist (buf (buffer-list))
-           (with-current-buffer buf
-             (when erc-d-u--process-buffer
-               (erc-d-t-search-for 3 "Starting")))))
+             (with-current-buffer buf
+               (when erc-d-u--process-buffer
+                 (erc-d-t-search-for 3 "Starting")))))
=20
-       (ert-info ("Activate erc-debug-irc-protocol")
-         (unless (and (or noninteractive erc-scenarios-common--graphical-p)
-                      (not erc-debug-irc-protocol))
-           (erc-toggle-debug-irc-protocol)))
+         (ert-info ("Activate erc-debug-irc-protocol")
+           (unless (and (or noninteractive erc-scenarios-common--graphical=
-p)
+                        (not erc-debug-irc-protocol))
+             (erc-toggle-debug-irc-protocol)))
=20
-       ,@body)))
+         ,@body))))
=20
 (defvar erc-scenarios-common--term-size '(34 . 80))
 (declare-function term-char-mode "term" nil)
@@ -336,9 +339,11 @@ erc-scenarios-common-with-noninteractive-in-term
=20
 (defun erc-scenarios-common--assert-date-stamps ()
   "Ensure all date stamps are accounted for."
-  (dolist (stamp erc-stamp--date-stamps)
-    (should (eq 'datestamp (get-text-property (erc-stamp--date-marker stam=
p)
-                                              'erc--msg)))))
+  (defvar erc-stamp--date-stamps)
+  (when (fboundp 'erc-stamp--date-marker)
+    (dolist (stamp erc-stamp--date-stamps)
+      (should (eq 'datestamp (get-text-property (erc-stamp--date-marker st=
amp)
+                                                'erc--msg))))))
=20
 (defun erc-scenarios-common-assert-initial-buf-name (id port)
   ;; Assert no limbo period when explicit ID given
diff --git a/test/lisp/erc/resources/erc-tests-common.el b/test/lisp/erc/re=
sources/erc-tests-common.el
index 525bc2ed868..28643e4834a 100644
--- a/test/lisp/erc/resources/erc-tests-common.el
+++ b/test/lisp/erc/resources/erc-tests-common.el
@@ -126,7 +126,7 @@ erc-tests-common-make-server-buf
   "Return a server buffer named NAME, creating it if necessary.
 Use NAME for the network and the session server as well."
   (with-current-buffer (if name
-                           (get-buffer-create name)
+                           (setq name (buffer-name (get-buffer-create name=
)))
                          (and (string-search "temp" (buffer-name))
                               (setq name "foonet")
                               (buffer-name)))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld b/te=
st/lisp/erc/resources/fill/snapshots/merge-01-start.eld
index 166ed59e292..07dd68f19ec 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<alice> one.\n<alice> two.\n<bob> t=
hree.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp =
erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(=
space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg no=
tice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) =
22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-=
prefix #1# line-prefix #2#) 184 191 (field erc-timestamp wrap-prefix #1# li=
ne-prefix #2# display (#6=3D(margin right-margin) #("[00:00]" 0 7 (invisibl=
e timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd =
PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198=
 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#=
) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-p=
refix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg =
erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=
=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix #4#) 354=
 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefix #1# line-prefix=
 #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg datestamp=
 erc--ts 1680307200 field erc-timestamp) 438 455 (field erc-timestamp wrap-=
prefix #1# line-prefix (space :width (- 27 (18)))) 456 457 (erc--msg msg er=
c--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-pref=
ix #5=3D(space :width (- 27 (6)))) 457 460 (wrap-prefix #1# line-prefix #5#=
) 460 467 (wrap-prefix #1# line-prefix #5#) 467 468 (field erc-timestamp wr=
ap-prefix #1# line-prefix #5#) 468 475 (field erc-timestamp wrap-prefix #1#=
 line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 476 =
477 (erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG wra=
p-prefix #1# line-prefix #8=3D(space :width (- 27 (8)))) 477 482 (wrap-pref=
ix #1# line-prefix #8#) 482 488 (wrap-prefix #1# line-prefix #8#) 489 490 (=
erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG wrap-pre=
fix #1# line-prefix #9=3D(space :width (- 27 0)) erc-fill--wrap-merge #7=3D=
"" display #7#) 490 495 (wrap-prefix #1# line-prefix #9# erc-fill--wrap-mer=
ge #7# display #7#) 495 497 (wrap-prefix #1# line-prefix #9# erc-fill--wrap=
-merge #7# display #7#) 497 501 (wrap-prefix #1# line-prefix #9#) 502 503 (=
erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefi=
x #1# line-prefix #10=3D(space :width (- 27 (6)))) 503 506 (wrap-prefix #1#=
 line-prefix #10#) 506 514 (wrap-prefix #1# line-prefix #10#) 515 516 (erc-=
-msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1=
# line-prefix #11=3D(space :width (- 27 0)) erc-fill--wrap-merge #7# displa=
y #7#) 516 519 (wrap-prefix #1# line-prefix #11# erc-fill--wrap-merge #7# d=
isplay #7#) 519 521 (wrap-prefix #1# line-prefix #11# erc-fill--wrap-merge =
#7# display #7#) 521 526 (wrap-prefix #1# line-prefix #11#) 527 528 (erc--m=
sg msg erc--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1=
# line-prefix #12=3D(space :width (- 27 (8)))) 528 533 (wrap-prefix #1# lin=
e-prefix #12#) 533 540 (wrap-prefix #1# line-prefix #12#) 541 542 (erc--msg=
 msg erc--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# =
line-prefix #13=3D(space :width (- 27 0)) erc-fill--wrap-merge #7# display =
#7#) 542 547 (wrap-prefix #1# line-prefix #13# erc-fill--wrap-merge #7# dis=
play #7#) 547 549 (wrap-prefix #1# line-prefix #13# erc-fill--wrap-merge #7=
# display #7#) 549 553 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<alice> one.\n<alice> two.\n<bob> t=
hree.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp =
erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(=
space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg no=
tice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wrap-prefix #1# line-prefi=
x #2=3D(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) =
183 184 (field erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (fiel=
d erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=3D(margin right=
-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc=
--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd w=
rap-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-pr=
efix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203=
 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#=
) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr=
 "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-pref=
ix #1# line-prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1#=
 line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-p=
refix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 43=
8 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 438 455 (fiel=
d erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 456=
 457 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc-=
-pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #5=3D(space :width =
(- 27 (6)))) 457 460 (wrap-prefix #1# line-prefix #5#) 460 467 (wrap-prefix=
 #1# line-prefix #5#) 467 468 (field erc-timestamp wrap-prefix #1# line-pre=
fix #5#) 468 475 (field erc-timestamp wrap-prefix #1# line-prefix #5# displ=
ay (#6# #("[07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg msg erc-=
-spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-sp=
kr-fwd wrap-prefix #1# line-prefix #8=3D(space :width (- 27 (8)))) 477 482 =
(wrap-prefix #1# line-prefix #8#) 482 488 (wrap-prefix #1# line-prefix #8#)=
 489 490 (erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMS=
G erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #9=3D(space :=
width (- 27 0)) erc-fill--wrap-merge #7=3D"" display #7#) 490 495 (wrap-pre=
fix #1# line-prefix #9# erc-fill--wrap-merge #7# display #7#) 495 497 (wrap=
-prefix #1# line-prefix #9# erc-fill--wrap-merge #7# display #7#) 497 501 (=
wrap-prefix #1# line-prefix #9#) 502 503 (erc--msg msg erc--spkr "bob" erc-=
-ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix=
 #1# line-prefix #10=3D(space :width (- 27 (6)))) 503 506 (wrap-prefix #1# =
line-prefix #10#) 506 514 (wrap-prefix #1# line-prefix #10#) 515 516 (erc--=
msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--p=
fx-skip-spkr-fwd wrap-prefix #1# line-prefix #11=3D(space :width (- 27 0)) =
erc-fill--wrap-merge #7# display #7#) 516 519 (wrap-prefix #1# line-prefix =
#11# erc-fill--wrap-merge #7# display #7#) 519 521 (wrap-prefix #1# line-pr=
efix #11# erc-fill--wrap-merge #7# display #7#) 521 526 (wrap-prefix #1# li=
ne-prefix #11#) 527 528 (erc--msg msg erc--spkr "Dummy" erc--ts 1680332400 =
erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefi=
x #12=3D(space :width (- 27 (8)))) 528 533 (wrap-prefix #1# line-prefix #12=
#) 533 540 (wrap-prefix #1# line-prefix #12#) 541 542 (erc--msg msg erc--sp=
kr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-=
fwd wrap-prefix #1# line-prefix #13=3D(space :width (- 27 0)) erc-fill--wra=
p-merge #7# display #7#) 542 547 (wrap-prefix #1# line-prefix #13# erc-fill=
--wrap-merge #7# display #7#) 547 549 (wrap-prefix #1# line-prefix #13# erc=
-fill--wrap-merge #7# display #7#) 549 553 (wrap-prefix #1# line-prefix #13=
#))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld b/te=
st/lisp/erc/resources/fill/snapshots/merge-02-right.eld
index 8b502373807..0bb25d8cd9f 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<alice> one.\n<alice> two.\n<bob> t=
hree.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp =
erc--ts 0 field erc-timestamp) 3 20 (wrap-prefix #1=3D(space :width 29) lin=
e-prefix (space :width (- 29 (18))) field erc-timestamp) 21 22 (wrap-prefix=
 #1# line-prefix #2=3D(space :width (- 29 (4))) erc--msg notice erc--ts 0) =
22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (wrap-prefix #1# line-pref=
ix #2# field erc-timestamp) 184 191 (wrap-prefix #1# line-prefix #2# field =
erc-timestamp display (#6=3D(margin right-margin) #("[00:00]" 0 7 (invisibl=
e timestamp)))) 192 193 (wrap-prefix #1# line-prefix #3=3D(space :width (- =
29 (8))) erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG) 193 198=
 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#=
) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-p=
refix #3#) 317 349 (wrap-prefix #1# line-prefix (space :width (- 29 (8)))) =
350 351 (wrap-prefix #1# line-prefix #4=3D(space :width (- 29 (6))) erc--ms=
g msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG) 351 354 (wrap-prefix #1# =
line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-pr=
efix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 438=
 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 438 455 (wrap-=
prefix #1# line-prefix (space :width (- 29 (18))) field erc-timestamp) 456 =
457 (wrap-prefix #1# line-prefix #5=3D(space :width (- 29 (6))) erc--msg ms=
g erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG) 457 460 (wrap-prefix=
 #1# line-prefix #5#) 460 467 (wrap-prefix #1# line-prefix #5#) 467 468 (wr=
ap-prefix #1# line-prefix #5# field erc-timestamp) 468 475 (wrap-prefix #1#=
 line-prefix #5# field erc-timestamp display (#6# #("[07:00]" 0 7 (invisibl=
e timestamp)))) 476 477 (wrap-prefix #1# line-prefix #8=3D(space :width (- =
29 (8))) erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG=
) 477 482 (wrap-prefix #1# line-prefix #8#) 482 488 (wrap-prefix #1# line-p=
refix #8#) 489 490 (wrap-prefix #1# line-prefix #9=3D(space :width (- 29 0)=
) erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG erc-fi=
ll--wrap-merge #7=3D"" display #7#) 490 495 (wrap-prefix #1# line-prefix #9=
# erc-fill--wrap-merge #7# display #7#) 495 497 (wrap-prefix #1# line-prefi=
x #9# erc-fill--wrap-merge #7# display #7#) 497 501 (wrap-prefix #1# line-p=
refix #9#) 502 503 (wrap-prefix #1# line-prefix #10=3D(space :width (- 29 (=
6))) erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG) 503 =
506 (wrap-prefix #1# line-prefix #10#) 506 514 (wrap-prefix #1# line-prefix=
 #10#) 515 516 (wrap-prefix #1# line-prefix #11=3D(space :width (- 29 0)) e=
rc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc-fill--w=
rap-merge #7# display #7#) 516 519 (wrap-prefix #1# line-prefix #11# erc-fi=
ll--wrap-merge #7# display #7#) 519 521 (wrap-prefix #1# line-prefix #11# e=
rc-fill--wrap-merge #7# display #7#) 521 526 (wrap-prefix #1# line-prefix #=
11#) 527 528 (wrap-prefix #1# line-prefix #12=3D(space :width (- 29 (8))) e=
rc--msg msg erc--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG) 528 533 =
(wrap-prefix #1# line-prefix #12#) 533 540 (wrap-prefix #1# line-prefix #12=
#) 541 542 (wrap-prefix #1# line-prefix #13=3D(space :width (- 29 0)) erc--=
msg msg erc--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG erc-fill--wra=
p-merge #7# display #7#) 542 547 (wrap-prefix #1# line-prefix #13# erc-fill=
--wrap-merge #7# display #7#) 547 549 (wrap-prefix #1# line-prefix #13# erc=
-fill--wrap-merge #7# display #7#) 549 553 (wrap-prefix #1# line-prefix #13=
#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<alice> one.\n<alice> two.\n<bob> t=
hree.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp =
erc--ts 0 field erc-timestamp) 3 20 (wrap-prefix #1=3D(space :width 29) lin=
e-prefix (space :width (- 29 (18))) field erc-timestamp) 21 22 (wrap-prefix=
 #1# line-prefix #2=3D(space :width (- 29 (4))) erc--msg notice erc--ts 0 e=
rc--pfx erc--pfx-skip-notice-fwd) 22 183 (wrap-prefix #1# line-prefix #2#) =
183 184 (wrap-prefix #1# line-prefix #2# field erc-timestamp) 184 191 (wrap=
-prefix #1# line-prefix #2# field erc-timestamp display (#6=3D(margin right=
-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 193 (wrap-prefix #1# =
line-prefix #3=3D(space :width (- 29 (8))) erc--msg msg erc--spkr "alice" e=
rc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd) 193 198 (wrap-pr=
efix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203=
 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#=
) 317 349 (wrap-prefix #1# line-prefix (space :width (- 29 (8)))) 350 351 (=
wrap-prefix #1# line-prefix #4=3D(space :width (- 29 (6))) erc--msg msg erc=
--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd) 35=
1 354 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefi=
x #4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# l=
ine-prefix #4#) 437 438 (erc--msg datestamp erc--ts 1680307200 field erc-ti=
mestamp) 438 455 (wrap-prefix #1# line-prefix (space :width (- 29 (18))) fi=
eld erc-timestamp) 456 457 (wrap-prefix #1# line-prefix #5=3D(space :width =
(- 29 (6))) erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMS=
G erc--pfx erc--pfx-skip-spkr-fwd) 457 460 (wrap-prefix #1# line-prefix #5#=
) 460 467 (wrap-prefix #1# line-prefix #5#) 467 468 (wrap-prefix #1# line-p=
refix #5# field erc-timestamp) 468 475 (wrap-prefix #1# line-prefix #5# fie=
ld erc-timestamp display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 476 =
477 (wrap-prefix #1# line-prefix #8=3D(space :width (- 29 (8))) erc--msg ms=
g erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-s=
kip-spkr-fwd) 477 482 (wrap-prefix #1# line-prefix #8#) 482 488 (wrap-prefi=
x #1# line-prefix #8#) 489 490 (wrap-prefix #1# line-prefix #9=3D(space :wi=
dth (- 29 0)) erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PR=
IVMSG erc-fill--wrap-merge #7=3D"" erc--pfx erc--pfx-skip-spkr-fwd display =
#7#) 490 495 (wrap-prefix #1# line-prefix #9# erc-fill--wrap-merge #7# disp=
lay #7#) 495 497 (wrap-prefix #1# line-prefix #9# erc-fill--wrap-merge #7# =
display #7#) 497 501 (wrap-prefix #1# line-prefix #9#) 502 503 (wrap-prefix=
 #1# line-prefix #10=3D(space :width (- 29 (6))) erc--msg msg erc--spkr "bo=
b" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd) 503=
 506 (wrap-prefix #1# line-prefix #10#) 506 514 (wrap-prefix #1# line-prefi=
x #10#) 515 516 (wrap-prefix #1# line-prefix #11=3D(space :width (- 29 0)) =
erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc-fill--=
wrap-merge #7# erc--pfx erc--pfx-skip-spkr-fwd display #7#) 516 519 (wrap-p=
refix #1# line-prefix #11# erc-fill--wrap-merge #7# display #7#) 519 521 (w=
rap-prefix #1# line-prefix #11# erc-fill--wrap-merge #7# display #7#) 521 5=
26 (wrap-prefix #1# line-prefix #11#) 527 528 (wrap-prefix #1# line-prefix =
#12=3D(space :width (- 29 (8))) erc--msg msg erc--spkr "Dummy" erc--ts 1680=
332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd) 528 533 (wrap-pref=
ix #1# line-prefix #12#) 533 540 (wrap-prefix #1# line-prefix #12#) 541 542=
 (wrap-prefix #1# line-prefix #13=3D(space :width (- 29 0)) erc--msg msg er=
c--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG erc-fill--wrap-merge #7=
# erc--pfx erc--pfx-skip-spkr-fwd display #7#) 542 547 (wrap-prefix #1# lin=
e-prefix #13# erc-fill--wrap-merge #7# display #7#) 547 549 (wrap-prefix #1=
# line-prefix #13# erc-fill--wrap-merge #7# display #7#) 549 553 (wrap-pref=
ix #1# line-prefix #13#))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld b/tes=
t/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
index 9744e659813..8d2959e9ee8 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n=
<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 fi=
eld erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(space :width=
 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts=
 0 wrap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap=
-prefix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# l=
ine-prefix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2=
# display (#5=3D(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)=
))) 192 193 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap=
-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefi=
x #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (w=
rap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 3=
17 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr "b=
ob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=3D(space :wid=
th (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-pre=
fix #1# line-prefix #4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 =
(wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg datestamp erc--ts 16803=
07200 field erc-timestamp) 438 455 (field erc-timestamp wrap-prefix #1# lin=
e-prefix (space :width (- 27 (18)))) 456 457 (erc--msg msg erc--spkr "bob" =
erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=3D(space=
 :width (- 27 (6)))) 457 460 (wrap-prefix #1# line-prefix #6#) 460 467 (wra=
p-prefix #1# line-prefix #6#) 467 468 (field erc-timestamp wrap-prefix #1# =
line-prefix #6#) 468 475 (field erc-timestamp wrap-prefix #1# line-prefix #=
6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg =
msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# lin=
e-prefix #7=3D(space :width (- 27 0)) erc-fill--wrap-merge #8=3D"" display =
#8#) 477 480 (wrap-prefix #1# line-prefix #7# erc-fill--wrap-merge #8# disp=
lay #8#) 480 482 (wrap-prefix #1# line-prefix #7# erc-fill--wrap-merge #8# =
display #8#) 482 485 (wrap-prefix #1# line-prefix #7#) 486 487 (erc--msg ct=
cp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACT=
ION wrap-prefix #1# line-prefix #9=3D(space :width (- 27 (6)))) 487 488 (wr=
ap-prefix #1# line-prefix #9#) 488 491 (wrap-prefix #1# line-prefix #9#) 49=
1 496 (wrap-prefix #1# line-prefix #9#) 497 498 (erc--msg msg erc--spkr "bo=
b" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=3D(s=
pace :width (- 27 (6)))) 498 501 (wrap-prefix #1# line-prefix #10#) 501 507=
 (wrap-prefix #1# line-prefix #10#) 508 509 (erc--msg msg erc--spkr "bob" e=
rc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=3D(space=
 :width (- 27 0)) erc-fill--wrap-merge #8# display #8#) 509 512 (wrap-prefi=
x #1# line-prefix #11# erc-fill--wrap-merge #8# display #8#) 512 514 (wrap-=
prefix #1# line-prefix #11# erc-fill--wrap-merge #8# display #8#) 514 517 (=
wrap-prefix #1# line-prefix #11#) 518 519 (erc--msg ctcp-action erc--spkr "=
bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# l=
ine-prefix #12=3D(space :width (- 27 (2)))) 519 520 (wrap-prefix #1# line-p=
refix #12#) 520 523 (wrap-prefix #1# line-prefix #12#) 523 529 (wrap-prefix=
 #1# line-prefix #12#) 530 531 (erc--msg msg erc--spkr "bob" erc--ts 168033=
2400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=3D(space :width (- 27=
 (6)))) 531 534 (wrap-prefix #1# line-prefix #13#) 534 541 (wrap-prefix #1#=
 line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n=
<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 fi=
eld erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(space :width=
 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts=
 0 erc--pfx erc--pfx-skip-notice-fwd wrap-prefix #1# line-prefix #2=3D(spac=
e :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (fie=
ld erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-timest=
amp wrap-prefix #1# line-prefix #2# display (#5=3D(margin right-margin) #("=
[00:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alic=
e" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #=
1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefix #1# lin=
e-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (wrap-prefi=
x #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 317 349 (w=
rap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr "bob" erc--=
ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-=
prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix=
 #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefix #1# li=
ne-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg =
datestamp erc--ts 1680307200 field erc-timestamp) 438 455 (field erc-timest=
amp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 456 457 (erc--m=
sg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pf=
x-skip-spkr-fwd wrap-prefix #1# line-prefix #6=3D(space :width (- 27 (6))))=
 457 460 (wrap-prefix #1# line-prefix #6#) 460 467 (wrap-prefix #1# line-pr=
efix #6#) 467 468 (field erc-timestamp wrap-prefix #1# line-prefix #6#) 468=
 475 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[=
07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg msg erc--spkr "bob" =
erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-pr=
efix #1# line-prefix #7=3D(space :width (- 27 0)) erc-fill--wrap-merge #8=
=3D"" display #8#) 477 480 (wrap-prefix #1# line-prefix #7# erc-fill--wrap-=
merge #8# display #8#) 480 482 (wrap-prefix #1# line-prefix #7# erc-fill--w=
rap-merge #8# display #8#) 482 485 (wrap-prefix #1# line-prefix #7#) 486 48=
7 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG=
 erc--ctcp ACTION erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-pref=
ix #9=3D(space :width (- 27 (6)))) 487 488 (wrap-prefix #1# line-prefix #9#=
) 488 491 (wrap-prefix #1# line-prefix #9#) 491 496 (wrap-prefix #1# line-p=
refix #9#) 497 498 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cm=
d PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #10=
=3D(space :width (- 27 (6)))) 498 501 (wrap-prefix #1# line-prefix #10#) 50=
1 507 (wrap-prefix #1# line-prefix #10#) 508 509 (erc--msg msg erc--spkr "b=
ob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wra=
p-prefix #1# line-prefix #11=3D(space :width (- 27 0)) erc-fill--wrap-merge=
 #8# display #8#) 509 512 (wrap-prefix #1# line-prefix #11# erc-fill--wrap-=
merge #8# display #8#) 512 514 (wrap-prefix #1# line-prefix #11# erc-fill--=
wrap-merge #8# display #8#) 514 517 (wrap-prefix #1# line-prefix #11#) 518 =
519 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVM=
SG erc--ctcp ACTION erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-pr=
efix #12=3D(space :width (- 27 (2)))) 519 520 (wrap-prefix #1# line-prefix =
#12#) 520 523 (wrap-prefix #1# line-prefix #12#) 523 529 (wrap-prefix #1# l=
ine-prefix #12#) 530 531 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 e=
rc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix=
 #13=3D(space :width (- 27 (6)))) 531 534 (wrap-prefix #1# line-prefix #13#=
) 534 541 (wrap-prefix #1# line-prefix #13#))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pr=
e-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-=
01.eld
index 36729b890be..c3829c59f36 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n=
<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 fi=
eld erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(space :width=
 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts=
 0 wrap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap=
-prefix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# l=
ine-prefix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2=
# display (#5=3D(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)=
))) 192 193 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap=
-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefi=
x #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (w=
rap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 3=
17 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr "b=
ob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=3D(space :wid=
th (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-pre=
fix #1# line-prefix #4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 =
(wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg datestamp erc--ts 16803=
07200 field erc-timestamp) 438 455 (field erc-timestamp wrap-prefix #1# lin=
e-prefix (space :width (- 27 (18)))) 456 457 (erc--msg msg erc--spkr "bob" =
erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=3D(space=
 :width (- 27 (6)))) 457 460 (wrap-prefix #1# line-prefix #6#) 460 467 (wra=
p-prefix #1# line-prefix #6#) 467 468 (field erc-timestamp wrap-prefix #1# =
line-prefix #6#) 468 475 (field erc-timestamp wrap-prefix #1# line-prefix #=
6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg =
msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# lin=
e-prefix #7=3D(space :width (- 27 #10=3D(2))) erc-fill--wrap-merge t displa=
y #8=3D#("> " 0 1 (font-lock-face shadow))) 477 480 (wrap-prefix #1# line-p=
refix #7# erc-fill--wrap-merge t display #8#) 480 482 (wrap-prefix #1# line=
-prefix #7# erc-fill--wrap-merge t display #8#) 482 485 (wrap-prefix #1# li=
ne-prefix #7#) 486 487 (erc--msg ctcp-action erc--spkr "bob" erc--ts 168033=
2400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=3D(sp=
ace :width (- 27 (6)))) 487 488 (wrap-prefix #1# line-prefix #9#) 488 491 (=
wrap-prefix #1# line-prefix #9#) 491 496 (wrap-prefix #1# line-prefix #9#) =
497 498 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG w=
rap-prefix #1# line-prefix #11=3D(space :width (- 27 (6)))) 498 501 (wrap-p=
refix #1# line-prefix #11#) 501 507 (wrap-prefix #1# line-prefix #11#) 508 =
509 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-=
prefix #1# line-prefix #12=3D(space :width (- 27 #10#)) erc-fill--wrap-merg=
e t display #8#) 509 512 (wrap-prefix #1# line-prefix #12# erc-fill--wrap-m=
erge t display #8#) 512 514 (wrap-prefix #1# line-prefix #12# erc-fill--wra=
p-merge t display #8#) 514 517 (wrap-prefix #1# line-prefix #12#) 518 519 (=
erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG er=
c--ctcp ACTION wrap-prefix #1# line-prefix #13=3D(space :width (- 27 (2))))=
 519 520 (wrap-prefix #1# line-prefix #13#) 520 523 (wrap-prefix #1# line-p=
refix #13#) 523 529 (wrap-prefix #1# line-prefix #13#) 530 531 (erc--msg ms=
g erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-=
prefix #14=3D(space :width (- 27 (6)))) 531 534 (wrap-prefix #1# line-prefi=
x #14#) 534 541 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n=
<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 fi=
eld erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(space :width=
 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts=
 0 erc--pfx erc--pfx-skip-notice-fwd wrap-prefix #1# line-prefix #2=3D(spac=
e :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (fie=
ld erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-timest=
amp wrap-prefix #1# line-prefix #2# display (#5=3D(margin right-margin) #("=
[00:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alic=
e" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #=
1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefix #1# lin=
e-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (wrap-prefi=
x #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 317 349 (w=
rap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr "bob" erc--=
ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-=
prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix=
 #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefix #1# li=
ne-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg =
datestamp erc--ts 1680307200 field erc-timestamp) 438 455 (field erc-timest=
amp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 456 457 (erc--m=
sg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pf=
x-skip-spkr-fwd wrap-prefix #1# line-prefix #6=3D(space :width (- 27 (6))))=
 457 460 (wrap-prefix #1# line-prefix #6#) 460 467 (wrap-prefix #1# line-pr=
efix #6#) 467 468 (field erc-timestamp wrap-prefix #1# line-prefix #6#) 468=
 475 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[=
07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg msg erc--spkr "bob" =
erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-pr=
efix #1# line-prefix #7=3D(space :width (- 27 #10=3D(2))) erc-fill--wrap-me=
rge t display #8=3D#("> " 0 1 (font-lock-face shadow))) 477 480 (wrap-prefi=
x #1# line-prefix #7# erc-fill--wrap-merge t display #8#) 480 482 (wrap-pre=
fix #1# line-prefix #7# erc-fill--wrap-merge t display #8#) 482 485 (wrap-p=
refix #1# line-prefix #7#) 486 487 (erc--msg ctcp-action erc--spkr "bob" er=
c--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION erc--pfx erc--pfx-skip-s=
pkr-fwd wrap-prefix #1# line-prefix #9=3D(space :width (- 27 (6)))) 487 488=
 (wrap-prefix #1# line-prefix #9#) 488 491 (wrap-prefix #1# line-prefix #9#=
) 491 496 (wrap-prefix #1# line-prefix #9#) 497 498 (erc--msg msg erc--spkr=
 "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd =
wrap-prefix #1# line-prefix #11=3D(space :width (- 27 (6)))) 498 501 (wrap-=
prefix #1# line-prefix #11#) 501 507 (wrap-prefix #1# line-prefix #11#) 508=
 509 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc-=
-pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #12=3D(space :width=
 (- 27 #10#)) erc-fill--wrap-merge t display #8#) 509 512 (wrap-prefix #1# =
line-prefix #12# erc-fill--wrap-merge t display #8#) 512 514 (wrap-prefix #=
1# line-prefix #12# erc-fill--wrap-merge t display #8#) 514 517 (wrap-prefi=
x #1# line-prefix #12#) 518 519 (erc--msg ctcp-action erc--spkr "bob" erc--=
ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION erc--pfx erc--pfx-skip-spkr=
-fwd wrap-prefix #1# line-prefix #13=3D(space :width (- 27 (2)))) 519 520 (=
wrap-prefix #1# line-prefix #13#) 520 523 (wrap-prefix #1# line-prefix #13#=
) 523 529 (wrap-prefix #1# line-prefix #13#) 530 531 (erc--msg msg erc--spk=
r "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd=
 wrap-prefix #1# line-prefix #14=3D(space :width (- 27 (6)))) 531 534 (wrap=
-prefix #1# line-prefix #14#) 534 541 (wrap-prefix #1# line-prefix #14#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld =
b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
index 0228e716731..e64e7f259eb 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 27) line-prefix (space :width (- 27 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(sp=
ace :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (f=
ield erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-time=
stamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00=
:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" =
erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (=
- 27 (8)))) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix =
#1# line-prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wra=
p-prefix #1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350=
 351 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #=
1# line-prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# lin=
e-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefi=
x #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 27) line-prefix (space :width (- 27 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wr=
ap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap-pref=
ix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-p=
refix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2# dis=
play ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 19=
3 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--=
pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #3=3D(space :width (- 27 (8))=
)) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-=
prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix =
#1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc=
--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip=
-spkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 27 (6)))) 351 3=
54 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #=
4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line=
-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld =
b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
index 9ab89041b53..c9bcd8af107 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 29) line-prefix (space :width (- 29 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(sp=
ace :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (f=
ield erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-time=
stamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00=
:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" =
erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (=
- 29 (8)))) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix =
#1# line-prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wra=
p-prefix #1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350=
 351 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #=
1# line-prefix #4=3D(space :width (- 29 (6)))) 351 354 (wrap-prefix #1# lin=
e-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefi=
x #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 29) line-prefix (space :width (- 29 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wr=
ap-prefix #1# line-prefix #2=3D(space :width (- 29 (4)))) 22 183 (wrap-pref=
ix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-p=
refix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2# dis=
play ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 19=
3 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--=
pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #3=3D(space :width (- 29 (8))=
)) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-=
prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix =
#1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc=
--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip=
-spkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 29 (6)))) 351 3=
54 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #=
4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line=
-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld b=
/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
index 87ea4692d9d..f7088c2ae40 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 25) line-prefix (space :width (- 25 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(sp=
ace :width (- 25 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (f=
ield erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-time=
stamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00=
:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" =
erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (=
- 25 (8)))) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix =
#1# line-prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wra=
p-prefix #1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350=
 351 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #=
1# line-prefix #4=3D(space :width (- 25 (6)))) 351 354 (wrap-prefix #1# lin=
e-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefi=
x #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 25) line-prefix (space :width (- 25 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wr=
ap-prefix #1# line-prefix #2=3D(space :width (- 25 (4)))) 22 183 (wrap-pref=
ix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-p=
refix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2# dis=
play ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 19=
3 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--=
pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #3=3D(space :width (- 25 (8))=
)) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-=
prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix =
#1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc=
--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip=
-spkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 25 (6)))) 351 3=
54 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #=
4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line=
-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld =
b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
index 0228e716731..e64e7f259eb 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 27) line-prefix (space :width (- 27 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(sp=
ace :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (f=
ield erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-time=
stamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00=
:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" =
erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (=
- 27 (8)))) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix =
#1# line-prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wra=
p-prefix #1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350=
 351 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #=
1# line-prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# lin=
e-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefi=
x #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 27) line-prefix (space :width (- 27 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wr=
ap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap-pref=
ix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-p=
refix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2# dis=
play ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 19=
3 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--=
pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #3=3D(space :width (- 27 (8))=
)) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-=
prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix =
#1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc=
--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip=
-spkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 27 (6)))) 351 3=
54 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #=
4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line=
-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld b/t=
est/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
index 5405ca2a7dc..13d8d30acb4 100644
--- a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
+++ b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n<b=
ob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> S=
omebody stop me\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3=
 20 (field erc-timestamp wrap-prefix #1=3D(space :width 27) line-prefix (sp=
ace :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# l=
ine-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-pre=
fix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-prefix #2#) 184 =
191 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin r=
ight-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (line-spacing=
 0.5) 192 193 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wr=
ap-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-pre=
fix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 =
(wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#)=
 317 349 (wrap-prefix #1# line-prefix #3#) 349 350 (line-spacing 0.5) 350 3=
51 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1#=
 line-prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# line-=
prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefix =
#1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 436 437 (lin=
e-spacing 0.5) 437 438 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRI=
VMSG wrap-prefix #1# line-prefix #5=3D(space :width (- 27 0)) erc-fill--wra=
p-merge #6=3D"" display #6#) 438 441 (wrap-prefix #1# line-prefix #5# erc-f=
ill--wrap-merge #6# display #6#) 441 443 (wrap-prefix #1# line-prefix #5# e=
rc-fill--wrap-merge #6# display #6#) 443 467 (wrap-prefix #1# line-prefix #=
5#) 467 468 (line-spacing 0.5) 468 469 (erc--msg notice erc--ts 0 wrap-pref=
ix #1# line-prefix #7=3D(space :width (- 27 (4)))) 469 485 (wrap-prefix #1#=
 line-prefix #7#) 486 487 (erc--msg notice erc--ts 0 wrap-prefix #1# line-p=
refix #8=3D(space :width (- 27 (4)))) 487 503 (wrap-prefix #1# line-prefix =
#8#) 503 504 (line-spacing 0.5) 504 505 (erc--msg msg erc--spkr "bob" erc--=
ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #9=3D(space :width (- 27 =
(6)))) 505 508 (wrap-prefix #1# line-prefix #9#) 508 526 (wrap-prefix #1# l=
ine-prefix #9#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n<b=
ob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> S=
omebody stop me\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3=
 20 (field erc-timestamp wrap-prefix #1=3D(space :width 27) line-prefix (sp=
ace :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx=
-skip-notice-fwd wrap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))=
) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (field erc-timestamp wra=
p-prefix #1# line-prefix #2#) 184 191 (field erc-timestamp wrap-prefix #1# =
line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible t=
imestamp)))) 191 192 (line-spacing 0.5) 192 193 (erc--msg msg erc--spkr "al=
ice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix=
 #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefix #1# l=
ine-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (wrap-pre=
fix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 317 349 =
(wrap-prefix #1# line-prefix #3#) 349 350 (line-spacing 0.5) 350 351 (erc--=
msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-s=
pkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 27 (6)))) 351 354=
 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#=
) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-p=
refix #4#) 436 437 (line-spacing 0.5) 437 438 (erc--msg msg erc--spkr "bob"=
 erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1#=
 line-prefix #5=3D(space :width (- 27 0)) erc-fill--wrap-merge #6=3D"" disp=
lay #6#) 438 441 (wrap-prefix #1# line-prefix #5# erc-fill--wrap-merge #6# =
display #6#) 441 443 (wrap-prefix #1# line-prefix #5# erc-fill--wrap-merge =
#6# display #6#) 443 467 (wrap-prefix #1# line-prefix #5#) 467 468 (line-sp=
acing 0.5) 468 469 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice=
-fwd wrap-prefix #1# line-prefix #7=3D(space :width (- 27 (4)))) 469 485 (w=
rap-prefix #1# line-prefix #7#) 486 487 (erc--msg notice erc--ts 0 erc--pfx=
 erc--pfx-skip-notice-fwd wrap-prefix #1# line-prefix #8=3D(space :width (-=
 27 (4)))) 487 503 (wrap-prefix #1# line-prefix #8#) 503 504 (line-spacing =
0.5) 504 505 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--=
pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #9=3D(space :width (=
- 27 (6)))) 505 508 (wrap-prefix #1# line-prefix #9#) 508 526 (wrap-prefix =
#1# line-prefix #9#))
diff --git a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld b/te=
st/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
index 1b22b6c5cfd..2310b1d7af2 100644
--- a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
@@ -1 +1 @@
-#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O=
. If you do not wish for everything you send to be readable by the server o=
wner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fo=
ol: to the purpose. What was done to Elbow's wife, that he hath cause to co=
mplain of? Come me to what was done to her.\n[00:00]<bob> alice: Either you=
r unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc=
--msg notice erc--ts 0 display #3=3D(#5=3D(margin left-margin) #("[00:00]" =
0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-tim=
estamp wrap-prefix #1=3D(space :width 27) line-prefix #2=3D(space :width (-=
 27 (4)))) 3 9 (display #3# field erc-timestamp wrap-prefix #1# line-prefix=
 #2#) 9 171 (wrap-prefix #1# line-prefix #2#) 172 173 (erc--msg msg erc--ts=
 0 erc--spkr "alice" erc--cmd PRIVMSG display #6=3D(#5# #("[00:00]" 0 7 (in=
visible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp =
wrap-prefix #1# line-prefix #4=3D(space :width (- 27 (8)))) 173 179 (displa=
y #6# field erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-pr=
efix #1# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187=
 (wrap-prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#=
) 190 303 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-p=
refix #4#) 337 338 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG=
 display #8=3D(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-=
timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #7=3D(spa=
ce :width (- 27 (6)))) 338 344 (display #8# field erc-timestamp wrap-prefix=
 #1# line-prefix #7#) 344 345 (wrap-prefix #1# line-prefix #7#) 345 348 (wr=
ap-prefix #1# line-prefix #7#) 348 350 (wrap-prefix #1# line-prefix #7#) 35=
0 355 (wrap-prefix #1# line-prefix #7#) 355 430 (wrap-prefix #1# line-prefi=
x #7#))
\ No newline at end of file
+#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O=
. If you do not wish for everything you send to be readable by the server o=
wner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fo=
ol: to the purpose. What was done to Elbow's wife, that he hath cause to co=
mplain of? Come me to what was done to her.\n[00:00]<bob> alice: Either you=
r unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc=
--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd display #3=3D(#5=
=3D(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face=
 erc-timestamp-face))) field erc-timestamp wrap-prefix #1=3D(space :width 2=
7) line-prefix #2=3D(space :width (- 27 (4)))) 3 9 (display #3# field erc-t=
imestamp wrap-prefix #1# line-prefix #2#) 9 171 (wrap-prefix #1# line-prefi=
x #2#) 172 173 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG e=
rc--pfx erc--pfx-skip-spkr-fwd display #6=3D(#5# #("[00:00]" 0 7 (invisible=
 timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-pr=
efix #1# line-prefix #4=3D(space :width (- 27 (8)))) 173 179 (display #6# f=
ield erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-prefix #1=
# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187 (wrap-=
prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#) 190 3=
03 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-prefix #=
4#) 337 338 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG erc--p=
fx erc--pfx-skip-spkr-fwd display #8=3D(#5# #("[00:00]" 0 7 (invisible time=
stamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix =
#1# line-prefix #7=3D(space :width (- 27 (6)))) 338 344 (display #8# field =
erc-timestamp wrap-prefix #1# line-prefix #7#) 344 345 (wrap-prefix #1# lin=
e-prefix #7#) 345 348 (wrap-prefix #1# line-prefix #7#) 348 350 (wrap-prefi=
x #1# line-prefix #7#) 350 355 (wrap-prefix #1# line-prefix #7#) 355 430 (w=
rap-prefix #1# line-prefix #7#))
diff --git a/test/lisp/erc/resources/match/functions/custom-match-log b/tes=
t/lisp/erc/resources/match/functions/custom-match-log
new file mode 100644
index 00000000000..4b07e4c4fe6
--- /dev/null
+++ b/test/lisp/erc/resources/match/functions/custom-match-log
@@ -0,0 +1,39 @@
+001 ?: Welcome to the foonet IRC Network tester
+002 ?: Your host is irc.foonet.org, running version ergo-v2.17.0
+003 ?: This server was created Sat, 25 Apr 2026 06:08:20 UTC
+004 ?: irc.foonet.org ergo-v2.17.0 BERTZios CEIMRUabefhiklmnoqstuv
+005 ?: AWAYLEN=3D390 BOT=3DB CASEMAPPING=3Dascii CHANLIMIT=3D#:100 CHANMOD=
ES=3DIbe,k,fl,CEMRUimnstu CHANNELLEN=3D64 CHANTYPES=3D# CHATHISTORY=3D25 EL=
IST=3DU EXCEPTS EXTBAN=3D,m FORWARD=3Df INVEX are supported by this server
+005 ?: KICKLEN=3D390 MAXLIST=3DbeI:60 MAXTARGETS=3D4 MODES MONITOR=3D100 M=
SGREFTYPES=3Dmsgid,timestamp NETWORK=3Dfoonet NICKLEN=3D32 PREFIX=3D(qaohv)=
~&@%+ SAFELIST SAFERATE STATUSMSG=3D~&@%+ TARGMAX=3DNAMES:1,LIST:1,KICK:,WH=
OIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 are supported by =
this server
+005 ?: TOPICLEN=3D390 UTF8ONLY WHOX draft/CHATHISTORY=3D25 are supported b=
y this server
+251 ?: There are 0 users and 4 invisible on 1 server(s)
+252 ?: 0 operator(s) online
+253 ?: 0 unknown connection(s)
+254 ?: 2 channels formed
+255 ?: I have 4 clients and 0 servers
+265 ?: Current local users 4, max 4
+266 ?: Current global users 4, max 4
+422 ?: MOTD File is missing
+221 ?: User modes for tester: +i
+NOTICE ?: This server is in debug mode and is logging all user I/O. If you=
 do not wish for everything you send to be readable by the server owner(s),=
 please disconnect.
+221 ?: User modes for tester: +i
+JOIN tester: You have joined channel #chan
+353 ?: Users on #chan: @fsbot alice bob tester
+PRIVMSG alice: tester, welcome!
+PRIVMSG bob: tester, welcome!
+PRIVMSG bob: alice: No tearing, lady; I perceive you know it.
+324 ?: #chan modes: +Cnt
+329 ?: #chan was created on @@DATESTAMP@@
+PRIVMSG alice: There were none principal; they were all like one another a=
s half-pence are; every one fault seeming monstrous till his fellow fault c=
ame to match it.
+PRIVMSG bob: As much as ever Coriolanus did.
+NOTICE NickServ: *** NickServ HELP ***
+NOTICE NickServ: Syntax: IDENTIFY <username> [password]
+NOTICE NickServ: IDENTIFY lets you login to the given username using eithe=
r password auth, or
+NOTICE NickServ: certfp (your client certificate) if a password is not giv=
en.
+NOTICE NickServ: *** End of NickServ HELP ***
+PRIVMSG bob: alice: Deceive me not now, Navarre is infected.
+PRIVMSG bob: Bear me to prison, where I am committed.
+PRIVMSG alice: Wisely and slow; they stumble that run fast.
+PRIVMSG alice: bob: Thus men may grow wiser every day: it is the first tim=
e that ever I heard breaking of ribs was sport for ladies.
+QUIT tester: tester ([email protected]) has quit: Quit: ERC 5.6.2-git (=
IRC client for GNU Emacs 31.0.50)
+QUIT tester: tester ([email protected]) has quit: Quit: ERC 5.6.2-git (=
IRC client for GNU Emacs 31.0.50)
+ERROR ?: ERROR from 127.0.0.1: Quit: ERC 5.6.2-git (IRC client for GNU Ema=
cs 31.0.50)
diff --git a/test/lisp/erc/resources/match/functions/custom.eld b/test/lisp=
/erc/resources/match/functions/custom.eld
new file mode 100644
index 00000000000..073c1e403bb
--- /dev/null
+++ b/test/lisp/erc/resources/match/functions/custom.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK tester"))
+((user 10 "USER tester 0 * :tester")
+ (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network test=
er")
+ (0.00 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running v=
ersion ergo-v2.17.0")
+ (0.00 ":irc.foonet.org 003 tester :This server was created Sat, 25 Apr 20=
26 06:08:20 UTC")
+ (0.00 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.17.0 BERTZios CE=
IMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 tester AWAYLEN=3D390 BOT=3DB CASEMAPPING=3Dasc=
ii CHANLIMIT=3D#:100 CHANMODES=3DIbe,k,fl,CEMRUimnstu CHANNELLEN=3D64 CHANT=
YPES=3D# CHATHISTORY=3D25 ELIST=3DU EXCEPTS EXTBAN=3D,m FORWARD=3Df INVEX :=
are supported by this server")
+ (0.00 ":irc.foonet.org 005 tester KICKLEN=3D390 MAXLIST=3DbeI:60 MAXTARGE=
TS=3D4 MODES MONITOR=3D100 MSGREFTYPES=3Dmsgid,timestamp NETWORK=3Dfoonet N=
ICKLEN=3D32 PREFIX=3D(qaohv)~&@%+ SAFELIST SAFERATE STATUSMSG=3D~&@%+ TARGM=
AX=3DNAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,M=
ONITOR:100 :are supported by this server")
+ (0.00 ":irc.foonet.org 005 tester TOPICLEN=3D390 UTF8ONLY WHOX draft/CHAT=
HISTORY=3D25 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 tester :There are 0 users and 4 invisible on 1=
 server(s)")
+ (0.00 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.00 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.10 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0.00 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0.00 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0.00 ":irc.foonet.org 422 tester :MOTD File is missing")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":irc.foonet.org NOTICE tester :This server is in debug mode and is=
 logging all user I/O. If you do not wish for everything you send to be rea=
dable by the server owner(s), please disconnect."))
+((mode 10 "MODE tester +i"))
+((join 10 "JOIN #chan")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":[email protected] JOIN #chan")
+ (0.01 ":irc.foonet.org 353 tester =3D #chan :@fsbot alice bob tester")
+ (0.00 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.01 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0.00 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0.03 ":[email protected] PRIVMSG #chan :alice: No tearing, lady; =
I perceive you know it."))
+((mode-chan 10 "MODE #chan")
+ (0.00 ":irc.foonet.org 324 tester #chan +Cnt")
+ (0.00 ":irc.foonet.org 329 tester #chan 1777097304")
+ (0.01 ":[email protected] PRIVMSG #chan :There were none princip=
al; they were all like one another as half-pence are; every one fault seemi=
ng monstrous till his fellow fault came to match it."))
+((privmsg-bob 10 "PRIVMSG bob :hi")
+ (0.03 ":[email protected] PRIVMSG tester :As much as ever Coriolan=
us did."))
+((privmsg-nickserv 10 "PRIVMSG NickServ :help identify")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :*** \2NickServ HELP\2 =
***")
+ (0.00 ":NickServ!NickServ@localhost NOTICE tester :Syntax: \2IDENTIFY <us=
ername> [password]\2")
+ (0.00 ":NickServ!NickServ@localhost NOTICE tester :IDENTIFY lets you logi=
n to the given username using either password auth, or")
+ (0.00 ":NickServ!NickServ@localhost NOTICE tester :certfp (your client ce=
rtificate) if a password is not given.")
+ (0.00 ":NickServ!NickServ@localhost NOTICE tester :*** \2End of NickServ =
HELP\2 ***")
+ ;;
+ (0.07 ":[email protected] PRIVMSG #chan :alice: Deceive me not now=
, Navarre is infected.")
+ (0.04 ":[email protected] PRIVMSG #chan :Bear me to prison, where =
I am committed.")
+ (0.07 ":[email protected] PRIVMSG #chan :Wisely and slow; they s=
tumble that run fast.")
+ (0.03 ":[email protected] PRIVMSG #chan :bob: Thus men may grow =
wiser every day: it is the first time that ever I heard breaking of ribs wa=
s sport for ladies."))
+((quit 10 "QUIT :")
+ (0.01 ":[email protected] QUIT :Quit: \2ERC\2 5.6.2-git (IRC cl=
ient for GNU Emacs 31.0.50)")
+ (0.00 "ERROR :Quit: \2ERC\2 5.6.2-git (IRC client for GNU Emacs 31.0.50)"=
))
--=20
2.53.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0001-5.7-Remove-some-forward-declarations-from-ERC-tests.patch

From e85af1643eac0e11fc9183c2a9f3a9d356574505 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Tue, 16 Sep 2025 18:43:58 -0700
Subject: [PATCH 1/6] [5.7] ; Remove some forward declarations from ERC tests

* lisp/erc/erc.el (erc--insert-before-markers-transplanting-hidden): Fix
what's almost certainly a typo since the return value is undefined.
* test/lisp/erc/erc-scenarios-join-display-context.el: Require `erc-join'.
* test/lisp/erc/erc-scenarios-log.el: Remove forward declares and
require `erc-stamp'.
* test/lisp/erc/erc-scenarios-match.el: Require `erc-match'.
* test/lisp/erc/erc-scenarios-services-misc.el: Require `erc-services'.
* test/lisp/erc/erc-tests.el (erc-tests--modules): Reflow into single
column for easier management of divergent WIP patch sets, which should
help minimize manual surgery when later applying them atop each other.
* test/lisp/erc/resources/erc-scenarios-common.el: Remove unused
requires and declarations from top of file.
(erc-scenarios-common-with-cleanup): Generate local defvars in test
scope for likely unloaded user options.
(erc-scenarios-common--assert-date-stamps): Don't run when erc-stamp
isn't loaded.
* test/lisp/erc/resources/erc-tests-common.el
(erc-tests-common-make-server-buf): Accept a buffer for the NAME arg.
---
 lisp/erc/erc.el                               |   3 +-
 .../erc/erc-scenarios-join-display-context.el |   2 +
 test/lisp/erc/erc-scenarios-log.el            |   6 +-
 test/lisp/erc/erc-scenarios-match.el          |   5 +-
 test/lisp/erc/erc-scenarios-services-misc.el  |   3 +-
 test/lisp/erc/erc-tests.el                    |  45 +++++++-
 .../erc/resources/erc-scenarios-common.el     | 103 +++++++++---------
 test/lisp/erc/resources/erc-tests-common.el   |   2 +-
 8 files changed, 101 insertions(+), 68 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 6facb7966b0..ae7cf479451 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3748,7 +3748,8 @@ erc--insert-before-markers-transplanting-hidden
          (new (and before (erc--solo (cl-intersection b a)))))
     (when new
       (erc--remove-from-prop-value-list (1- (point)) (point) 'invisible a))
-    (prog1 (insert-before-markers string)
+    (progn
+      (insert-before-markers string)
       (when new
         (erc--merge-prop (1- (point)) (point) 'invisible new)))))
 
diff --git a/test/lisp/erc/erc-scenarios-join-display-context.el b/test/lisp/erc/erc-scenarios-join-display-context.el
index 07d7ff1519e..e525eb59f82 100644
--- a/test/lisp/erc/erc-scenarios-join-display-context.el
+++ b/test/lisp/erc/erc-scenarios-join-display-context.el
@@ -26,6 +26,8 @@
   (let ((load-path (cons (ert-resource-directory) load-path)))
     (require 'erc-scenarios-common)))
 
+(require 'erc-join)
+
 ;; This module uses the list `erc-join--requested-channels' to detect
 ;; whether a JOIN response was likely triggered by an outgoing JOIN
 ;; emitted on behalf of `erc-autojoin-channels-alist'.  When a related
diff --git a/test/lisp/erc/erc-scenarios-log.el b/test/lisp/erc/erc-scenarios-log.el
index 7452062e3c5..4f190ae898e 100644
--- a/test/lisp/erc/erc-scenarios-log.el
+++ b/test/lisp/erc/erc-scenarios-log.el
@@ -28,8 +28,7 @@
 
 (require 'erc-log)
 (require 'erc-truncate)
-
-(defvar erc-timestamp-format-left)
+(require 'erc-stamp)
 
 (ert-deftest erc-scenarios-log--kill-hook ()
   :tags '(:expensive-test)
@@ -327,9 +326,6 @@ erc-scenarios-log--truncate/left-stamps
        (funcall expect 1 "loathed enemy")
        (funcall expect -0.001 "please your lordship")))))
 
-(defvar erc-insert-timestamp-function)
-(declare-function erc-insert-timestamp-left "erc-stamp" (string))
-
 (ert-deftest erc-scenarios-log--save-buffer-in-logs/truncate-on-save ()
   :tags '(:expensive-test)
   (with-suppressed-warnings ((obsolete erc-truncate-buffer-on-save))
diff --git a/test/lisp/erc/erc-scenarios-match.el b/test/lisp/erc/erc-scenarios-match.el
index bc5ebe27ce9..b22cff18c46 100644
--- a/test/lisp/erc/erc-scenarios-match.el
+++ b/test/lisp/erc/erc-scenarios-match.el
@@ -24,10 +24,7 @@
   (let ((load-path (cons (ert-resource-directory) load-path)))
     (require 'erc-scenarios-common)))
 
-(eval-when-compile
-  (require 'erc-join)
-  (require 'erc-match))
-
+(require 'erc-match)
 (require 'erc-stamp)
 (require 'erc-fill)
 
diff --git a/test/lisp/erc/erc-scenarios-services-misc.el b/test/lisp/erc/erc-scenarios-services-misc.el
index bc6522ae110..6a7d3c2f882 100644
--- a/test/lisp/erc/erc-scenarios-services-misc.el
+++ b/test/lisp/erc/erc-scenarios-services-misc.el
@@ -24,8 +24,7 @@
   (let ((load-path (cons (ert-resource-directory) load-path)))
     (require 'erc-scenarios-common)))
 
-(eval-when-compile (require 'erc-join)
-                   (require 'erc-services))
+(require 'erc-services)
 
 (ert-deftest erc-scenarios-services-password ()
   :tags '(:expensive-test)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index dd439e73fc9..fb9be63de23 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -3685,12 +3685,45 @@ erc-channel-user
       (should (= 0 (erc-channel-user-status u))))))
 
 (defconst erc-tests--modules
-  '( autoaway autojoin bufbar button capab-identify
-     command-indicator completion dcc fill identd
-     imenu irccontrols keep-place list log match menu move-to-prompt netsplit
-     networks nickbar nicks noncommands notifications notify page readonly
-     replace ring sasl scrolltobottom services smiley sound
-     spelling stamp track truncate unmorse xdcc))
+  '(autoaway
+    autojoin
+    bufbar
+    button
+    capab-identify
+    command-indicator
+    completion
+    dcc
+    fill
+    identd
+    imenu
+    irccontrols
+    keep-place
+    list log
+    match
+    menu
+    move-to-prompt
+    netsplit
+    networks
+    nickbar
+    nicks
+    noncommands
+    notifications
+    notify
+    page
+    readonly
+    replace
+    ring
+    sasl
+    scrolltobottom
+    services
+    smiley
+    sound
+    spelling
+    stamp
+    track
+    truncate
+    unmorse
+    xdcc))
 
 ;; Ensure that `:initialize' doesn't change the ordering of the
 ;; members because otherwise the widget's state is "edited".
diff --git a/test/lisp/erc/resources/erc-scenarios-common.el b/test/lisp/erc/resources/erc-scenarios-common.el
index fa187cc59f7..5e1686ab7b7 100644
--- a/test/lisp/erc/resources/erc-scenarios-common.el
+++ b/test/lisp/erc/resources/erc-scenarios-common.el
@@ -93,13 +93,6 @@
 
 (require 'erc)
 
-(eval-when-compile (require 'erc-join)
-                   (require 'erc-services)
-                   (require 'erc-fill))
-
-(declare-function erc-network "erc-networks")
-(defvar erc-network)
-
 (defvar erc-scenarios-common--resources-dir
   (expand-file-name "../" (ert-resource-directory)))
 
@@ -149,7 +142,8 @@ erc-scenarios-common--print-trace
       (auth-source-do-cache nil)
       (timer-list (copy-sequence timer-list))
       (timer-idle-list (copy-sequence timer-idle-list))
-      (erc-auth-source-parameters-join-function nil)
+      ;; This binding exists to protect the default value because ERC
+      ;; adds all joined channels automatically.
       (erc-autojoin-channels-alist nil)
       (erc-server-auto-reconnect nil)
       (erc-after-connect nil)
@@ -178,47 +172,56 @@ erc-scenarios-common-with-cleanup
 
   (let* ((orig-autojoin-mode (make-symbol "orig-autojoin-mode"))
          (combined `((,orig-autojoin-mode (bound-and-true-p erc-autojoin-mode))
-                     ,@(erc-scenarios-common--make-bindings bindings))))
-
-    `(erc-d-t-with-cleanup (,@combined)
-
-         (ert-info ("Restore autojoin, etc., kill ERC buffers")
+                     ,@(erc-scenarios-common--make-bindings bindings)))
+         (dynvars ()))
+
+    ;; Declare "erc-" variables dynamic in test scope.
+    (dolist (binder combined)
+      (setq binder (ensure-list binder))
+      (when (and (string-prefix-p "erc-" (symbol-name (car binder)))
+                 (not (special-variable-p (car binder))))
+        (push `(defvar ,(car binder)) dynvars)))
+    `(progn
+       ,@dynvars
+       (erc-d-t-with-cleanup (,@combined)
+
+           (ert-info ("Restore autojoin, etc., kill ERC buffers")
+             (dolist (buf (buffer-list))
+               (when-let* ((erc-d-u--process-buffer)
+                           (proc (get-buffer-process buf)))
+                 (delete-process proc)))
+
+             (erc-scenarios-common--remove-silence)
+
+             (when erc-scenarios-common-extra-teardown
+               (ert-info ("Running extra teardown")
+                 (funcall erc-scenarios-common-extra-teardown)))
+
+             (erc-buffer-do #'erc-scenarios-common--assert-date-stamps)
+             (when (and (boundp 'erc-autojoin-mode)
+                        (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
+               (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
+
+             (when (or noninteractive erc-scenarios-common--graphical-p)
+               (when noninteractive
+                 (erc-scenarios-common--print-trace))
+               (erc-d-t-kill-related-buffers)
+               (delete-other-windows)))
+
+         (erc-scenarios-common--add-silence)
+
+         (ert-info ("Wait for dumb server")
            (dolist (buf (buffer-list))
-             (when-let* ((erc-d-u--process-buffer)
-                         (proc (get-buffer-process buf)))
-               (delete-process proc)))
-
-           (erc-scenarios-common--remove-silence)
-
-           (when erc-scenarios-common-extra-teardown
-             (ert-info ("Running extra teardown")
-               (funcall erc-scenarios-common-extra-teardown)))
-
-           (erc-buffer-do #'erc-scenarios-common--assert-date-stamps)
-           (when (and (boundp 'erc-autojoin-mode)
-                      (not (eq erc-autojoin-mode ,orig-autojoin-mode)))
-             (erc-autojoin-mode (if ,orig-autojoin-mode +1 -1)))
-
-           (when (or noninteractive erc-scenarios-common--graphical-p)
-             (when noninteractive
-               (erc-scenarios-common--print-trace))
-             (erc-d-t-kill-related-buffers)
-             (delete-other-windows)))
-
-       (erc-scenarios-common--add-silence)
-
-       (ert-info ("Wait for dumb server")
-         (dolist (buf (buffer-list))
-           (with-current-buffer buf
-             (when erc-d-u--process-buffer
-               (erc-d-t-search-for 3 "Starting")))))
+             (with-current-buffer buf
+               (when erc-d-u--process-buffer
+                 (erc-d-t-search-for 3 "Starting")))))
 
-       (ert-info ("Activate erc-debug-irc-protocol")
-         (unless (and (or noninteractive erc-scenarios-common--graphical-p)
-                      (not erc-debug-irc-protocol))
-           (erc-toggle-debug-irc-protocol)))
+         (ert-info ("Activate erc-debug-irc-protocol")
+           (unless (and (or noninteractive erc-scenarios-common--graphical-p)
+                        (not erc-debug-irc-protocol))
+             (erc-toggle-debug-irc-protocol)))
 
-       ,@body)))
+         ,@body))))
 
 (defvar erc-scenarios-common--term-size '(34 . 80))
 (declare-function term-char-mode "term" nil)
@@ -336,9 +339,11 @@ erc-scenarios-common-with-noninteractive-in-term
 
 (defun erc-scenarios-common--assert-date-stamps ()
   "Ensure all date stamps are accounted for."
-  (dolist (stamp erc-stamp--date-stamps)
-    (should (eq 'datestamp (get-text-property (erc-stamp--date-marker stamp)
-                                              'erc--msg)))))
+  (defvar erc-stamp--date-stamps)
+  (when (fboundp 'erc-stamp--date-marker)
+    (dolist (stamp erc-stamp--date-stamps)
+      (should (eq 'datestamp (get-text-property (erc-stamp--date-marker stamp)
+                                                'erc--msg))))))
 
 (defun erc-scenarios-common-assert-initial-buf-name (id port)
   ;; Assert no limbo period when explicit ID given
diff --git a/test/lisp/erc/resources/erc-tests-common.el b/test/lisp/erc/resources/erc-tests-common.el
index 525bc2ed868..28643e4834a 100644
--- a/test/lisp/erc/resources/erc-tests-common.el
+++ b/test/lisp/erc/resources/erc-tests-common.el
@@ -126,7 +126,7 @@ erc-tests-common-make-server-buf
   "Return a server buffer named NAME, creating it if necessary.
 Use NAME for the network and the session server as well."
   (with-current-buffer (if name
-                           (get-buffer-create name)
+                           (setq name (buffer-name (get-buffer-create name)))
                          (and (string-search "temp" (buffer-name))
                               (setq name "foonet")
                               (buffer-name)))
-- 
2.53.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0002-5.7-Preserve-order-of-local-ERC-modules-for-activati.patch

From d64e22eb47b161cb413e7ba8c7f3ca9f82d6ae9a Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sun, 21 Sep 2025 15:52:16 -0700
Subject: [PATCH 2/6] [5.7] Preserve order of local ERC modules for activation

* etc/ERC-NEWS: Add new section for ERC 5.7.
* lisp/erc/erc.el (erc--update-modules): Reverse returned list.
* test/lisp/erc/erc-tests.el (erc--update-modules/local): Update.
---
 etc/ERC-NEWS               | 14 ++++++++++++++
 lisp/erc/erc.el            |  2 +-
 test/lisp/erc/erc-tests.el |  2 +-
 3 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 082c10702f3..de27b8f07e7 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -11,6 +11,20 @@ This file is about changes in ERC, the powerful, modular, and
 extensible IRC (Internet Relay Chat) client distributed with
 GNU Emacs since Emacs version 22.1.
 
+
+* Changes in ERC 5.7
+
+** Changes in the library API.
+
+*** Local modules activate in preferred order instead of in reverse.
+In recent versions, ERC has enabled local modules in the reverse order
+of that produced by the "set" function used by 'setopt' and the Custom
+UI for the option 'erc-modules'.  Specifically, Built-in locals were
+activated in reverse lexicographic order after third-party ones, which
+were simply reversed as given.  Now, just like with global modules, ERC
+preserves the preferred order when activating local modules for new
+sessions.
+
 
 * Changes in ERC 5.6.2
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index ae7cf479451..7ad476e4913 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2450,7 +2450,7 @@ erc--find-mode
 
 (defun erc--update-modules (modules)
   (let (local-modes)
-    (dolist (module modules local-modes)
+    (dolist (module modules (nreverse local-modes))
       (if-let* ((mode (erc--find-mode module)))
           (if (custom-variable-p mode)
               (funcall mode 1)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index fb9be63de23..46e3f26a45d 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -3956,7 +3956,7 @@ erc--update-modules/local
 
        ;; Returns local modules.
        (should (equal (mapcar #'symbol-name (erc--update-modules erc-modules))
-                      '("erc-lo2-mode" "erc-lo1-mode")))
+                      '("erc-lo1-mode" "erc-lo2-mode")))
 
        ;; Requiring `erc-lo2' defines `erc-lo2-mode'.
        (should (equal (mapcar #'prin1-to-string (funcall get-calls))
-- 
2.53.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0003-5.7-Split-ERC-module-documentation-into-subnodes.patch

From 4947d8a5a68a5cf511a9fb3e95c818f70b6c4a06 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Wed, 12 Feb 2025 21:11:51 -0800
Subject: [PATCH 3/6] [5.7] Split ERC module documentation into subnodes

* doc/misc/erc.texi: Add "Modules" section to the main detailed menu.
(Modules): Promote "Local Modules" and "Module Loading" subheadings to
sections and proper nodes.  Rename "Local Modules" to "Module Scope" but
retain anchor for compatibility.
---
 doc/misc/erc.texi | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index ef4b85da485..b0d4b178127 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -76,6 +76,11 @@ Top
 * Sample Session::              Example of connecting to the @samp{#emacs} channel
 * Special Features::            Differences from standalone IRC clients
 
+Advanced Module Topics
+
+* Scope: Module Scope.          Differences between module types.
+* Loading: Module Loading.      How ERC loads modules.
+
 Advanced Usage
 
 * Connecting::                  Ways of connecting to an IRC server.
@@ -615,9 +620,13 @@ Modules
 At present, the only such module is @code{networks}, whose library ERC
 always loads anyway.
 
+@c Advanced module topics and individual module usage.
+
 @anchor{Local Modules}
-@subheading Local Modules
+@node Module Scope
+@section Scope
 @cindex local modules
+@cindex module scope
 
 @c Earlier language in code comments, commit messages, and tracker
 @c discussions used to describe a local module as being "active" in a
@@ -698,10 +707,8 @@ Modules
 unlike global toggles, none of these ever mutates @code{erc-modules}.
 
 
-@c FIXME add section to Advanced chapter for creating modules, and
-@c move this there.
-@anchor{Module Loading}
-@subheading Loading
+@node Module Loading
+@section Loading
 @cindex module loading
 
 ERC loads internal modules in alphabetical order and third-party
-- 
2.53.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0004-5.7-Make-prefix-body-boundary-more-detectable-in-ERC.patch
Content-Transfer-Encoding: quoted-printable

From 1106cc427977411ae0b25f9832d9e900d4d1783b Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 23 Apr 2026 01:09:45 -0700
Subject: [PATCH 4/6] [5.7] Make prefix-body boundary more detectable in ERC

* lisp/erc/erc-fill.el (erc-fill-wrap): Use new `erc--pfx' msg-prop
interface.
* lisp/erc/erc-goodies.el (erc--command-indicator-body-find): New
function.
(erc--command-indicator-display): Add `erc--pfx' function-valued msg
prop for moving past command-indicator prompt to start of body.  Stash
inserted prompt string in oneoff `erc--command-indicator' prop.
* lisp/erc/erc.el (erc--ensure-spkr-prop): Add `erc--pfx' msg prop for
finding the start of the message body after a speaker tag with the
`erc--speaker' text property.
(erc-display-message-highlight): Add `erc--pfx' functions made for known
variants of the `erc-display-message' TYPE parameter, such as `notice'
and `error'.
(erc--ctcp-action-speaker-in-prefix-p): New variable.
(erc--pfx-skip-word-fwd, erc--pfx-skip-spkr-fwd)
(erc--pfx-skip-notice-fwd, erc--pfx-skip-template-fwd): New
functions.
* test/lisp/erc/erc-tests.el (erc--pfx-skip-notice-fwd)
(erc--pfx-skip-template-fwd): New tests.  (Bug#73798)

;; test/lisp/erc/resources/fill/snapshots/merge-01-start.eld:
;; test/lisp/erc/resources/fill/snapshots/merge-02-right.eld:
;; test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld:
;; test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld:
;; test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld:
;; test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld:
;; test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld:
;; test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld:
;; test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld:
;; test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld: Update.
---
 lisp/erc/erc-fill.el                          | 20 ++++---
 lisp/erc/erc-goodies.el                       |  6 ++
 lisp/erc/erc.el                               | 42 +++++++++++++-
 test/lisp/erc/erc-tests.el                    | 57 +++++++++++++++++++
 .../fill/snapshots/merge-01-start.eld         |  2 +-
 .../fill/snapshots/merge-02-right.eld         |  2 +-
 .../fill/snapshots/merge-wrap-01.eld          |  2 +-
 .../snapshots/merge-wrap-indicator-pre-01.eld |  2 +-
 .../fill/snapshots/monospace-01-start.eld     |  2 +-
 .../fill/snapshots/monospace-02-right.eld     |  2 +-
 .../fill/snapshots/monospace-03-left.eld      |  2 +-
 .../fill/snapshots/monospace-04-reset.eld     |  2 +-
 .../fill/snapshots/spacing-01-mono.eld        |  2 +-
 .../fill/snapshots/stamps-left-01.eld         |  2 +-
 14 files changed, 125 insertions(+), 20 deletions(-)

diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index 7ca29f16e03..db7df2d50ae 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -706,15 +706,17 @@ erc-fill-wrap
     (let ((len (or (and erc-fill--wrap-length-function
                         (funcall erc-fill--wrap-length-function))
                    (and-let* ((msg-prop (erc--check-msg-prop 'erc--msg))
-                              ((not (eq msg-prop 'unknown))))
-                     (when-let* ((e (erc--get-speaker-bounds))
-                                 (b (pop e))
-                                 ((or erc-fill--wrap-action-dedent-p
-                                      (not (erc--check-msg-prop 'erc--ctcp
-                                                                'ACTION)))=
))
-                       (goto-char e))
-                     (skip-syntax-forward "^-")
-                     (forward-char)
+                              (_ (not (eq msg-prop 'unknown))))
+                     (if-let* ((fn (erc--check-msg-prop 'erc--pfx)))
+                         (let ((erc--ctcp-action-speaker-in-prefix-p
+                                erc-fill--wrap-action-dedent-p))
+                           (funcall fn))
+                       ;; FIXME remove this before merging.
+                       (unless (eq msg-prop 'datestamp)
+                         (erc--lwarn 'erc-fill :error
+                                     "Missing `erc--pfx' skip-fwd function=
: %S"
+                                     (list :msg-prop msg-prop
+                                           :buffer-string (buffer-string))=
)))
                      (cond ((eq msg-prop 'datestamp)
                             (when erc-fill--wrap-rejigger-last-message
                               (set-marker erc-fill--wrap-last-msg (point-m=
in)))
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index f1f92923ea2..cd6096f5133 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -631,6 +631,9 @@ erc--command-indicator-permit-insertion
           (erc--input-split-substxt state) #'erc--command-indicator-displa=
y)
     (erc-send-distinguish-noncommands state)))
=20
+(defun erc--command-indicator-body-find ()
+  (search-forward (erc--check-msg-prop 'erc--command-indicator) (pos-eol) =
t))
+
 ;; This function used to be called `erc-display-command'.  It was
 ;; neutered in ERC 5.3.x (Emacs 24.5), commented out in 5.4, removed
 ;; in 5.5, and restored in 5.6.
@@ -649,6 +652,9 @@ erc--command-indicator-display
                                             'hash-table)))))
         (when-let* ((string (erc-command-indicator))
                     (erc-input-marker (copy-marker erc-input-marker)))
+          (puthash 'erc--command-indicator string erc--msg-props)
+          (puthash 'erc--pfx #'erc--command-indicator-body-find
+                   erc--msg-props)
           (erc-display-prompt nil nil string 'erc-command-indicator-face)
           (remove-text-properties insert-position (point)
                                   '(field nil erc-prompt nil))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 7ad476e4913..42fb41610c1 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -187,6 +187,9 @@ erc--msg-props
  - `erc--skip': list of symbols known to modules that indicate an
     intent to skip or simplify module-specific processing
=20
+ - `erc--pfx': function taking no args that advances point to the
+    start of the semantic body
+
  - `erc--ephemeral': a symbol prefixed by or matching a module
     name; indicates to other modules and members of modification
     hooks that the current message should not affect stateful
@@ -3237,11 +3240,13 @@ erc--ensure-spkr-prop
 and return it."
   (cond (erc--msg-props
          (puthash 'erc--spkr nick erc--msg-props)
+         (puthash 'erc--pfx #'erc--pfx-skip-spkr-fwd erc--msg-props)
          (dolist (entry overrides)
            (puthash (car entry) (cdr entry) erc--msg-props)))
         (erc--msg-prop-overrides
          (setq erc--msg-prop-overrides
-               `((erc--spkr . ,nick) ,@overrides ,@erc--msg-prop-overrides=
))))
+               `((erc--spkr . ,nick) (erc--pfx . ,#'erc--pfx-skip-spkr-fwd)
+                 ,@overrides ,@erc--msg-prop-overrides))))
   nick)
=20
 (defun erc-string-invisible-p (string)
@@ -3836,8 +3841,14 @@ erc-display-message-highlight
=20
 See also `erc-make-notice'."
   (cond ((eq type 'notice)
+         (when erc--msg-props
+           (puthash 'erc--pfx #'erc--pfx-skip-notice-fwd erc--msg-props))
          (erc-make-notice string))
         (t
+         (when (and erc--msg-props ; prefer notice if combined
+                    (not (erc--check-msg-prop 'erc--pfx
+                                              #'erc--pfx-skip-notice-fwd)))
+           (puthash 'erc--pfx #'erc--pfx-skip-template-fwd erc--msg-props))
          (erc-put-text-property
           0 (length string)
           'font-lock-face
@@ -6251,6 +6262,35 @@ erc--get-speaker-bounds
                                          'erc--speaker nil)))
     (cons beg (next-single-property-change beg 'erc--speaker))))
=20
+(defvar erc--ctcp-action-speaker-in-prefix-p nil
+  ;; This variable replaces `erc-fill--wrap-action-dedent-p' in ERC 5.6.
+  "Whether the speaker of a /ME is part of the prefix rather than the body=
.")
+
+;; Insertion hook members defer to the function value of the `erc--pfx'
+;; msg prop to find the "body" portion of a message after a prefix, such
+;; as a "<speaker> " tag, assuming the `erc--msg' prop isn't `unknown'.
+(defun erc--pfx-skip-word-fwd ()
+  "Go to a space following an initial run of non-space chars."
+  (unless (and (bobp) (eq ?\s (char-after (point))))
+    (search-forward " " (pos-eol) t)))
+
+(defun erc--pfx-skip-spkr-fwd ()
+  "Move point after the first space following the speaker tag.
+That's one char beyond the last with a `erc--speaker' text property."
+  (let ((bounds (erc--get-speaker-bounds)))
+    (when (or erc--ctcp-action-speaker-in-prefix-p
+              (not (erc--check-msg-prop 'erc--ctcp 'ACTION)))
+      (goto-char (cdr bounds)))
+    (search-forward " " (pos-eol) t)))
+
+(defun erc--pfx-skip-notice-fwd ()
+  "Move point past `erc-notice-prefix'."
+  (search-forward erc-notice-prefix (+ (point) (length erc-notice-prefix))=
 t))
+
+(defun erc--pfx-skip-template-fwd ()
+  "Skip common prefixes from the English format-template catalog."
+  (search-forward-regexp (rx bol (| "\n\n*** " "=3D=3D> ")) (+ (point) 6) =
t))
+
 (defvar erc--cmem-from-nick-function #'erc--cmem-get-existing
   "Function maybe returning a \"channel member\" cons from a nick.
 Must return either nil or a cons of an `erc-server-user' and an
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 46e3f26a45d..515a0716549 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -3030,6 +3030,63 @@ erc--determine-speaker-message-format-args/queries
=20
     (when noninteractive (kill-buffer))))
=20
+(ert-deftest erc--pfx-skip-notice-fwd ()
+  (erc-tests-common-make-server-buf)
+  ;; Inhibit login I/O.
+  (setq erc-logged-in t)
+
+  ;; Moves past leading `erc-notice-prefix'.
+  (let* ((calledp nil)
+         (erc-insert-modify-hook
+          (lambda ()
+            (goto-char (point-min))
+            (erc--pfx-skip-notice-fwd)
+            (should (looking-at (rx "One!" eol)))
+            (setq calledp t))))
+    (erc-tests-common-simulate-line ":irc.foonet.net 375 tester :One!")
+    (should calledp))
+
+  ;; Respects customized option.
+  (let* ((erc-notice-prefix "~~> ")
+         (calledp nil)
+         (erc-insert-modify-hook
+          (lambda ()
+            (goto-char (point-min))
+            (erc--pfx-skip-notice-fwd)
+            (should (looking-at (rx "Two!" eol)))
+            (should (looking-back (rx bol "~~> ")))
+            (setq calledp t))))
+    (erc-tests-common-simulate-line ":irc.foonet.net 372 tester :Two!")
+    (should calledp)))
+
+(ert-deftest erc--pfx-skip-template-fwd ()
+  (erc-tests-common-make-server-buf)
+
+  ;; Skips over two leading newlines.
+  (let* ((calledp nil)
+         (erc-insert-modify-hook
+          (lambda ()
+            (goto-char (point-min))
+            (should (looking-at (rx eol bol)))
+            (erc--pfx-skip-template-fwd)
+            (should (equal (buffer-substring (point) (pos-eol))
+                           "ERC finished ***"))
+            (setq calledp t))))
+    (erc-display-message nil 'error (current-buffer) 'finished)
+    (should calledp))
+
+  ;; Does the same for arbitrary non-whitespace prefix.
+  (let* ((calledp nil)
+         (erc-insert-modify-hook
+          (lambda ()
+            (goto-char (point-min))
+            (erc--pfx-skip-template-fwd)
+            (should (looking-at (rx "ERROR from : Quit 123" eol)))
+            (should (looking-back (rx bol "=3D=3D> ")))
+            (setq calledp t))))
+    (erc-tests-common-simulate-line "ERROR :Quit 123")
+    (should calledp)))
+
 (defun erc-tests--format-my-nick (message)
   (concat (erc-format-my-nick)
           (propertize message 'font-lock-face 'erc-input-face)))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld b/te=
st/lisp/erc/resources/fill/snapshots/merge-01-start.eld
index 166ed59e292..07dd68f19ec 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<alice> one.\n<alice> two.\n<bob> t=
hree.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp =
erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(=
space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg no=
tice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) =
22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-=
prefix #1# line-prefix #2#) 184 191 (field erc-timestamp wrap-prefix #1# li=
ne-prefix #2# display (#6=3D(margin right-margin) #("[00:00]" 0 7 (invisibl=
e timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd =
PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198=
 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#=
) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-p=
refix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg =
erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=
=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix #4#) 354=
 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefix #1# line-prefix=
 #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg datestamp=
 erc--ts 1680307200 field erc-timestamp) 438 455 (field erc-timestamp wrap-=
prefix #1# line-prefix (space :width (- 27 (18)))) 456 457 (erc--msg msg er=
c--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-pref=
ix #5=3D(space :width (- 27 (6)))) 457 460 (wrap-prefix #1# line-prefix #5#=
) 460 467 (wrap-prefix #1# line-prefix #5#) 467 468 (field erc-timestamp wr=
ap-prefix #1# line-prefix #5#) 468 475 (field erc-timestamp wrap-prefix #1#=
 line-prefix #5# display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 476 =
477 (erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG wra=
p-prefix #1# line-prefix #8=3D(space :width (- 27 (8)))) 477 482 (wrap-pref=
ix #1# line-prefix #8#) 482 488 (wrap-prefix #1# line-prefix #8#) 489 490 (=
erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG wrap-pre=
fix #1# line-prefix #9=3D(space :width (- 27 0)) erc-fill--wrap-merge #7=3D=
"" display #7#) 490 495 (wrap-prefix #1# line-prefix #9# erc-fill--wrap-mer=
ge #7# display #7#) 495 497 (wrap-prefix #1# line-prefix #9# erc-fill--wrap=
-merge #7# display #7#) 497 501 (wrap-prefix #1# line-prefix #9#) 502 503 (=
erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefi=
x #1# line-prefix #10=3D(space :width (- 27 (6)))) 503 506 (wrap-prefix #1#=
 line-prefix #10#) 506 514 (wrap-prefix #1# line-prefix #10#) 515 516 (erc-=
-msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1=
# line-prefix #11=3D(space :width (- 27 0)) erc-fill--wrap-merge #7# displa=
y #7#) 516 519 (wrap-prefix #1# line-prefix #11# erc-fill--wrap-merge #7# d=
isplay #7#) 519 521 (wrap-prefix #1# line-prefix #11# erc-fill--wrap-merge =
#7# display #7#) 521 526 (wrap-prefix #1# line-prefix #11#) 527 528 (erc--m=
sg msg erc--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1=
# line-prefix #12=3D(space :width (- 27 (8)))) 528 533 (wrap-prefix #1# lin=
e-prefix #12#) 533 540 (wrap-prefix #1# line-prefix #12#) 541 542 (erc--msg=
 msg erc--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# =
line-prefix #13=3D(space :width (- 27 0)) erc-fill--wrap-merge #7# display =
#7#) 542 547 (wrap-prefix #1# line-prefix #13# erc-fill--wrap-merge #7# dis=
play #7#) 547 549 (wrap-prefix #1# line-prefix #13# erc-fill--wrap-merge #7=
# display #7#) 549 553 (wrap-prefix #1# line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<alice> one.\n<alice> two.\n<bob> t=
hree.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp =
erc--ts 0 field erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(=
space :width 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg no=
tice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wrap-prefix #1# line-prefi=
x #2=3D(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) =
183 184 (field erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (fiel=
d erc-timestamp wrap-prefix #1# line-prefix #2# display (#6=3D(margin right=
-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc=
--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd w=
rap-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-pr=
efix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203=
 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#=
) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr=
 "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-pref=
ix #1# line-prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1#=
 line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-p=
refix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 43=
8 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 438 455 (fiel=
d erc-timestamp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 456=
 457 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc-=
-pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #5=3D(space :width =
(- 27 (6)))) 457 460 (wrap-prefix #1# line-prefix #5#) 460 467 (wrap-prefix=
 #1# line-prefix #5#) 467 468 (field erc-timestamp wrap-prefix #1# line-pre=
fix #5#) 468 475 (field erc-timestamp wrap-prefix #1# line-prefix #5# displ=
ay (#6# #("[07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg msg erc-=
-spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-sp=
kr-fwd wrap-prefix #1# line-prefix #8=3D(space :width (- 27 (8)))) 477 482 =
(wrap-prefix #1# line-prefix #8#) 482 488 (wrap-prefix #1# line-prefix #8#)=
 489 490 (erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMS=
G erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #9=3D(space :=
width (- 27 0)) erc-fill--wrap-merge #7=3D"" display #7#) 490 495 (wrap-pre=
fix #1# line-prefix #9# erc-fill--wrap-merge #7# display #7#) 495 497 (wrap=
-prefix #1# line-prefix #9# erc-fill--wrap-merge #7# display #7#) 497 501 (=
wrap-prefix #1# line-prefix #9#) 502 503 (erc--msg msg erc--spkr "bob" erc-=
-ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix=
 #1# line-prefix #10=3D(space :width (- 27 (6)))) 503 506 (wrap-prefix #1# =
line-prefix #10#) 506 514 (wrap-prefix #1# line-prefix #10#) 515 516 (erc--=
msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--p=
fx-skip-spkr-fwd wrap-prefix #1# line-prefix #11=3D(space :width (- 27 0)) =
erc-fill--wrap-merge #7# display #7#) 516 519 (wrap-prefix #1# line-prefix =
#11# erc-fill--wrap-merge #7# display #7#) 519 521 (wrap-prefix #1# line-pr=
efix #11# erc-fill--wrap-merge #7# display #7#) 521 526 (wrap-prefix #1# li=
ne-prefix #11#) 527 528 (erc--msg msg erc--spkr "Dummy" erc--ts 1680332400 =
erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefi=
x #12=3D(space :width (- 27 (8)))) 528 533 (wrap-prefix #1# line-prefix #12=
#) 533 540 (wrap-prefix #1# line-prefix #12#) 541 542 (erc--msg msg erc--sp=
kr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-=
fwd wrap-prefix #1# line-prefix #13=3D(space :width (- 27 0)) erc-fill--wra=
p-merge #7# display #7#) 542 547 (wrap-prefix #1# line-prefix #13# erc-fill=
--wrap-merge #7# display #7#) 547 549 (wrap-prefix #1# line-prefix #13# erc=
-fill--wrap-merge #7# display #7#) 549 553 (wrap-prefix #1# line-prefix #13=
#))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld b/te=
st/lisp/erc/resources/fill/snapshots/merge-02-right.eld
index 8b502373807..0bb25d8cd9f 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<alice> one.\n<alice> two.\n<bob> t=
hree.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp =
erc--ts 0 field erc-timestamp) 3 20 (wrap-prefix #1=3D(space :width 29) lin=
e-prefix (space :width (- 29 (18))) field erc-timestamp) 21 22 (wrap-prefix=
 #1# line-prefix #2=3D(space :width (- 29 (4))) erc--msg notice erc--ts 0) =
22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (wrap-prefix #1# line-pref=
ix #2# field erc-timestamp) 184 191 (wrap-prefix #1# line-prefix #2# field =
erc-timestamp display (#6=3D(margin right-margin) #("[00:00]" 0 7 (invisibl=
e timestamp)))) 192 193 (wrap-prefix #1# line-prefix #3=3D(space :width (- =
29 (8))) erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG) 193 198=
 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#=
) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-p=
refix #3#) 317 349 (wrap-prefix #1# line-prefix (space :width (- 29 (8)))) =
350 351 (wrap-prefix #1# line-prefix #4=3D(space :width (- 29 (6))) erc--ms=
g msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG) 351 354 (wrap-prefix #1# =
line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-pr=
efix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 438=
 (erc--msg datestamp erc--ts 1680307200 field erc-timestamp) 438 455 (wrap-=
prefix #1# line-prefix (space :width (- 29 (18))) field erc-timestamp) 456 =
457 (wrap-prefix #1# line-prefix #5=3D(space :width (- 29 (6))) erc--msg ms=
g erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG) 457 460 (wrap-prefix=
 #1# line-prefix #5#) 460 467 (wrap-prefix #1# line-prefix #5#) 467 468 (wr=
ap-prefix #1# line-prefix #5# field erc-timestamp) 468 475 (wrap-prefix #1#=
 line-prefix #5# field erc-timestamp display (#6# #("[07:00]" 0 7 (invisibl=
e timestamp)))) 476 477 (wrap-prefix #1# line-prefix #8=3D(space :width (- =
29 (8))) erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG=
) 477 482 (wrap-prefix #1# line-prefix #8#) 482 488 (wrap-prefix #1# line-p=
refix #8#) 489 490 (wrap-prefix #1# line-prefix #9=3D(space :width (- 29 0)=
) erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG erc-fi=
ll--wrap-merge #7=3D"" display #7#) 490 495 (wrap-prefix #1# line-prefix #9=
# erc-fill--wrap-merge #7# display #7#) 495 497 (wrap-prefix #1# line-prefi=
x #9# erc-fill--wrap-merge #7# display #7#) 497 501 (wrap-prefix #1# line-p=
refix #9#) 502 503 (wrap-prefix #1# line-prefix #10=3D(space :width (- 29 (=
6))) erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG) 503 =
506 (wrap-prefix #1# line-prefix #10#) 506 514 (wrap-prefix #1# line-prefix=
 #10#) 515 516 (wrap-prefix #1# line-prefix #11=3D(space :width (- 29 0)) e=
rc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc-fill--w=
rap-merge #7# display #7#) 516 519 (wrap-prefix #1# line-prefix #11# erc-fi=
ll--wrap-merge #7# display #7#) 519 521 (wrap-prefix #1# line-prefix #11# e=
rc-fill--wrap-merge #7# display #7#) 521 526 (wrap-prefix #1# line-prefix #=
11#) 527 528 (wrap-prefix #1# line-prefix #12=3D(space :width (- 29 (8))) e=
rc--msg msg erc--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG) 528 533 =
(wrap-prefix #1# line-prefix #12#) 533 540 (wrap-prefix #1# line-prefix #12=
#) 541 542 (wrap-prefix #1# line-prefix #13=3D(space :width (- 29 0)) erc--=
msg msg erc--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG erc-fill--wra=
p-merge #7# display #7#) 542 547 (wrap-prefix #1# line-prefix #13# erc-fill=
--wrap-merge #7# display #7#) 547 549 (wrap-prefix #1# line-prefix #13# erc=
-fill--wrap-merge #7# display #7#) 549 553 (wrap-prefix #1# line-prefix #13=
#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<alice> one.\n<alice> two.\n<bob> t=
hree.\n<bob> four.\n<Dummy> five.\n<Dummy> six.\n" 2 3 (erc--msg datestamp =
erc--ts 0 field erc-timestamp) 3 20 (wrap-prefix #1=3D(space :width 29) lin=
e-prefix (space :width (- 29 (18))) field erc-timestamp) 21 22 (wrap-prefix=
 #1# line-prefix #2=3D(space :width (- 29 (4))) erc--msg notice erc--ts 0 e=
rc--pfx erc--pfx-skip-notice-fwd) 22 183 (wrap-prefix #1# line-prefix #2#) =
183 184 (wrap-prefix #1# line-prefix #2# field erc-timestamp) 184 191 (wrap=
-prefix #1# line-prefix #2# field erc-timestamp display (#6=3D(margin right=
-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 193 (wrap-prefix #1# =
line-prefix #3=3D(space :width (- 29 (8))) erc--msg msg erc--spkr "alice" e=
rc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd) 193 198 (wrap-pr=
efix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203=
 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#=
) 317 349 (wrap-prefix #1# line-prefix (space :width (- 29 (8)))) 350 351 (=
wrap-prefix #1# line-prefix #4=3D(space :width (- 29 (6))) erc--msg msg erc=
--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd) 35=
1 354 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefi=
x #4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# l=
ine-prefix #4#) 437 438 (erc--msg datestamp erc--ts 1680307200 field erc-ti=
mestamp) 438 455 (wrap-prefix #1# line-prefix (space :width (- 29 (18))) fi=
eld erc-timestamp) 456 457 (wrap-prefix #1# line-prefix #5=3D(space :width =
(- 29 (6))) erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMS=
G erc--pfx erc--pfx-skip-spkr-fwd) 457 460 (wrap-prefix #1# line-prefix #5#=
) 460 467 (wrap-prefix #1# line-prefix #5#) 467 468 (wrap-prefix #1# line-p=
refix #5# field erc-timestamp) 468 475 (wrap-prefix #1# line-prefix #5# fie=
ld erc-timestamp display (#6# #("[07:00]" 0 7 (invisible timestamp)))) 476 =
477 (wrap-prefix #1# line-prefix #8=3D(space :width (- 29 (8))) erc--msg ms=
g erc--spkr "alice" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-s=
kip-spkr-fwd) 477 482 (wrap-prefix #1# line-prefix #8#) 482 488 (wrap-prefi=
x #1# line-prefix #8#) 489 490 (wrap-prefix #1# line-prefix #9=3D(space :wi=
dth (- 29 0)) erc--msg msg erc--spkr "alice" erc--ts 1680332400 erc--cmd PR=
IVMSG erc-fill--wrap-merge #7=3D"" erc--pfx erc--pfx-skip-spkr-fwd display =
#7#) 490 495 (wrap-prefix #1# line-prefix #9# erc-fill--wrap-merge #7# disp=
lay #7#) 495 497 (wrap-prefix #1# line-prefix #9# erc-fill--wrap-merge #7# =
display #7#) 497 501 (wrap-prefix #1# line-prefix #9#) 502 503 (wrap-prefix=
 #1# line-prefix #10=3D(space :width (- 29 (6))) erc--msg msg erc--spkr "bo=
b" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd) 503=
 506 (wrap-prefix #1# line-prefix #10#) 506 514 (wrap-prefix #1# line-prefi=
x #10#) 515 516 (wrap-prefix #1# line-prefix #11=3D(space :width (- 29 0)) =
erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc-fill--=
wrap-merge #7# erc--pfx erc--pfx-skip-spkr-fwd display #7#) 516 519 (wrap-p=
refix #1# line-prefix #11# erc-fill--wrap-merge #7# display #7#) 519 521 (w=
rap-prefix #1# line-prefix #11# erc-fill--wrap-merge #7# display #7#) 521 5=
26 (wrap-prefix #1# line-prefix #11#) 527 528 (wrap-prefix #1# line-prefix =
#12=3D(space :width (- 29 (8))) erc--msg msg erc--spkr "Dummy" erc--ts 1680=
332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd) 528 533 (wrap-pref=
ix #1# line-prefix #12#) 533 540 (wrap-prefix #1# line-prefix #12#) 541 542=
 (wrap-prefix #1# line-prefix #13=3D(space :width (- 29 0)) erc--msg msg er=
c--spkr "Dummy" erc--ts 1680332400 erc--cmd PRIVMSG erc-fill--wrap-merge #7=
# erc--pfx erc--pfx-skip-spkr-fwd display #7#) 542 547 (wrap-prefix #1# lin=
e-prefix #13# erc-fill--wrap-merge #7# display #7#) 547 549 (wrap-prefix #1=
# line-prefix #13# erc-fill--wrap-merge #7# display #7#) 549 553 (wrap-pref=
ix #1# line-prefix #13#))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld b/tes=
t/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
index 9744e659813..8d2959e9ee8 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n=
<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 fi=
eld erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(space :width=
 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts=
 0 wrap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap=
-prefix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# l=
ine-prefix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2=
# display (#5=3D(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)=
))) 192 193 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap=
-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefi=
x #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (w=
rap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 3=
17 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr "b=
ob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=3D(space :wid=
th (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-pre=
fix #1# line-prefix #4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 =
(wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg datestamp erc--ts 16803=
07200 field erc-timestamp) 438 455 (field erc-timestamp wrap-prefix #1# lin=
e-prefix (space :width (- 27 (18)))) 456 457 (erc--msg msg erc--spkr "bob" =
erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=3D(space=
 :width (- 27 (6)))) 457 460 (wrap-prefix #1# line-prefix #6#) 460 467 (wra=
p-prefix #1# line-prefix #6#) 467 468 (field erc-timestamp wrap-prefix #1# =
line-prefix #6#) 468 475 (field erc-timestamp wrap-prefix #1# line-prefix #=
6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg =
msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# lin=
e-prefix #7=3D(space :width (- 27 0)) erc-fill--wrap-merge #8=3D"" display =
#8#) 477 480 (wrap-prefix #1# line-prefix #7# erc-fill--wrap-merge #8# disp=
lay #8#) 480 482 (wrap-prefix #1# line-prefix #7# erc-fill--wrap-merge #8# =
display #8#) 482 485 (wrap-prefix #1# line-prefix #7#) 486 487 (erc--msg ct=
cp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACT=
ION wrap-prefix #1# line-prefix #9=3D(space :width (- 27 (6)))) 487 488 (wr=
ap-prefix #1# line-prefix #9#) 488 491 (wrap-prefix #1# line-prefix #9#) 49=
1 496 (wrap-prefix #1# line-prefix #9#) 497 498 (erc--msg msg erc--spkr "bo=
b" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #10=3D(s=
pace :width (- 27 (6)))) 498 501 (wrap-prefix #1# line-prefix #10#) 501 507=
 (wrap-prefix #1# line-prefix #10#) 508 509 (erc--msg msg erc--spkr "bob" e=
rc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #11=3D(space=
 :width (- 27 0)) erc-fill--wrap-merge #8# display #8#) 509 512 (wrap-prefi=
x #1# line-prefix #11# erc-fill--wrap-merge #8# display #8#) 512 514 (wrap-=
prefix #1# line-prefix #11# erc-fill--wrap-merge #8# display #8#) 514 517 (=
wrap-prefix #1# line-prefix #11#) 518 519 (erc--msg ctcp-action erc--spkr "=
bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# l=
ine-prefix #12=3D(space :width (- 27 (2)))) 519 520 (wrap-prefix #1# line-p=
refix #12#) 520 523 (wrap-prefix #1# line-prefix #12#) 523 529 (wrap-prefix=
 #1# line-prefix #12#) 530 531 (erc--msg msg erc--spkr "bob" erc--ts 168033=
2400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #13=3D(space :width (- 27=
 (6)))) 531 534 (wrap-prefix #1# line-prefix #13#) 534 541 (wrap-prefix #1#=
 line-prefix #13#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n=
<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 fi=
eld erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(space :width=
 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts=
 0 erc--pfx erc--pfx-skip-notice-fwd wrap-prefix #1# line-prefix #2=3D(spac=
e :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (fie=
ld erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-timest=
amp wrap-prefix #1# line-prefix #2# display (#5=3D(margin right-margin) #("=
[00:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alic=
e" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #=
1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefix #1# lin=
e-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (wrap-prefi=
x #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 317 349 (w=
rap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr "bob" erc--=
ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-=
prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix=
 #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefix #1# li=
ne-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg =
datestamp erc--ts 1680307200 field erc-timestamp) 438 455 (field erc-timest=
amp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 456 457 (erc--m=
sg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pf=
x-skip-spkr-fwd wrap-prefix #1# line-prefix #6=3D(space :width (- 27 (6))))=
 457 460 (wrap-prefix #1# line-prefix #6#) 460 467 (wrap-prefix #1# line-pr=
efix #6#) 467 468 (field erc-timestamp wrap-prefix #1# line-prefix #6#) 468=
 475 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[=
07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg msg erc--spkr "bob" =
erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-pr=
efix #1# line-prefix #7=3D(space :width (- 27 0)) erc-fill--wrap-merge #8=
=3D"" display #8#) 477 480 (wrap-prefix #1# line-prefix #7# erc-fill--wrap-=
merge #8# display #8#) 480 482 (wrap-prefix #1# line-prefix #7# erc-fill--w=
rap-merge #8# display #8#) 482 485 (wrap-prefix #1# line-prefix #7#) 486 48=
7 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG=
 erc--ctcp ACTION erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-pref=
ix #9=3D(space :width (- 27 (6)))) 487 488 (wrap-prefix #1# line-prefix #9#=
) 488 491 (wrap-prefix #1# line-prefix #9#) 491 496 (wrap-prefix #1# line-p=
refix #9#) 497 498 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cm=
d PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #10=
=3D(space :width (- 27 (6)))) 498 501 (wrap-prefix #1# line-prefix #10#) 50=
1 507 (wrap-prefix #1# line-prefix #10#) 508 509 (erc--msg msg erc--spkr "b=
ob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wra=
p-prefix #1# line-prefix #11=3D(space :width (- 27 0)) erc-fill--wrap-merge=
 #8# display #8#) 509 512 (wrap-prefix #1# line-prefix #11# erc-fill--wrap-=
merge #8# display #8#) 512 514 (wrap-prefix #1# line-prefix #11# erc-fill--=
wrap-merge #8# display #8#) 514 517 (wrap-prefix #1# line-prefix #11#) 518 =
519 (erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVM=
SG erc--ctcp ACTION erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-pr=
efix #12=3D(space :width (- 27 (2)))) 519 520 (wrap-prefix #1# line-prefix =
#12#) 520 523 (wrap-prefix #1# line-prefix #12#) 523 529 (wrap-prefix #1# l=
ine-prefix #12#) 530 531 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 e=
rc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix=
 #13=3D(space :width (- 27 (6)))) 531 534 (wrap-prefix #1# line-prefix #13#=
) 534 541 (wrap-prefix #1# line-prefix #13#))
diff --git a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pr=
e-01.eld b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-=
01.eld
index 36729b890be..c3829c59f36 100644
--- a/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/merge-wrap-indicator-pre-01.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n=
<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 fi=
eld erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(space :width=
 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts=
 0 wrap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap=
-prefix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# l=
ine-prefix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2=
# display (#5=3D(margin right-margin) #("[00:00]" 0 7 (invisible timestamp)=
))) 192 193 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wrap=
-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefi=
x #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (w=
rap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 3=
17 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr "b=
ob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #4=3D(space :wid=
th (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-pre=
fix #1# line-prefix #4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 =
(wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg datestamp erc--ts 16803=
07200 field erc-timestamp) 438 455 (field erc-timestamp wrap-prefix #1# lin=
e-prefix (space :width (- 27 (18)))) 456 457 (erc--msg msg erc--spkr "bob" =
erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #6=3D(space=
 :width (- 27 (6)))) 457 460 (wrap-prefix #1# line-prefix #6#) 460 467 (wra=
p-prefix #1# line-prefix #6#) 467 468 (field erc-timestamp wrap-prefix #1# =
line-prefix #6#) 468 475 (field erc-timestamp wrap-prefix #1# line-prefix #=
6# display (#5# #("[07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg =
msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# lin=
e-prefix #7=3D(space :width (- 27 #10=3D(2))) erc-fill--wrap-merge t displa=
y #8=3D#("> " 0 1 (font-lock-face shadow))) 477 480 (wrap-prefix #1# line-p=
refix #7# erc-fill--wrap-merge t display #8#) 480 482 (wrap-prefix #1# line=
-prefix #7# erc-fill--wrap-merge t display #8#) 482 485 (wrap-prefix #1# li=
ne-prefix #7#) 486 487 (erc--msg ctcp-action erc--spkr "bob" erc--ts 168033=
2400 erc--cmd PRIVMSG erc--ctcp ACTION wrap-prefix #1# line-prefix #9=3D(sp=
ace :width (- 27 (6)))) 487 488 (wrap-prefix #1# line-prefix #9#) 488 491 (=
wrap-prefix #1# line-prefix #9#) 491 496 (wrap-prefix #1# line-prefix #9#) =
497 498 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG w=
rap-prefix #1# line-prefix #11=3D(space :width (- 27 (6)))) 498 501 (wrap-p=
refix #1# line-prefix #11#) 501 507 (wrap-prefix #1# line-prefix #11#) 508 =
509 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-=
prefix #1# line-prefix #12=3D(space :width (- 27 #10#)) erc-fill--wrap-merg=
e t display #8#) 509 512 (wrap-prefix #1# line-prefix #12# erc-fill--wrap-m=
erge t display #8#) 512 514 (wrap-prefix #1# line-prefix #12# erc-fill--wra=
p-merge t display #8#) 514 517 (wrap-prefix #1# line-prefix #12#) 518 519 (=
erc--msg ctcp-action erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG er=
c--ctcp ACTION wrap-prefix #1# line-prefix #13=3D(space :width (- 27 (2))))=
 519 520 (wrap-prefix #1# line-prefix #13#) 520 523 (wrap-prefix #1# line-p=
refix #13#) 523 529 (wrap-prefix #1# line-prefix #13#) 530 531 (erc--msg ms=
g erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG wrap-prefix #1# line-=
prefix #14=3D(space :width (- 27 (6)))) 531 534 (wrap-prefix #1# line-prefi=
x #14#) 534 541 (wrap-prefix #1# line-prefix #14#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n\n=
[Sat Apr  1 2023]\n<bob> zero. [07:00]\n<bob> 0.5\n* bob one.\n<bob> two.\n=
<bob> 2.5\n* bob three\n<bob> four.\n" 2 3 (erc--msg datestamp erc--ts 0 fi=
eld erc-timestamp) 3 20 (field erc-timestamp wrap-prefix #1=3D(space :width=
 27) line-prefix (space :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts=
 0 erc--pfx erc--pfx-skip-notice-fwd wrap-prefix #1# line-prefix #2=3D(spac=
e :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (fie=
ld erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-timest=
amp wrap-prefix #1# line-prefix #2# display (#5=3D(margin right-margin) #("=
[00:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alic=
e" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #=
1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefix #1# lin=
e-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (wrap-prefi=
x #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 317 349 (w=
rap-prefix #1# line-prefix #3#) 350 351 (erc--msg msg erc--spkr "bob" erc--=
ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-=
prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# line-prefix=
 #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefix #1# li=
ne-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 437 438 (erc--msg =
datestamp erc--ts 1680307200 field erc-timestamp) 438 455 (field erc-timest=
amp wrap-prefix #1# line-prefix (space :width (- 27 (18)))) 456 457 (erc--m=
sg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pf=
x-skip-spkr-fwd wrap-prefix #1# line-prefix #6=3D(space :width (- 27 (6))))=
 457 460 (wrap-prefix #1# line-prefix #6#) 460 467 (wrap-prefix #1# line-pr=
efix #6#) 467 468 (field erc-timestamp wrap-prefix #1# line-prefix #6#) 468=
 475 (field erc-timestamp wrap-prefix #1# line-prefix #6# display (#5# #("[=
07:00]" 0 7 (invisible timestamp)))) 476 477 (erc--msg msg erc--spkr "bob" =
erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-pr=
efix #1# line-prefix #7=3D(space :width (- 27 #10=3D(2))) erc-fill--wrap-me=
rge t display #8=3D#("> " 0 1 (font-lock-face shadow))) 477 480 (wrap-prefi=
x #1# line-prefix #7# erc-fill--wrap-merge t display #8#) 480 482 (wrap-pre=
fix #1# line-prefix #7# erc-fill--wrap-merge t display #8#) 482 485 (wrap-p=
refix #1# line-prefix #7#) 486 487 (erc--msg ctcp-action erc--spkr "bob" er=
c--ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION erc--pfx erc--pfx-skip-s=
pkr-fwd wrap-prefix #1# line-prefix #9=3D(space :width (- 27 (6)))) 487 488=
 (wrap-prefix #1# line-prefix #9#) 488 491 (wrap-prefix #1# line-prefix #9#=
) 491 496 (wrap-prefix #1# line-prefix #9#) 497 498 (erc--msg msg erc--spkr=
 "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd =
wrap-prefix #1# line-prefix #11=3D(space :width (- 27 (6)))) 498 501 (wrap-=
prefix #1# line-prefix #11#) 501 507 (wrap-prefix #1# line-prefix #11#) 508=
 509 (erc--msg msg erc--spkr "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc-=
-pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #12=3D(space :width=
 (- 27 #10#)) erc-fill--wrap-merge t display #8#) 509 512 (wrap-prefix #1# =
line-prefix #12# erc-fill--wrap-merge t display #8#) 512 514 (wrap-prefix #=
1# line-prefix #12# erc-fill--wrap-merge t display #8#) 514 517 (wrap-prefi=
x #1# line-prefix #12#) 518 519 (erc--msg ctcp-action erc--spkr "bob" erc--=
ts 1680332400 erc--cmd PRIVMSG erc--ctcp ACTION erc--pfx erc--pfx-skip-spkr=
-fwd wrap-prefix #1# line-prefix #13=3D(space :width (- 27 (2)))) 519 520 (=
wrap-prefix #1# line-prefix #13#) 520 523 (wrap-prefix #1# line-prefix #13#=
) 523 529 (wrap-prefix #1# line-prefix #13#) 530 531 (erc--msg msg erc--spk=
r "bob" erc--ts 1680332400 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd=
 wrap-prefix #1# line-prefix #14=3D(space :width (- 27 (6)))) 531 534 (wrap=
-prefix #1# line-prefix #14#) 534 541 (wrap-prefix #1# line-prefix #14#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld =
b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
index 0228e716731..e64e7f259eb 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-01-start.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 27) line-prefix (space :width (- 27 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(sp=
ace :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (f=
ield erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-time=
stamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00=
:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" =
erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (=
- 27 (8)))) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix =
#1# line-prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wra=
p-prefix #1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350=
 351 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #=
1# line-prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# lin=
e-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefi=
x #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 27) line-prefix (space :width (- 27 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wr=
ap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap-pref=
ix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-p=
refix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2# dis=
play ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 19=
3 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--=
pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #3=3D(space :width (- 27 (8))=
)) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-=
prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix =
#1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc=
--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip=
-spkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 27 (6)))) 351 3=
54 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #=
4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line=
-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld =
b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
index 9ab89041b53..c9bcd8af107 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-02-right.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 29) line-prefix (space :width (- 29 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(sp=
ace :width (- 29 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (f=
ield erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-time=
stamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00=
:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" =
erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (=
- 29 (8)))) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix =
#1# line-prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wra=
p-prefix #1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350=
 351 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #=
1# line-prefix #4=3D(space :width (- 29 (6)))) 351 354 (wrap-prefix #1# lin=
e-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefi=
x #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 29) line-prefix (space :width (- 29 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wr=
ap-prefix #1# line-prefix #2=3D(space :width (- 29 (4)))) 22 183 (wrap-pref=
ix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-p=
refix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2# dis=
play ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 19=
3 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--=
pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #3=3D(space :width (- 29 (8))=
)) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-=
prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix =
#1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc=
--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip=
-spkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 29 (6)))) 351 3=
54 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #=
4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line=
-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld b=
/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
index 87ea4692d9d..f7088c2ae40 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-03-left.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 25) line-prefix (space :width (- 25 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(sp=
ace :width (- 25 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (f=
ield erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-time=
stamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00=
:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" =
erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (=
- 25 (8)))) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix =
#1# line-prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wra=
p-prefix #1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350=
 351 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #=
1# line-prefix #4=3D(space :width (- 25 (6)))) 351 354 (wrap-prefix #1# lin=
e-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefi=
x #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 25) line-prefix (space :width (- 25 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wr=
ap-prefix #1# line-prefix #2=3D(space :width (- 25 (4)))) 22 183 (wrap-pref=
ix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-p=
refix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2# dis=
play ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 19=
3 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--=
pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #3=3D(space :width (- 25 (8))=
)) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-=
prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix =
#1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc=
--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip=
-spkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 25 (6)))) 351 3=
54 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #=
4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line=
-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld =
b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
index 0228e716731..e64e7f259eb 100644
--- a/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
+++ b/test/lisp/erc/resources/fill/snapshots/monospace-04-reset.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 27) line-prefix (space :width (- 27 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# line-prefix #2=3D(sp=
ace :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (f=
ield erc-timestamp wrap-prefix #1# line-prefix #2#) 184 191 (field erc-time=
stamp wrap-prefix #1# line-prefix #2# display ((margin right-margin) #("[00=
:00]" 0 7 (invisible timestamp)))) 192 193 (erc--msg msg erc--spkr "alice" =
erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #3=3D(space :width (=
- 27 (8)))) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix =
#1# line-prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wra=
p-prefix #1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350=
 351 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #=
1# line-prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# lin=
e-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefi=
x #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n" =
2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3 20 (field erc-time=
stamp wrap-prefix #1=3D(space :width 27) line-prefix (space :width (- 27 (1=
8)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd wr=
ap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap-pref=
ix #1# line-prefix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-p=
refix #2#) 184 191 (field erc-timestamp wrap-prefix #1# line-prefix #2# dis=
play ((margin right-margin) #("[00:00]" 0 7 (invisible timestamp)))) 192 19=
3 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--=
pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #3=3D(space :width (- 27 (8))=
)) 193 198 (wrap-prefix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-=
prefix #3#) 200 203 (wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix =
#1# line-prefix #3#) 317 349 (wrap-prefix #1# line-prefix #3#) 350 351 (erc=
--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip=
-spkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 27 (6)))) 351 3=
54 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #=
4#) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line=
-prefix #4#))
diff --git a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld b/t=
est/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
index 5405ca2a7dc..13d8d30acb4 100644
--- a/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
+++ b/test/lisp/erc/resources/fill/snapshots/spacing-01-mono.eld
@@ -1 +1 @@
-#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n<b=
ob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> S=
omebody stop me\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3=
 20 (field erc-timestamp wrap-prefix #1=3D(space :width 27) line-prefix (sp=
ace :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 wrap-prefix #1# l=
ine-prefix #2=3D(space :width (- 27 (4)))) 22 183 (wrap-prefix #1# line-pre=
fix #2#) 183 184 (field erc-timestamp wrap-prefix #1# line-prefix #2#) 184 =
191 (field erc-timestamp wrap-prefix #1# line-prefix #2# display ((margin r=
ight-margin) #("[00:00]" 0 7 (invisible timestamp)))) 191 192 (line-spacing=
 0.5) 192 193 (erc--msg msg erc--spkr "alice" erc--ts 0 erc--cmd PRIVMSG wr=
ap-prefix #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-pre=
fix #1# line-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 =
(wrap-prefix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#)=
 317 349 (wrap-prefix #1# line-prefix #3#) 349 350 (line-spacing 0.5) 350 3=
51 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG wrap-prefix #1#=
 line-prefix #4=3D(space :width (- 27 (6)))) 351 354 (wrap-prefix #1# line-=
prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#) 356 361 (wrap-prefix =
#1# line-prefix #4#) 361 436 (wrap-prefix #1# line-prefix #4#) 436 437 (lin=
e-spacing 0.5) 437 438 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRI=
VMSG wrap-prefix #1# line-prefix #5=3D(space :width (- 27 0)) erc-fill--wra=
p-merge #6=3D"" display #6#) 438 441 (wrap-prefix #1# line-prefix #5# erc-f=
ill--wrap-merge #6# display #6#) 441 443 (wrap-prefix #1# line-prefix #5# e=
rc-fill--wrap-merge #6# display #6#) 443 467 (wrap-prefix #1# line-prefix #=
5#) 467 468 (line-spacing 0.5) 468 469 (erc--msg notice erc--ts 0 wrap-pref=
ix #1# line-prefix #7=3D(space :width (- 27 (4)))) 469 485 (wrap-prefix #1#=
 line-prefix #7#) 486 487 (erc--msg notice erc--ts 0 wrap-prefix #1# line-p=
refix #8=3D(space :width (- 27 (4)))) 487 503 (wrap-prefix #1# line-prefix =
#8#) 503 504 (line-spacing 0.5) 504 505 (erc--msg msg erc--spkr "bob" erc--=
ts 0 erc--cmd PRIVMSG wrap-prefix #1# line-prefix #9=3D(space :width (- 27 =
(6)))) 505 508 (wrap-prefix #1# line-prefix #9#) 508 526 (wrap-prefix #1# l=
ine-prefix #9#))
\ No newline at end of file
+#("\n\n\n[Thu Jan  1 1970]\n*** This server is in debug mode and is loggin=
g all user I/O. If you do not wish for everything you send to be readable b=
y the server owner(s), please disconnect. [00:00]\n<alice> bob: come, you a=
re a tedious fool: to the purpose. What was done to Elbow's wife, that he h=
ath cause to complain of? Come me to what was done to her.\n<bob> alice: Ei=
ther your unparagoned mistress is dead, or she's outprized by a trifle.\n<b=
ob> This buffer is for text.\n*** one two three\n*** four five six\n<bob> S=
omebody stop me\n" 2 3 (erc--msg datestamp erc--ts 0 field erc-timestamp) 3=
 20 (field erc-timestamp wrap-prefix #1=3D(space :width 27) line-prefix (sp=
ace :width (- 27 (18)))) 21 22 (erc--msg notice erc--ts 0 erc--pfx erc--pfx=
-skip-notice-fwd wrap-prefix #1# line-prefix #2=3D(space :width (- 27 (4)))=
) 22 183 (wrap-prefix #1# line-prefix #2#) 183 184 (field erc-timestamp wra=
p-prefix #1# line-prefix #2#) 184 191 (field erc-timestamp wrap-prefix #1# =
line-prefix #2# display ((margin right-margin) #("[00:00]" 0 7 (invisible t=
imestamp)))) 191 192 (line-spacing 0.5) 192 193 (erc--msg msg erc--spkr "al=
ice" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix=
 #1# line-prefix #3=3D(space :width (- 27 (8)))) 193 198 (wrap-prefix #1# l=
ine-prefix #3#) 198 200 (wrap-prefix #1# line-prefix #3#) 200 203 (wrap-pre=
fix #1# line-prefix #3#) 203 316 (wrap-prefix #1# line-prefix #3#) 317 349 =
(wrap-prefix #1# line-prefix #3#) 349 350 (line-spacing 0.5) 350 351 (erc--=
msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-s=
pkr-fwd wrap-prefix #1# line-prefix #4=3D(space :width (- 27 (6)))) 351 354=
 (wrap-prefix #1# line-prefix #4#) 354 356 (wrap-prefix #1# line-prefix #4#=
) 356 361 (wrap-prefix #1# line-prefix #4#) 361 436 (wrap-prefix #1# line-p=
refix #4#) 436 437 (line-spacing 0.5) 437 438 (erc--msg msg erc--spkr "bob"=
 erc--ts 0 erc--cmd PRIVMSG erc--pfx erc--pfx-skip-spkr-fwd wrap-prefix #1#=
 line-prefix #5=3D(space :width (- 27 0)) erc-fill--wrap-merge #6=3D"" disp=
lay #6#) 438 441 (wrap-prefix #1# line-prefix #5# erc-fill--wrap-merge #6# =
display #6#) 441 443 (wrap-prefix #1# line-prefix #5# erc-fill--wrap-merge =
#6# display #6#) 443 467 (wrap-prefix #1# line-prefix #5#) 467 468 (line-sp=
acing 0.5) 468 469 (erc--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice=
-fwd wrap-prefix #1# line-prefix #7=3D(space :width (- 27 (4)))) 469 485 (w=
rap-prefix #1# line-prefix #7#) 486 487 (erc--msg notice erc--ts 0 erc--pfx=
 erc--pfx-skip-notice-fwd wrap-prefix #1# line-prefix #8=3D(space :width (-=
 27 (4)))) 487 503 (wrap-prefix #1# line-prefix #8#) 503 504 (line-spacing =
0.5) 504 505 (erc--msg msg erc--spkr "bob" erc--ts 0 erc--cmd PRIVMSG erc--=
pfx erc--pfx-skip-spkr-fwd wrap-prefix #1# line-prefix #9=3D(space :width (=
- 27 (6)))) 505 508 (wrap-prefix #1# line-prefix #9#) 508 526 (wrap-prefix =
#1# line-prefix #9#))
diff --git a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld b/te=
st/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
index 1b22b6c5cfd..2310b1d7af2 100644
--- a/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
+++ b/test/lisp/erc/resources/fill/snapshots/stamps-left-01.eld
@@ -1 +1 @@
-#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O=
. If you do not wish for everything you send to be readable by the server o=
wner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fo=
ol: to the purpose. What was done to Elbow's wife, that he hath cause to co=
mplain of? Come me to what was done to her.\n[00:00]<bob> alice: Either you=
r unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc=
--msg notice erc--ts 0 display #3=3D(#5=3D(margin left-margin) #("[00:00]" =
0 7 (invisible timestamp font-lock-face erc-timestamp-face))) field erc-tim=
estamp wrap-prefix #1=3D(space :width 27) line-prefix #2=3D(space :width (-=
 27 (4)))) 3 9 (display #3# field erc-timestamp wrap-prefix #1# line-prefix=
 #2#) 9 171 (wrap-prefix #1# line-prefix #2#) 172 173 (erc--msg msg erc--ts=
 0 erc--spkr "alice" erc--cmd PRIVMSG display #6=3D(#5# #("[00:00]" 0 7 (in=
visible timestamp font-lock-face erc-timestamp-face))) field erc-timestamp =
wrap-prefix #1# line-prefix #4=3D(space :width (- 27 (8)))) 173 179 (displa=
y #6# field erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-pr=
efix #1# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187=
 (wrap-prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#=
) 190 303 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-p=
refix #4#) 337 338 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG=
 display #8=3D(#5# #("[00:00]" 0 7 (invisible timestamp font-lock-face erc-=
timestamp-face))) field erc-timestamp wrap-prefix #1# line-prefix #7=3D(spa=
ce :width (- 27 (6)))) 338 344 (display #8# field erc-timestamp wrap-prefix=
 #1# line-prefix #7#) 344 345 (wrap-prefix #1# line-prefix #7#) 345 348 (wr=
ap-prefix #1# line-prefix #7#) 348 350 (wrap-prefix #1# line-prefix #7#) 35=
0 355 (wrap-prefix #1# line-prefix #7#) 355 430 (wrap-prefix #1# line-prefi=
x #7#))
\ No newline at end of file
+#("\n\n[00:00]*** This server is in debug mode and is logging all user I/O=
. If you do not wish for everything you send to be readable by the server o=
wner(s), please disconnect.\n[00:00]<alice> bob: come, you are a tedious fo=
ol: to the purpose. What was done to Elbow's wife, that he hath cause to co=
mplain of? Come me to what was done to her.\n[00:00]<bob> alice: Either you=
r unparagoned mistress is dead, or she's outprized by a trifle.\n" 2 3 (erc=
--msg notice erc--ts 0 erc--pfx erc--pfx-skip-notice-fwd display #3=3D(#5=
=3D(margin left-margin) #("[00:00]" 0 7 (invisible timestamp font-lock-face=
 erc-timestamp-face))) field erc-timestamp wrap-prefix #1=3D(space :width 2=
7) line-prefix #2=3D(space :width (- 27 (4)))) 3 9 (display #3# field erc-t=
imestamp wrap-prefix #1# line-prefix #2#) 9 171 (wrap-prefix #1# line-prefi=
x #2#) 172 173 (erc--msg msg erc--ts 0 erc--spkr "alice" erc--cmd PRIVMSG e=
rc--pfx erc--pfx-skip-spkr-fwd display #6=3D(#5# #("[00:00]" 0 7 (invisible=
 timestamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-pr=
efix #1# line-prefix #4=3D(space :width (- 27 (8)))) 173 179 (display #6# f=
ield erc-timestamp wrap-prefix #1# line-prefix #4#) 179 180 (wrap-prefix #1=
# line-prefix #4#) 180 185 (wrap-prefix #1# line-prefix #4#) 185 187 (wrap-=
prefix #1# line-prefix #4#) 187 190 (wrap-prefix #1# line-prefix #4#) 190 3=
03 (wrap-prefix #1# line-prefix #4#) 304 336 (wrap-prefix #1# line-prefix #=
4#) 337 338 (erc--msg msg erc--ts 0 erc--spkr "bob" erc--cmd PRIVMSG erc--p=
fx erc--pfx-skip-spkr-fwd display #8=3D(#5# #("[00:00]" 0 7 (invisible time=
stamp font-lock-face erc-timestamp-face))) field erc-timestamp wrap-prefix =
#1# line-prefix #7=3D(space :width (- 27 (6)))) 338 344 (display #8# field =
erc-timestamp wrap-prefix #1# line-prefix #7#) 344 345 (wrap-prefix #1# lin=
e-prefix #7#) 345 348 (wrap-prefix #1# line-prefix #7#) 348 350 (wrap-prefi=
x #1# line-prefix #7#) 350 355 (wrap-prefix #1# line-prefix #7#) 355 430 (w=
rap-prefix #1# line-prefix #7#))
--=20
2.53.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0005-5.7-Introduce-lower-level-erc-match-API.patch

From e1fc967878a4a6b4255ee1e5d2cb10299dbb7788 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sat, 3 Jun 2023 02:01:29 -0700
Subject: [PATCH 5/6] [5.7] Introduce lower level erc-match API

* doc/misc/erc.texi (Match API): New node under the Advanced chapter.
Update menus.
* lisp/erc/erc-match.el (erc-pal-highlight-type, erc-fool-highlight-type)
(erc-dangerous-host-highlight-type): Add `nick-or-mention' variant.
(erc-text-matched-hook): Doc.
(erc-match-functions): New option.
(erc-add-entry-to-list, erc-remove-entry-from-list): Clear options
cache.
(erc-match, erc-match-traditional, erc-match-opt-current-nick)
(erc-match-opt-keyword, erc-match-opt-user, erc-match-opt-fool)
(erc-match-opt-pal, erc-match-opt-dangerous-host): New struct types.
(erc-match--opt-pat-cache): New variable.
(erc-match--opt-pat-ttl): New variable.
(erc-match--opt-pat): New struct type.
(erc-match--opt-pat-cache-clear, erc-match--opt-pat-cache-clear-all)
(erc-match--opt-pat-get, erc-match--opt-pat-make)
(erc-match--opt-pat-kw-make, erc-match--opt-pat-addr-beg-make)
(erc-match--opt-pat-addr-end-make, erc-match--current-nick-p)
(erc-match--keyword-p, erc-match--user-nuh-or-mention-p): New functions.
(erc-match-highlight-by-part): New generic function and methods.
(erc-match-highlight-matched): New variable.
(erc-match--instances): New variable.
(erc-match-highlight): New function.
(erc-match-get-message-body): New function.
(erc-match-get-match): New function
(erc-match--run-match): New function.
(erc-match--message): New function.
(erc-match-use-legacy-logic-p): New variable.
(erc-match-message): Move body to `erc-match--message-legacy.  Rework as
thin wrapper.
(erc-match--message-legacy): New function with body of former
`erc-match-message'.
(erc-log-matches): Rework to be slightly less wasteful.
* test/lisp/erc/erc-match-tests.el (erc-match-tests--perform): Shadow
`erc-match--opt-pat-cache'.
(erc-match-message/pal/nick/legacy, erc-match-message/fool/nick/legacy)
(erc-match-message/dangerous-host/nick/legacy): New tests.
(erc-match-tests--hl-type-nick-or-mention): New function.
(erc-match-message/pal/nick-or-mention)
(erc-match-message/fool/nick-or-mention)
(erc-match-message/dangerous-host/nick-or-mention)
(erc-match-message/pal/message/legacy)
(erc-match-message/fool/message/legacy)
(erc-match-message/dangerous-host/message/legacy)
(erc-match-message/pal/all/legacy, erc-match-message/fool/all/legacy)
(erc-match-message/dangerous-host/all/legacy)
(erc-match-message/current-nick/nick-or-keyword/legacy)
(erc-match-message/keyword/keyword/legacy, erc-log-matches/legacy)
(erc-match-functions/api/non-parts-based)
(erc-match-functions/api/parts-based): New tests.
* test/lisp/erc/erc-scenarios-match-api.el: New file.
* test/lisp/erc/resources/match/functions/custom-match-log: New file.
* test/lisp/erc/resources/match/functions/custom.eld: New file.
(Bug#73798)
---
 doc/misc/erc.texi                             | 381 +++++++++++++++++
 lisp/erc/erc-match.el                         | 384 ++++++++++++++++--
 test/lisp/erc/erc-match-tests.el              | 250 +++++++++++-
 test/lisp/erc/erc-scenarios-match-api.el      | 127 ++++++
 .../match/functions/custom-match-log          |  39 ++
 .../erc/resources/match/functions/custom.eld  |  49 +++
 6 files changed, 1202 insertions(+), 28 deletions(-)
 create mode 100644 test/lisp/erc/erc-scenarios-match-api.el
 create mode 100644 test/lisp/erc/resources/match/functions/custom-match-log
 create mode 100644 test/lisp/erc/resources/match/functions/custom.eld

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index b0d4b178127..cc6820480cc 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -87,6 +87,7 @@ Top
 * SASL::                        Authenticating via SASL.
 * Sample Configuration::        An example configuration file.
 * Integrations::                Integrations available for ERC.
+* Match API::                   Custom matching and highlighting.
 * Options::                     Options that are available for ERC.
 
 @end detailmenu
@@ -774,6 +775,7 @@ Advanced Usage
 * SASL::                        Authenticating via SASL.
 * Sample Configuration::        An example configuration file.
 * Integrations::                Integrations available for ERC.
+* Match API::                   Custom matching and highlighting.
 * Options::                     Options that are available for ERC.
 
 @detailmenu
@@ -2108,6 +2110,385 @@ display-buffer
 @end itemize
 @end table
 
+@node Match API
+@section Match API
+@cindex low-level match
+
+This section describes the low-level @samp{match} @acronym{API}
+introduced in ERC 5.7.  For basic configuration help, please see the doc
+strings for option @code{erc-pal-highlight-type} and friends in the
+@code{erc-match} group.  Unfortunately, those options often prove
+insufficient for more granular filtering and highlighting needs, and
+advanced users may come to outgrow them.  However, under the hood, those
+options all use the same foundational @code{erc-match} API, which
+centers around a @code{cl-defstruct} @dfn{type} of the same name:
+
+@deftp {Struct} erc-match @
+  predicate spkr-beg spkr-end body-beg sender nick command handler
+
+  This is a @code{cl-struct} type that contains some handy facts about
+  the message being processed.  That formatted message occupies the
+  narrowed buffer when ERC creates and provides access to each
+  @code{erc-match} instance.  To use this interface, you add a
+  @dfn{constructor}-like function to the hook
+  @code{erc-match-functions}:
+
+  @defopt erc-match-functions
+
+  An abnormal hook for which each member function accepts the parameters
+  named above as an @samp{&rest}-style plist and returns a new
+  @code{erc-match} instance.  Conforming to this interface are, of
+  course, traditional constructors automatically provided by a
+  @code{cl-defstruct} definition derived from @code{erc-match}, such as
+  @samp{make-my-match} for some @dfn{subtype} @samp{my-match}.
+
+  @end defopt
+
+  The only slot you definitely need to specify is @samp{predicate}.
+  Both it and @samp{handler} are functions that take a single argument:
+  the instance itself.  As its name implies, @samp{predicate} must
+  return non-@code{nil} if @samp{handler}, whose return value ERC
+  ignores, should run.
+
+  A few slots, like @samp{spkr-beg}, @samp{spkr-end}, and @samp{nick},
+  may surprise you.  The first two are null for non-chat messages, like
+  those displayed for @samp{JOIN} events.  The @samp{nick} slot can
+  likewise be @code{nil} if the sender of the message is a domain-style
+  host name, such as @samp{irc.example.org}, which it often is for
+  informational messages, like @samp{*** #chan was created on 2023-12-26
+  00:36:42}.  This includes, for example, server-sent @samp{NOTICE}s and
+  @samp{221} user-mode changes.
+
+  To locate the start of the just-inserted message, use slot
+  @samp{body-beg}, a marker indicating the beginning of the message
+  proper.  Don't forget: all inserted messages include a trailing
+  newline.  If you want to extract just the message body's text, use the
+  function @code{erc-match-get-message-body}:
+
+  @defun erc-match-get-message-body match
+
+  Takes an @code{erc-match} instance and returns a string containing the
+  message body, sans trailing newline and any leading speaker or
+  decorative component, such as @code{erc-notice-prefix}.  The match
+  buffer must be current.
+
+  @end defun
+
+@end deftp
+
+@noindent
+Unless you're writing a module, there's often no need to subclass
+@code{erc-match}.  For everyday tasks or simple customizations in your
+@file{init.el}, you can often just instantiate it directly (it's
+@dfn{concrete}).  To do this, define a function that invokes its
+constructor:
+
+@lisp
+(require 'erc-match)
+
+(defvar my-mentions 0)
+
+(defun my-match (&rest plist)
+  (apply #'erc-match
+         :predicate (lambda (_) (search-forward "my-project" nil t))
+         :handler (lambda (_) (cl-incf my-mentions))
+         plist))
+
+(add-hook 'erc-match-functions #'my-match)
+(setopt erc-prompt (lambda () (format "%d!" my-mentions)))
+@end lisp
+
+@noindent
+Here, the user could just as well shove the incrementer into the
+@samp{predicate} body, since @samp{handler} is set to @code{ignore} by
+default (however, some frown at the notion of a predicate exhibiting
+side effects).  The user could also choose to concentrate only on chat
+content by filtering out non-@samp{PRIVMSG} messages via the slot
+@samp{command}.
+
+In cases where you need a handler to only run when some other match type
+appearing earlier in @code{erc-match-functions} has _not_ yielded a
+match, use:
+
+@defun erc-match-get-match constructor
+
+When called from a @samp{handler} or a @samp{predicate} body, this
+utility returns the @code{erc-match} instance from the last successful
+match by @code{erc-match-functions} member @var{constructor} for the
+current message.  Use this for deduplication and to share data between
+match instances.
+
+@end defun
+
+@noindent
+For a detailed example of matching for non-highlighting purposes, see
+the @samp{jabbycat} demo module, available on ERC's dev-oriented package
+archive: @uref{https://emacs-erc.gitlab.io/bugs/archive/jabbycat.html}.
+If you're in a hurry, check out @file{erc-desktop-notifications.el},
+which ships with ERC.  Just be aware that it's full of potentially
+confusing compatibility-related fuss you'll want to ignore, such as
+adapting the global setup and teardown business to a buffer-local
+context.  (New modules most likely adhere to the modern convention of
+being defined as @code{local} in order to allow things like adding local
+members to @code{erc-match-functions} in their @dfn{enable body}.)
+
+@anchor{highlighting}
+@subsection Highlighting
+@cindex highlighting
+
+Module authors and normal users alike will often want to manage and
+apply faces themselves.  If that's you, feel free to skip to the more
+extensive examples in the subsection below.  However, for the sake of
+completeness, it's worth mentioning that (especially in a pinch) you can
+likely piggyback atop the highlighting functionality already provided by
+@samp{match} to support its many high-level options.
+
+@lisp
+(require 'erc-match)
+
+(defvar my-keywords
+  `((foonet ("#chan" ,(rx bow (or "foo" "bar" "baz") eow)))))
+
+(defface my-face
+  '((t (:inherit font-lock-constant-face :weight bold)))
+  "My face.")
+
+(defun my-match (&rest plist)
+  (apply #'erc-match-opt-keyword
+         :data (and-let* ((chans (alist-get (erc-network) my-keywords)))
+                 (cdr (assoc (erc-target) chans)))
+         :face 'my-face
+         plist))
+
+(add-hook 'erc-match-functions #'my-match)
+@end lisp
+
+@noindent
+Here, the user leverages a handy subtype of @code{erc-match}, called
+@code{erc-match-opt-keyword}, which actually descends directly from
+another, intermediate @code{erc-match} type:
+
+@deftp {Struct} erc-match-traditional category face data part
+
+Use this type or one of its descendants (see below) if you want
+@code{erc-text-matched-hook} to run right after the @samp{handler}
+slot's default highlighter, @code{erc-match-highlight}, on every match
+for which the @samp{category} slot's value is non-@code{nil} (it becomes
+the argument provided for the hook's @var{match-type} parameter).
+
+The @samp{part} slot determines what portion of the message is being
+highlighted or otherwise operated on.  It can be any symbol, but the
+ones with predefined methods are @code{nick}, @code{message},
+@code{all}, @code{keyword}, @code{nick-or-keyword}, and
+@code{nick-or-mention}.
+
+Accompanying the @samp{part} slot is @samp{data}, which holds the value
+of the module's option corresponding to the specific type.  For example,
+ERC initializes the @samp{data} slot for the @code{erc-match-opt-pal}
+type with the value of @code{erc-pals}.
+
+The default handler, @code{erc-match-highlight}, does its work by
+deferring to a purpose-built @dfn{method} meant to handle
+@samp{part}-based highlighting:
+
+@defop {Method} erc-match-traditional erc-match-highlight-by-part @
+  instance part
+
+  You can override this method by @dfn{specializing} on any subclassed
+  @code{erc-match-traditional} type and/or non-reserved @var{part}, such
+  as one known only to your @file{init.el} or (informally) associated
+  with your package by its library @dfn{namespace}.
+
+@end defop
+
+@end deftp
+
+@noindent
+You likely won't be needing these, but just for the record, other
+options-based types similar to @code{erc-match-opt-keyword} include
+@code{erc-match-opt-current-nick}, @code{erc-match-opt-fool},
+@code{erc-match-opt-pal}, and @code{erc-match-opt-dangerous-host}.  (If
+you're familiar with this module's user options, you'll notice some
+parallels here.)
+
+@anchor{highlighting examples}
+@subsubsection Complete Highlighting Examples
+@cindex highlighting examples
+
+As mentioned, most users needn't bother with the piggybacking approach
+detailed above, which can oftentimes be more complicated than starting
+afresh.  Here's a more elaborate, module-like example demoing some
+highlighting with a bespoke @code{erc-match}-derived type:
+
+@lisp
+;;; erc-org-markup.el --- Org Markup for ERC -*- lexical-binding: t; -*-
+
+(require 'erc-match)
+(require 'org)
+
+(defgroup erc-org-markup nil
+  "Highlight messages written in Org markup."
+  :group 'erc)
+
+(defcustom erc-org-markup-targets '("#erc-org-markup")
+  "List of buffers in which to highlight messages."
+  :type '(repeat string))
+
+(define-erc-module org-markup nil
+  "Local module that treats messages as having Org markup."
+  ((erc-org-markup-ensure-buffer)
+   (if (member (erc-target) erc-org-markup-targets)
+       (progn
+         (add-hook 'erc-match-functions #'erc-org-markup 0 t)
+         (add-to-invisibility-spec 'org-link))
+     (erc-org-markup-mode -1)))
+  ((remove-hook 'erc-match-functions #'erc-org-markup t)
+   (remove-from-invisibility-spec 'org-link))
+  localp)
+
+(cl-defstruct (erc-org-markup
+               (:include erc-match
+                         (predicate #'erc-org-markup--should-p)
+                         (handler #'erc-org-markup--fontify))
+               (:constructor erc-org-markup))
+  "Match type to highlight messages written in Org markup.")
+
+(defun erc-org-markup--should-p (match)
+  "Return non-nil if MATCH describes an Org-markup worthy message."
+  ;; For demo purposes, treat all spoken chat messages as markup.
+  (and erc-org-markup-mode (erc-match-nick match)))
+
+(defun erc-org-markup-ensure-buffer ()
+  "Return existing global work buffer or create it anew."
+  (or (get-buffer "*erc-org-markup*")
+      (with-current-buffer (get-buffer-create "*erc-org-markup*")
+        (org-mode)
+        (make-local-variable 'org-link-parameters)
+        (setf (plist-get (cdr (assoc "https" org-link-parameters))
+                         :activate-func)
+              #'erc-org-markup-activate-link)
+        (setq-local org-hide-emphasis-markers t)
+        (current-buffer))))
+
+(defun erc-org-markup--fontify (match)
+  "Overwrite text properties in MATCH'd message with Org's."
+  (save-restriction
+    (narrow-to-region (erc-match-body-beg match) (1- (point-max)))
+    (let ((buffer (current-buffer)))
+      (with-current-buffer (erc-org-markup-ensure-buffer)
+        (save-window-excursion
+          (buffer-swap-text buffer)
+          (font-lock-ensure)
+          (buffer-swap-text buffer))))))
+
+(defun erc-org-markup-activate-link (beg end path _)
+  "Ensure Org https link between BEG and END has `erc-button' props."
+  (erc-button-add-button beg end #'browse-url-button-open-url nil
+                         (list (concat "https:" path)) ""))
+
+(provide 'erc-org-markup)
+
+;;; erc-org-markup.el ends here
+@end lisp
+
+@noindent
+Finally, here's a slightly more complete demo module: a superficial
+rewrite of @file{erc-colorize.el} by Sylvain Rousseau
+@uref{https://github.com/thisirs/erc-colorize.git}.
+
+@lisp
+;;; erc-colorize.el --- Per-user message faces -*- lexical-binding: t; -*-
+
+(require 'ring)
+(require 'erc-match)
+(require 'erc-button) ; for `erc-button-add-face'
+
+(defgroup erc-colorize nil
+  "Highlight messages with per-user faces from a limited pool."
+  :group 'erc)
+
+(defface erc-colorize-1 '((t :inherit font-lock-keyword-face))
+  "Auto-assigned face for distinguishing between messages.")
+
+(defface erc-colorize-2 '((t :inherit font-lock-type-face))
+  "Auto-assigned face for distinguishing between messages.")
+
+(defface erc-colorize-3 '((t :inherit font-lock-string-face))
+  "Auto-assigned face for distinguishing between messages.")
+
+(defface erc-colorize-4 '((t :inherit font-lock-constant-face))
+  "Auto-assigned face for distinguishing between messages.")
+
+(defface erc-colorize-5 '((t :inherit font-lock-preprocessor-face))
+  "Auto-assigned face for distinguishing between messages.")
+
+(defface erc-colorize-6 '((t :inherit font-lock-variable-name-face))
+  "Auto-assigned face for distinguishing between messages.")
+
+(defface erc-colorize-7 '((t :inherit font-lock-warning-face))
+  "Auto-assigned face for distinguishing between messages.")
+
+(defvar erc-colorize-faces '(erc-colorize-1
+                             erc-colorize-2
+                             erc-colorize-3
+                             erc-colorize-4
+                             erc-colorize-5
+                             erc-colorize-6
+                             erc-colorize-7)
+  "List of faces to apply to chat messages.")
+
+(defvar-local erc-colorize-ring nil
+  "Ring of cons cells of the form (NICK . FACE).")
+
+(define-erc-module colorize nil
+  "Highlight messages from a speaker with the same face in target buffers."
+  ((when (erc-target)
+     (add-hook 'erc-match-functions 'erc-colorize 0 t)
+     (setq erc-colorize-ring (make-ring (length erc-colorize-faces)))))
+  ((remove-hook 'erc-match-functions 'erc-colorize t))
+  localp)
+
+(defun erc-colorize-color (ring nick)
+  "Return a face to use for string NICK.
+Prefer an existing entry in RING.  If there isn't one, pick the first
+unused face in `erc-colorize-faces'.  Otherwise, pick the least used
+face."
+  (or
+   (and-let* ((i (catch 'found
+                   (dotimes (i (ring-length ring))
+                     (when (equal (car (ring-ref ring i)) nick)
+                       (throw 'found i))))))
+     (ring-insert ring (ring-remove ring i))
+     (cdr (ring-ref ring 0)))
+   (let ((used (mapcar #'cdr (ring-elements ring))))
+     (and-let* ((face (catch 'found
+                        (dolist (face erc-colorize-faces)
+                          (unless (member face used)
+                            (throw 'found face))))))
+       (prog1 face
+         (ring-insert ring (cons nick face)))))
+   (let ((older (ring-remove ring)))
+     (ring-insert ring (cons nick (cdr older)))
+     (cdr older))))
+
+(cl-defstruct (erc-colorize ( :include erc-match
+                              (predicate #'erc-colorize-nick)
+                              (handler #'erc-colorize-message))
+                            (:constructor erc-colorize))
+  "An `erc-match' type for the `erc-colorize' module.")
+
+(defun erc-colorize-message (match)
+  "Highlight MATCH's full message with a face from `erc-colorize-faces'."
+  (erc-button-add-face (point-min) (1- (point-max))
+                       (erc-colorize-color erc-colorize-ring
+                                           (erc-colorize-nick match))))
+
+(provide 'erc-colorize)
+
+;;; erc-colorize.el ends here
+@end lisp
+
+
 @node Options
 @section Options
 @cindex options
diff --git a/lisp/erc/erc-match.el b/lisp/erc/erc-match.el
index b003cb1f49e..46184844a27 100644
--- a/lisp/erc/erc-match.el
+++ b/lisp/erc/erc-match.el
@@ -63,14 +63,28 @@ match
    (erc-buffer-do #'erc-match--setup)
    (erc--modify-local-map nil "C-c C-k" #'erc-go-to-log-matches-buffer)))
 
+;; This caches the result of applying `regexp-opt' analogs to the
+;; regexp-based user options, mainly for history playback bursts.
+(defvar erc-match--opt-pat-cache nil
+  "Alist of (COMPUTE-FN . PAIRS) where PAIRS is an alist of (IN . OUT).")
+
+(defun erc-match--opt-pat-custom-set (sym val &optional _)
+  "Assign VAL to SYM via `set'."
+  (when erc-match--opt-pat-cache
+    (setq erc-match--opt-pat-cache nil))
+  (set sym val))
+
+
 ;; Remaining customizations
 
 (defcustom erc-pals nil
   "List of pals on IRC."
+  :set #'erc-match--opt-pat-custom-set
   :type '(repeat regexp))
 
 (defcustom erc-fools nil
   "List of fools on IRC."
+  :set #'erc-match--opt-pat-custom-set
   :type '(repeat regexp))
 
 (defcustom erc-keywords nil
@@ -78,12 +92,14 @@ erc-keywords
 Each entry in the list is either a regexp, or a cons cell with the
 regexp in the car and the face to use in the cdr.  If no face is
 specified, `erc-keyword-face' is used."
+  :set #'erc-match--opt-pat-custom-set
   :type '(repeat (choice regexp
 			 (list regexp face))))
 
 (defcustom erc-dangerous-hosts nil
   "List of regexps for hosts to highlight.
 Useful to mark nicks from dangerous hosts."
+  :set #'erc-match--opt-pat-custom-set
   :type '(repeat regexp))
 
 (defcustom erc-current-nick-highlight-type 'keyword
@@ -122,10 +138,15 @@ erc-pal-highlight-type
     `all'     - highlight the entire message (including the nick)
                 from pal
 
+    `nick-or-mention' - highlight a matching speaker or all matching
+                        mentions as quasi keywords
+
 A value of `nick' only highlights a matching sender's nick in the
 bracketed speaker portion of the message.  A value of \\+`message'
 basically highlights its complement: the message-body alone, after the
-speaker tag.  All values for this option require a matching sender to be
+speaker tag.  A value of `nick-or-mention' works like `nick' but also
+matches \"mentions,\" which `erc-fool-highlight-type' explains in its
+doc string.  All values for this option require a matching sender to be
 an actual user on the network \(or a bot/service) as opposed to a host
 name, such as that of the server itself \(e.g. \"irc.gnu.org\").  When
 patterns from other user-based categories \(namely, \\+`fool' and
@@ -135,6 +156,7 @@ erc-pal-highlight-type
 \\+`fool'-related invisibility may not survive such collisions.)"
   :type '(choice (const nil)
 		 (const nick)
+                 (const nick-or-mention)
                  (const message)
 		 (const all)))
 
@@ -148,12 +170,12 @@ erc-fool-highlight-type
   <speaker> USER: hi.
   <speaker> USER, hi.
 
-However, at present, this option doesn't offer a means of highlighting
-matched mentions alone.  See `erc-pal-highlight-type' for a summary of
-possible values and additional details common to categories like
-\\+`fool' that normally match against a message's sender."
+See `erc-pal-highlight-type' for a summary of possible values and
+additional details common to categories like \\+`fool' that normally
+match against a message's sender."
   :type '(choice (const nil)
 		 (const nick)
+                 (const nick-or-mention)
                  (const message)
 		 (const all)))
 
@@ -182,6 +204,7 @@ erc-dangerous-host-highlight-type
 normally match against a message's sender."
   :type '(choice (const nil)
 		 (const nick)
+                 (const nick-or-mention)
                  (const message)
 		 (const all)))
 
@@ -267,6 +290,22 @@ erc-match-quote-when-adding
                  (const t)
                  (const nil)))
 
+(defcustom erc-match-functions '(erc-match-opt-pal
+                                 erc-match-opt-fool
+                                 erc-match-opt-dangerous-host
+                                 erc-match-opt-keyword
+                                 erc-match-opt-current-nick)
+  "Type constructors for \\+`match' processing.
+See the struct `erc-match' as well as Info node `(erc) Match API' for
+details."
+  :package-version '(ERC . "5.7") ; FIXME sync on release
+  :type '(hook :options (erc-match-opt-pal
+                         erc-match-opt-fool
+                         erc-match-opt-dangerous-host
+                         erc-match-opt-keyword
+                         erc-match-opt-current-nick)))
+
+
 ;; Internal variables:
 
 ;; This is exactly the same as erc-button-syntax-table.  Should we
@@ -322,6 +361,8 @@ erc-add-entry-to-list
 LIST must be passed as a symbol
 The query happens using PROMPT.
 Completion is performed on the optional alist COMPLETIONS."
+  (when erc-match--opt-pat-cache
+    (setq erc-match--opt-pat-cache nil))
   (let ((entry (completing-read
 		prompt
 		completions
@@ -345,6 +386,8 @@ erc-remove-entry-from-list
 LIST must be passed as a symbol.
 The elements of LIST can be strings, or cons cells where the
 car is the string."
+  (when erc-match--opt-pat-cache
+    (setq erc-match--opt-pat-cache nil))
   (let* ((alist (mapcar (lambda (x)
 			  (if (listp x)
 			      x
@@ -468,7 +511,298 @@ erc-match-directed-at-fool-p
     (or (erc-list-match fools-beg msg)
 	(erc-list-match fools-end msg))))
 
+(cl-defstruct (erc-match (:constructor erc-match))
+  "Base type for text and user matching performed by the \\+`match' module.
+Users wishing to perform custom matching should add a constructor that
+returns an instance of this type to the hook `erc-match-functions'.  If
+the `:predicate' slot's predicate returns non-nil after being called
+with its own instance in the narrowed single-message buffer, ERC calls
+the `:handler' slot's function with the same instance and with the match
+data still intact.  More details in Info node `(erc) Match API'."
+  ( predicate (error "Keyword `:predicate' missing") :type function
+    :documentation "Called in narrowed buffer with own instance.")
+  ( spkr-beg nil :type (or null natnum)
+    :documentation "Position of the beginning of speaker's nick, if known.")
+  ( spkr-end nil :type (or null natnum)
+    :documentation "Position of the end of speaker's nick, if known.")
+  ( body-beg (error "Keyword `:body-beg' missing") :type marker
+    :documentation "Marker residing at the beginning of the message body.")
+  ( sender (error "Keyword `:sender' missing") :type string
+    :documentation "The sender's n!u@h.")
+  ( nick nil :type (or null string)
+    :documentation "The sender's nick if they're a user and not the server.")
+  ( command (error "Keyword `:command' missing") :type (or symbol natnum)
+    :documentation "Protocol command or numeric, like `PRIVMSG' or 353.")
+  ( handler #'ignore :type function
+    :documentation "Called on `:predicate' match with own instance."))
+
+(cl-defstruct (erc-match-traditional
+               (:constructor erc-match-traditional)
+               (:include erc-match (handler #'erc-match-highlight)))
+  "Match type for user-option based on \"categories\" and \"parts\".
+The `:category' slot exists for the benefit of `erc-text-matched-hook',
+which receives its value as a second parameter (the hook only runs when
+the slot is non-nil)."
+  ( category (error "Keyword `:category' missing") :type symbol
+    :documentation "Traditional \\+`match' \"category\", like `pal'.")
+  ( face 'erc-default-face :type face
+    :documentation "Face to highlight the matched portion with.")
+  ( part nil :type symbol
+    :documentation "Symbol for the portion of the message to highlight.")
+  ( data nil :type list
+    :documentation "User-specified patterns or other type-specific data."))
+
+(cl-defstruct (erc-match-opt-current-nick
+               (:include erc-match-traditional
+                         (category 'current-nick)
+                         (predicate #'erc-match--current-nick-p)
+                         (part erc-current-nick-highlight-type)
+                         (face 'erc-current-nick-face)
+                         (data (list (concat "\\b"
+                                             (regexp-quote (erc-current-nick))
+                                             "\\b"))))
+               (:constructor erc-match-opt-current-nick))
+  "An options-based type for the `current-nick' category.")
+
+(cl-defstruct (erc-match-opt-keyword
+               (:include erc-match-traditional
+                         (category 'keyword)
+                         (predicate #'erc-match--keyword-p)
+                         (part erc-keyword-highlight-type)
+                         (face 'erc-keyword-face)
+                         (data erc-keywords))
+               (:constructor erc-match-opt-keyword))
+  "An options-based type for the `keyword' category.")
+
+(cl-defstruct (erc-match-user (:include erc-match-traditional)
+                              (:constructor erc-match-user))
+  "An `erc-match' that's only processed when `:nick' is non-nil.")
+
+(cl-defstruct (erc-match-opt-fool
+               (:include erc-match-user
+                         (category 'fool)
+                         (predicate #'erc-match--user-nuh-or-mention-p)
+                         (part erc-fool-highlight-type)
+                         (face 'erc-fool-face)
+                         (data erc-fools))
+               (:constructor erc-match-opt-fool))
+  "An options-based type for the `fool' category.")
+
+(cl-defstruct (erc-match-opt-pal
+               (:include erc-match-user
+                         (category 'pal)
+                         (predicate #'erc-match--user-nuh-or-mention-p)
+                         (part erc-pal-highlight-type)
+                         (face 'erc-pal-face)
+                         (data erc-pals))
+               (:constructor erc-match-opt-pal))
+  "An options-based type for the `pal' category.")
+
+(cl-defstruct (erc-match-opt-dangerous-host
+               (:include erc-match-user
+                         (category 'dangerous-host)
+                         (predicate #'erc-match--user-nuh-or-mention-p)
+                         (part erc-dangerous-host-highlight-type)
+                         (face 'erc-dangerous-host-face)
+                         (data erc-dangerous-hosts))
+               (:constructor erc-match-opt-dangerous-host))
+  "An options-based type for the `dangerous-host' category.")
+
+(defun erc-match--opt-pat-get (compute-fn input)
+  "Retrieve cached results for computing INPUT with COMPUTE-FN."
+  (with-memoization (alist-get input (alist-get compute-fn
+                                                erc-match--opt-pat-cache nil t)
+                               nil t)
+    (funcall compute-fn input)))
+
+(defun erc-match--opt-pat-make (patterns)
+  (string-join patterns "\\|"))
+
+(defun erc-match--opt-pat-kw-make (patterns)
+  (mapconcat (lambda (w) (or (car-safe w) w)) patterns "\\|"))
+
+(defun erc-match--opt-pat-addr-beg-make (patterns)
+  (concat "\\<\\(" (erc-match--opt-pat-make patterns) "\\)[:,] "))
+
+(defun erc-match--opt-pat-addr-end-make (patterns)
+  (concat "\\s. \\(" (erc-match--opt-pat-make patterns) "\\)\\s."))
+
+(defun erc-match--current-nick-p (match)
+  (re-search-forward (car (erc-match-traditional-data match)) nil t))
+
+(defun erc-match--keyword-p (match)
+  "Return non-nil if the pattern given for MATCH's user option matches."
+  (and-let* ((patterns (erc-match-traditional-data match)))
+    (goto-char (erc-match-body-beg match))
+    (re-search-forward (erc-match--opt-pat-get #'erc-match--opt-pat-kw-make
+                                               patterns)
+                       nil t)))
+
+(defun erc-match--user-nuh-or-mention-p (match)
+  "Return non-nil on matching \"NUH\" for MATCH object.
+Also do so on mentions if the category is `fool' or the corresponding
+\"part\" option is `nick-or-mention'."
+  (and-let* ((patterns (erc-match-traditional-data match)))
+    (or (string-match (erc-match--opt-pat-get #'erc-match--opt-pat-make
+                                              patterns)
+                      (erc-match-sender match))
+        (and (or (eq (erc-match-traditional-category match) 'fool)
+                 (eq (erc-match-traditional-part match) 'nick-or-mention))
+             ;; Mimic `erc-match-directed-at-fool-p', but search
+             ;; the narrowed buffer instead of a string argument.
+             (goto-char (erc-match-body-beg match))
+             (or (looking-at (erc-match--opt-pat-get
+                              #'erc-match--opt-pat-addr-beg-make
+                              patterns))
+                 (search-forward-regexp
+                  (erc-match--opt-pat-get #'erc-match--opt-pat-addr-end-make
+                                          patterns)
+                  nil t))))))
+
+(cl-defgeneric erc-match-highlight-by-part (match part)
+  "Highlight PART of narrowed buffer for `erc-match' object MATCH.")
+
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
+                                           (_ (eql nick)))
+  "Highlight MATCH's nick in the bracketed speaker portion of the message."
+  (when (erc-match-spkr-beg match)
+    (erc-put-text-property (erc-match-spkr-beg match)
+                           (erc-match-spkr-end match)
+                           'font-lock-face
+                           (erc-match-traditional-face match))))
+
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
+                                           (_ (eql message)))
+  "Highlight MATCH's message body, not including the leading speaker tag."
+  (erc-put-text-property (erc-match-body-beg match) (point-max)
+                         'font-lock-face (erc-match-traditional-face match)))
+
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
+                                           (_ (eql all)))
+  "Highlight MATCH's whole message, including the speaker tag."
+  (erc-put-text-property (point-min) (point-max)
+                         'font-lock-face (erc-match-traditional-face match)))
+
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
+                                           (_ (eql keyword)))
+  "Highlight all occurrences of all keyword patterns for MATCH."
+  (dolist (pat (erc-match-traditional-data match))
+    (let ((regex (if (consp pat) (car pat) pat))
+          (face (if (consp pat) (cdr pat) (erc-match-traditional-face match))))
+      (goto-char (erc-match-body-beg match))
+      (while (re-search-forward regex nil t)
+        (erc-put-text-property (match-beginning 0) (match-end 0)
+                               'font-lock-face face)))))
+
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
+                                           (_ (eql nick-or-keyword)))
+  "Highlight MATCH's speaker-tag nick if applicable, otherwise all mentions."
+  (if (erc-match-spkr-end match)
+      (erc-put-text-property (erc-match-spkr-beg match)
+                             (erc-match-spkr-end match)
+                             'font-lock-face
+                             (erc-match-traditional-face match))
+    (erc-match-highlight-by-part match 'keyword)))
+
+(cl-defmethod erc-match-highlight-by-part ((match erc-match-traditional)
+                                           (_ (eql nick-or-mention)))
+  "Highlight MATCH's speaker tag nick of matching users or all mentions."
+  (cl-letf (((erc-match-body-beg match)
+             (or (erc-match-spkr-beg match) (point-min))))
+    (erc-match-highlight-by-part match 'keyword)))
+
+(defvar erc-match-highlight-matched nil
+  "Matched `erc-match' instance in `erc-text-matched-hook'.")
+
+(defvar erc-match--instances nil
+  "Alist mapping constructors to successful `erc-match' instances.")
+
+(defun erc-match-highlight (match)
+  "Dispatch `erc-match-highlight-by-part' on MATCH's `:part' slot.
+Run `erc-text-matched-hook' when MATCH's `category' slot is non-nil."
+  (unless (erc-match-traditional-p match)
+    (signal 'wrong-type-argument (list 'erc-match-traditional match)))
+  (erc-match-highlight-by-part match (erc-match-traditional-part match))
+  (when (erc-match-traditional-category match)
+    (let ((user-nuh (and (erc-match-nick match) (erc-match-sender match)))
+          (erc-match-highlight-matched match))
+      (run-hook-with-args 'erc-text-matched-hook
+                          (erc-match-traditional-category match)
+                          (or user-nuh (format "Server:%s"
+                                               (erc-match-command match)))
+                          ;; For compatibility, include a leading "*** ".
+                          (buffer-substring (if user-nuh
+                                                (erc-match-body-beg match)
+                                              (point-min))
+                                            (point-max))))))
+
+(defun erc-match-get-message-body (match)
+  "Return the message body for MATCH in the current narrowed buffer."
+  (buffer-substring (erc-match-body-beg match) (1- (point-max))))
+
+(defun erc-match-get-match (constructor)
+  "Return successful `erc-match' instance for CONSTRUCTOR, if any.
+Expect to be called only from `erc-match' :predicate and :handler
+functions as well as `erc-text-matched-hook' members."
+  (alist-get constructor erc-match--instances))
+
+(defun erc-match--run-match (constructor spkr-beg spkr-end body-beg
+                                         nick sender command)
+  "Run :handler for for `erc-match' instance if :predicate returns non-nil.
+Call CONSTRUCTOR with SPKR-BEG, SPKR-END, BODY-BEG, NICK SENDER, and
+COMMAND to create said instance."
+  (when-let* ((instance (funcall constructor
+                                 :spkr-beg spkr-beg
+                                 :spkr-end spkr-end
+                                 :body-beg body-beg
+                                 :nick nick
+                                 :sender sender
+                                 :command command))
+              (_ (or nick (not (erc-match-user-p instance))))
+              (_ (goto-char (point-min)))
+              (_ (funcall (erc-match-predicate instance) instance)))
+    (funcall (erc-match-handler instance) instance)
+    (push (cons constructor instance) erc-match--instances)
+    nil))
+
+(defun erc-match--message ()
+  "Highlight matches in narrowed buffer's current message."
+  (goto-char (point-min))
+  (let* ((response erc--parsed-response)
+         (user-nuh (and response (erc-get-parsed-vector-nick response)))
+         ;; Nick of sender's NUH if they are not the server itself.
+         (nick (and user-nuh (or (erc--check-msg-prop 'erc--spkr)
+                                 (erc-extract-nick user-nuh))))
+         (unknownp (erc--check-msg-prop 'erc--msg 'unknown))
+         (spkr-end (and (not unknownp) (erc--get-speaker-bounds)))
+         (spkr-beg (and spkr-end (pop spkr-end)))
+         (body-beg (save-excursion
+                     (unless unknownp
+                       (when-let* ((fn (erc--check-msg-prop 'erc--pfx)))
+                         (funcall fn)))
+                     (point-marker)))
+         (command (erc--check-msg-prop 'erc--cmd))
+         (erc-match--instances ()))
+    (with-syntax-table erc-match-syntax-table
+      (run-hook-wrapped 'erc-match-functions #'erc-match--run-match
+                        spkr-beg spkr-end body-beg nick
+                        (erc-response.sender response) command))))
+
+(defvar erc-match-use-legacy-logic-p nil
+  "When non-nil, use the non-`erc-match' variant of `erc-match-message'.")
+(make-obsolete 'erc-match-use-legacy-logic-p
+               "non-nil behavior is missing features and integrations" "31.1")
+
 (defun erc-match-message ()
+  "Highlight matched portions of the narrowed buffer."
+  (if (or erc-match-use-legacy-logic-p (null erc--parsed-response))
+      (erc-match--message-legacy)
+    (unless (or (and erc-match-exclude-server-buffer (erc--server-buffer-p))
+                (null (erc--check-msg-prop 'erc--cmd))
+                (erc--memq-msg-prop 'erc--skip 'match))
+      (erc-match--message))))
+
+(defun erc-match--message-legacy ()
   "Mark certain keywords in a region.
 Use this defun with `erc-insert-modify-hook'."
   ;; This needs some refactoring.
@@ -591,27 +925,25 @@ erc-log-matches
 Specify the match types which should be logged in the former,
 and deactivate/activate match logging in the latter.
 See `erc-log-match-format'."
-  (let  ((match-buffer-name (cdr (assq match-type
-				       erc-log-matches-types-alist)))
-	 (nick (nth 0 (erc-parse-user nickuserhost))))
-    (when (and
-	   (or (eq erc-log-matches-flag t)
-	       (and (eq erc-log-matches-flag 'away)
-		    (erc-away-time)))
-	   match-buffer-name)
-      (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))
-	    (insert line)))))))
+  (when-let*
+      ((erc-log-matches-flag)
+       (_ (or (eq erc-log-matches-flag t) (erc-away-time)))
+       (match-buffer-name (cdr (assq match-type erc-log-matches-types-alist)))
+       (line (format-spec
+              erc-log-match-format
+              (erc-compat--defer-format-spec-in-buffer
+               (?n . (or (erc--check-msg-prop 'erc--spkr)
+                         (erc-extract-nick nickuserhost)))
+               (?t . (format-time-string
+                      (or (bound-and-true-p erc-timestamp-format)
+                          "[%Y-%m-%d %H:%M] ")))
+               (?c erc-default-target)
+               (?m . message)
+               (?u . nickuserhost)))))
+    (with-current-buffer (erc-log-matches-make-buffer match-buffer-name)
+      (with-silent-modifications
+        (goto-char (point-max))
+        (insert line)))))
 
 (defun erc-log-matches-make-buffer (name)
   "Create or get a log-matches buffer named NAME and return it."
diff --git a/test/lisp/erc/erc-match-tests.el b/test/lisp/erc/erc-match-tests.el
index 178addeb604..6691f5506aa 100644
--- a/test/lisp/erc/erc-match-tests.el
+++ b/test/lisp/erc/erc-match-tests.el
@@ -251,8 +251,9 @@ erc-match-tests--assert-speaker-only-highlighted
 (defun erc-match-tests--perform (test)
   (erc-tests-common-make-server-buf)
   (setq erc-server-current-nick "tester")
-  (with-current-buffer (erc--open-target "#chan")
-    (funcall test))
+  (let (erc-match--opt-pat-cache)
+    (with-current-buffer (erc--open-target "#chan")
+      (funcall test)))
   (when noninteractive
     (erc-tests-common-kill-buffers)))
 
@@ -346,6 +347,77 @@ erc-match-message/dangerous-host/nick
   (let ((erc-dangerous-hosts (list "bob")))
     (erc-match-tests--hl-type-nick 'erc-dangerous-host-face)))
 
+(ert-deftest erc-match-message/pal/nick/legacy ()
+  (should (eq erc-pal-highlight-type 'nick))
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t)
+          (erc-pals (list "bob")))
+      (erc-match-tests--hl-type-nick 'erc-pal-face))))
+
+(ert-deftest erc-match-message/fool/nick/legacy ()
+  (should (eq erc-fool-highlight-type 'nick))
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t)
+          (erc-fools (list "bob")))
+      (erc-match-tests--hl-type-nick/mention 'erc-fool-face))))
+
+(ert-deftest erc-match-message/dangerous-host/nick/legacy ()
+  (should (eq erc-dangerous-host-highlight-type 'nick))
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t)
+          (erc-dangerous-hosts (list "bob")))
+      (erc-match-tests--hl-type-nick 'erc-dangerous-host-face))))
+
+;; Mentions are treated as keywords, even in the speaker portion.
+;; Contrast this with `erc-match-tests--hl-type-nick/mention', where the
+;; speakers are highlighted despite "mention" matches occurring in the
+;; message body.
+(defun erc-match-tests--hl-type-nick-or-mention (face)
+  (erc-match-tests--hl-type-nick
+   face
+   (lambda ()
+     (erc-tests-common-simulate-privmsg "alice" "bob: one bob ONE")
+     (erc-tests-common-simulate-privmsg "alice" "bob, two")
+     (erc-tests-common-simulate-privmsg "alice" "three, bob.")
+
+     (search-forward "<alice> bob: one")
+     (goto-char (pos-bol))
+     (erc-match-tests--assert-face-absent face "bob: one")
+     (erc-match-tests--assert-face-present face ": one ")
+     (erc-match-tests--assert-face-absent face "bob ONE")
+     (erc-match-tests--assert-face-present face " ONE")
+     (erc-match-tests--assert-face-absent face (pos-eol))
+
+     (search-forward "<alice> bob, two")
+     (goto-char (pos-bol))
+     (erc-match-tests--assert-face-absent face "bob, two")
+     (erc-match-tests--assert-face-present face ", two")
+     (erc-match-tests--assert-face-absent face (pos-eol))
+
+     (search-forward "<alice> three, bob.")
+     (goto-char (pos-bol))
+     (erc-match-tests--assert-face-absent face "bob.")
+     (erc-match-tests--assert-face-present face ".")
+     (erc-match-tests--assert-face-absent face (pos-eol)))))
+
+(ert-deftest erc-match-message/pal/nick-or-mention ()
+  (should (eq erc-pal-highlight-type 'nick))
+  (let ((erc-pal-highlight-type 'nick-or-mention)
+        (erc-pals (list "bob")))
+    (erc-match-tests--hl-type-nick-or-mention 'erc-pal-face)))
+
+(ert-deftest erc-match-message/fool/nick-or-mention ()
+  (should (eq erc-fool-highlight-type 'nick))
+  (let ((erc-fool-highlight-type 'nick-or-mention)
+        (erc-fools (list "bob")))
+    (erc-match-tests--hl-type-nick-or-mention 'erc-fool-face)))
+
+(ert-deftest erc-match-message/dangerous-host/nick-or-mention ()
+  (should (eq erc-dangerous-host-highlight-type 'nick))
+  (let ((erc-dangerous-host-highlight-type 'nick-or-mention)
+        (erc-dangerous-hosts (list "bob")))
+    (erc-match-tests--hl-type-nick-or-mention 'erc-dangerous-host-face)))
+
 (defun erc-match-tests--hl-type-message (face)
   (should (eq erc-current-nick-highlight-type 'keyword))
   (should (eq erc-keyword-highlight-type 'keyword))
@@ -411,6 +483,30 @@ erc-match-message/dangerous-host/message
         (erc-dangerous-host-highlight-type 'message))
     (erc-match-tests--hl-type-message 'erc-dangerous-host-face)))
 
+(ert-deftest erc-match-message/pal/message/legacy ()
+  (should (eq erc-pal-highlight-type 'nick))
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t)
+          (erc-pals (list "bob"))
+          (erc-pal-highlight-type 'message))
+      (erc-match-tests--hl-type-message 'erc-pal-face))))
+
+(ert-deftest erc-match-message/fool/message/legacy ()
+  (should (eq erc-fool-highlight-type 'nick))
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t)
+          (erc-fools (list "bob"))
+          (erc-fool-highlight-type 'message))
+      (erc-match-tests--hl-type-message 'erc-fool-face))))
+
+(ert-deftest erc-match-message/dangerous-host/message/legacy ()
+  (should (eq erc-dangerous-host-highlight-type 'nick))
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t)
+          (erc-dangerous-hosts (list "bob"))
+          (erc-dangerous-host-highlight-type 'message))
+      (erc-match-tests--hl-type-message 'erc-dangerous-host-face))))
+
 (defun erc-match-tests--hl-type-all (face)
   (should (eq erc-current-nick-highlight-type 'keyword))
   (should (eq erc-keyword-highlight-type 'keyword))
@@ -476,6 +572,30 @@ erc-match-message/dangerous-host/all
         (erc-dangerous-host-highlight-type 'all))
     (erc-match-tests--hl-type-all 'erc-dangerous-host-face)))
 
+(ert-deftest erc-match-message/pal/all/legacy ()
+  (should (eq erc-pal-highlight-type 'nick))
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t)
+          (erc-pals (list "bob"))
+          (erc-pal-highlight-type 'all))
+      (erc-match-tests--hl-type-all 'erc-pal-face))))
+
+(ert-deftest erc-match-message/fool/all/legacy ()
+  (should (eq erc-fool-highlight-type 'nick))
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t)
+          (erc-fools (list "bob"))
+          (erc-fool-highlight-type 'all))
+      (erc-match-tests--hl-type-all 'erc-fool-face))))
+
+(ert-deftest erc-match-message/dangerous-host/all/legacy ()
+  (should (eq erc-dangerous-host-highlight-type 'nick))
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t)
+          (erc-dangerous-hosts (list "bob"))
+          (erc-dangerous-host-highlight-type 'all))
+      (erc-match-tests--hl-type-all 'erc-dangerous-host-face))))
+
 (defun erc-match-tests--hl-type-nick-or-keyword ()
   (should (eq erc-current-nick-highlight-type 'keyword))
 
@@ -520,6 +640,11 @@ erc-match-tests--hl-type-nick-or-keyword
 (ert-deftest erc-match-message/current-nick/nick-or-keyword ()
   (erc-match-tests--hl-type-nick-or-keyword))
 
+(ert-deftest erc-match-message/current-nick/nick-or-keyword/legacy ()
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t))
+      (erc-match-tests--hl-type-nick-or-keyword))))
+
 (defun erc-match-tests--hl-type-keyword ()
   (should (eq erc-keyword-highlight-type 'keyword))
 
@@ -576,6 +701,11 @@ erc-match-tests--hl-type-keyword
 (ert-deftest erc-match-message/keyword/keyword ()
   (erc-match-tests--hl-type-keyword))
 
+(ert-deftest erc-match-message/keyword/keyword/legacy ()
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t))
+      (erc-match-tests--hl-type-keyword))))
+
 (defun erc-match-tests--log-matches ()
   (let ((erc-log-matches-flag t)
         (erc-timestamp-format "[@@TS@@]")
@@ -598,5 +728,121 @@ erc-match-tests--log-matches
 (ert-deftest erc-log-matches ()
   (erc-match-tests--log-matches))
 
+(ert-deftest erc-log-matches/legacy ()
+  (with-suppressed-warnings ((erc-match-use-legacy-logic-p obsolete))
+    (let ((erc-match-use-legacy-logic-p t))
+      (erc-match-tests--log-matches))))
+
+(ert-deftest erc-match--opt-pat-cache ()
+  (let ((erc-match--opt-pat-cache ()))
+    (let ((erc-keywords '("foo")))
+      (erc-match--keyword-p (erc-match-opt-keyword :body-beg 1
+                                                   :sender ""
+                                                   :command 'fake)))
+    (let ((erc-keywords '("bar")))
+      (erc-match--keyword-p (erc-match-opt-keyword :body-beg 1
+                                                   :sender ""
+                                                   :command 'fake)))
+    (let ((erc-fools '("baz")))
+      (erc-match--user-nuh-or-mention-p (erc-match-opt-fool :body-beg 1
+                                                            :sender ""
+                                                            :command 'fake)))
+    (should (equal erc-match--opt-pat-cache
+                   '((erc-match--opt-pat-addr-end-make
+                      (("baz") . "\\s. \\(baz\\)\\s."))
+                     (erc-match--opt-pat-addr-beg-make
+                      (("baz") . "\\<\\(baz\\)[:,] "))
+                     (erc-match--opt-pat-make
+                      (("baz") . "baz"))
+                     (erc-match--opt-pat-kw-make
+                      (("bar") . "bar") (("foo") . "foo")))))))
+
+;; This demos bare-bones usage of the `erc-match' API that implicitly
+;; opts out of the traditional options and "parts"-based mechanism.  The
+;; user does not have to provide a `:part' keyword because they've
+;; overridden the `:handler', meaning `erc-match-highlight-by-part'
+;; never runs.  This is somewhat analogous but ultimately orthogonal to
+;; `erc-text-matched-hook' not running because that happens on account
+;; of the user not specifying a `:category' field.
+(ert-deftest erc-match-functions/api/non-parts-based ()
+  (let* ((results ())
+         (erc-text-matched-hook (lambda (&rest r) (push r results)))
+         (erc-match-functions
+          (list
+           (lambda (&rest plist)
+             ;; Doing everything in `:pred' would also work if
+             ;; specifying `ignore' for `:handler'.  And you wouldn't
+             ;; even need to return non-nil on matches.
+             (apply #'erc-match
+                    :predicate (lambda (_) (search-forward "alice" nil t))
+                    :handler (lambda (_) (push (match-string 0) results))
+                    plist)))))
+
+    (erc-match-tests--perform
+     (lambda ()
+       (erc-tests-common-add-cmem "bob")
+       (erc-tests-common-add-cmem "Alice")
+       (erc-tests-common-simulate-line
+        ":irc.foonet.org 353 tester = #chan :bob tester Alice")
+       (erc-tests-common-simulate-line
+        ":irc.foonet.org 366 tester #chan :End of NAMES list")
+       (erc-tests-common-simulate-privmsg "bob" "hi ALICE")
+       (goto-char (point-min))
+
+       (should (equal results '("ALICE" "Alice")))))))
+
+;; This one piggybacks on infrastructure supporting the traditional
+;; `match' interface.
+(ert-deftest erc-match-functions/api/parts-based ()
+  (let* ((results ())
+         (bodies ())
+         (erc-text-matched-hook (lambda (&rest r)
+                                  (push r results)))
+         (erc-match-functions ()))
+
+    (erc-match-tests--perform
+     (lambda ()
+
+       ;; Use local setter for no particular reason.
+       (add-hook 'erc-match-functions
+                 (lambda (&rest plist)
+                   (apply #'erc-match-traditional
+                          :category 'keyword
+                          :part 'keyword
+                          :data '("alice")
+                          :face 'error
+                          :predicate (lambda (_)
+                                       (search-forward "alice" nil t))
+                          ;; Override `erc-match-highlight'.
+                          :handler (lambda (m)
+                                     (push (erc-match-get-message-body m)
+                                           bodies)
+                                     (erc-match-highlight m))
+                          plist))
+                 0 t)
+
+       (erc-tests-common-add-cmem "bob")
+       (erc-tests-common-add-cmem "Alice")
+       (erc-tests-common-simulate-line
+        ":irc.foonet.org 353 tester = #chan :Alice bob tester")
+       (erc-tests-common-simulate-line
+        ":irc.foonet.org 366 tester #chan :End of NAMES list")
+       (erc-tests-common-simulate-privmsg "bob" "hi ALICE")
+       (goto-char (point-min))
+
+       (search-forward "*** Users on #chan:")
+       (erc-match-tests--assert-face-absent 'error "Alice")
+       (erc-match-tests--assert-face-present 'error " bob")
+       (erc-match-tests--assert-face-absent 'error (pos-eol))
+
+       ;; Prefixes detected for notices and spoken messages.
+       (should (equal bodies
+                      '("hi ALICE"
+                        "Users on #chan: Alice bob tester")))
+
+       (should (equal results
+                      '(( keyword "[email protected]" "hi ALICE\n")
+                        ( keyword "Server:353"
+                          "*** Users on #chan: Alice bob tester\n"))))))))
 
 ;;; erc-match-tests.el ends here
diff --git a/test/lisp/erc/erc-scenarios-match-api.el b/test/lisp/erc/erc-scenarios-match-api.el
new file mode 100644
index 00000000000..0a8afbf33f2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-match-api.el
@@ -0,0 +1,127 @@
+;;; erc-scenarios-match-api.el --- `erc-match-functions' scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2026 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs 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.
+
+;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-match)
+(require 'erc-stamp)
+
+(defun erc-scenarios-match-api--test-fn (&rest plist)
+  (apply #'erc-match
+         :predicate #'always
+         :handler (lambda (m)
+                    (let ((body (erc-match-get-message-body m))
+                          (cmd (format (if (numberp (erc-match-command m))
+                                           "%03i"
+                                         "%s")
+                                       (erc-match-command m)))
+                          (nick (or (erc-match-nick m) "?")))
+                      (with-current-buffer "*erc-match test matches*"
+                        (save-excursion
+                          (goto-char (point-max))
+                          (insert cmd " " nick ": " body "\n")))))
+         plist))
+
+(defun erc-scenarios-match-api--custom-match-functions ()
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "match/functions")
+       (dumb-server (erc-d-run "localhost" t 'custom))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-server-flood-penalty 0.1)
+       (erc-autojoin-channels-alist '((foonet "#chan")))
+       (match-buffer (get-buffer-create "*erc-match test matches*"))
+       (erc-match-functions
+        (cons #'erc-scenarios-match-api--test-fn erc-match-functions))
+       ;; Shadow this date stamp instead of mocking the time function.
+       ;; The version numbers in the QUIT message don't need this
+       ;; because they're printed verbatim from the server response.
+       (erc-message-english-s329 "%c was created on @@DATESTAMP@@"))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :full-name "tester"
+                                :user "tester"
+                                :nick "tester")
+        (funcall expect 5 "debug")))
+
+    (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+      (funcall expect 10 "<alice> There were none principal")
+      (erc-scenarios-common-say "/query bob"))
+
+    (with-current-buffer "bob"
+      (erc-scenarios-common-say "hi")
+      (funcall expect 10 "<bob> As much as ever Coriolanus did"))
+
+    (with-current-buffer "foonet"
+      (erc-scenarios-common-say "/msg NickServ help identify")
+      (funcall expect 10 "End of NickServ HELP"))
+
+    (with-current-buffer "#chan"
+      (funcall expect 10 "<alice> bob: Thus men may grow wiser")
+      (erc-scenarios-common-say "/quit")
+      (funcall expect 10 "quit: Quit:"))
+
+    (with-current-buffer "foonet"
+      (funcall expect 10 "==> ERROR"))
+
+    (with-temp-buffer
+      (insert-file-contents
+       (expand-file-name "match/functions/custom-match-log"
+                         (ert-resource-directory)))
+      (let ((expected (buffer-string)))
+        (with-current-buffer "*erc-match test matches*"
+          (should (equal expected (buffer-string))))))))
+
+;; These tests primarily ensure that the various fields of the
+;; `erc-match' object, like `spkr-beg', as well as associated utilities,
+;; like `erc-match-get-message-body', work as expected.  It defines a
+;; custom `erc-match-functions' member that prints a summary of every
+;; displayed message to its own buffer.  That buffer's contents appear
+;; in the file resources/match/functions/custom-match-log.
+
+(ert-deftest erc-scenarios-match-api--custom-functions/basic ()
+  :tags '(:expensive-test)
+  (erc-scenarios-match-api--custom-match-functions))
+
+(ert-deftest erc-scenarios-match-api--custom-functions/fill-wrap ()
+  :tags '(:expensive-test)
+  (let ((erc-modules (cons 'fill-wrap erc-modules)))
+    (erc-scenarios-match-api--custom-match-functions)))
+
+(ert-deftest erc-scenarios-match-api--custom-functions/left-stamps ()
+  :tags '(:expensive-test)
+  (let ((erc-insert-timestamp-function #'erc-insert-timestamp-left)
+        (erc-timestamp-only-if-changed-flag nil))
+    (erc-scenarios-match-api--custom-match-functions)))
+
+(ert-deftest erc-scenarios-match-api--custom-functions/left-stamps/fill-wrap ()
+  :tags '(:expensive-test)
+  (let ((erc-insert-timestamp-function #'erc-insert-timestamp-left)
+        (erc-timestamp-only-if-changed-flag nil)
+        (erc-modules (cons 'fill-wrap erc-modules)))
+    (erc-scenarios-match-api--custom-match-functions)))
+
+;;; erc-scenarios-match-api.el ends here
diff --git a/test/lisp/erc/resources/match/functions/custom-match-log b/test/lisp/erc/resources/match/functions/custom-match-log
new file mode 100644
index 00000000000..4b07e4c4fe6
--- /dev/null
+++ b/test/lisp/erc/resources/match/functions/custom-match-log
@@ -0,0 +1,39 @@
+001 ?: Welcome to the foonet IRC Network tester
+002 ?: Your host is irc.foonet.org, running version ergo-v2.17.0
+003 ?: This server was created Sat, 25 Apr 2026 06:08:20 UTC
+004 ?: irc.foonet.org ergo-v2.17.0 BERTZios CEIMRUabefhiklmnoqstuv
+005 ?: AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX are supported by this server
+005 ?: KICKLEN=390 MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 MSGREFTYPES=msgid,timestamp NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ SAFELIST SAFERATE STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 are supported by this server
+005 ?: TOPICLEN=390 UTF8ONLY WHOX draft/CHATHISTORY=25 are supported by this server
+251 ?: There are 0 users and 4 invisible on 1 server(s)
+252 ?: 0 operator(s) online
+253 ?: 0 unknown connection(s)
+254 ?: 2 channels formed
+255 ?: I have 4 clients and 0 servers
+265 ?: Current local users 4, max 4
+266 ?: Current global users 4, max 4
+422 ?: MOTD File is missing
+221 ?: User modes for tester: +i
+NOTICE ?: This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.
+221 ?: User modes for tester: +i
+JOIN tester: You have joined channel #chan
+353 ?: Users on #chan: @fsbot alice bob tester
+PRIVMSG alice: tester, welcome!
+PRIVMSG bob: tester, welcome!
+PRIVMSG bob: alice: No tearing, lady; I perceive you know it.
+324 ?: #chan modes: +Cnt
+329 ?: #chan was created on @@DATESTAMP@@
+PRIVMSG alice: There were none principal; they were all like one another as half-pence are; every one fault seeming monstrous till his fellow fault came to match it.
+PRIVMSG bob: As much as ever Coriolanus did.
+NOTICE NickServ: *** NickServ HELP ***
+NOTICE NickServ: Syntax: IDENTIFY <username> [password]
+NOTICE NickServ: IDENTIFY lets you login to the given username using either password auth, or
+NOTICE NickServ: certfp (your client certificate) if a password is not given.
+NOTICE NickServ: *** End of NickServ HELP ***
+PRIVMSG bob: alice: Deceive me not now, Navarre is infected.
+PRIVMSG bob: Bear me to prison, where I am committed.
+PRIVMSG alice: Wisely and slow; they stumble that run fast.
+PRIVMSG alice: bob: Thus men may grow wiser every day: it is the first time that ever I heard breaking of ribs was sport for ladies.
+QUIT tester: tester ([email protected]) has quit: Quit: ERC 5.6.2-git (IRC client for GNU Emacs 31.0.50)
+QUIT tester: tester ([email protected]) has quit: Quit: ERC 5.6.2-git (IRC client for GNU Emacs 31.0.50)
+ERROR ?: ERROR from 127.0.0.1: Quit: ERC 5.6.2-git (IRC client for GNU Emacs 31.0.50)
diff --git a/test/lisp/erc/resources/match/functions/custom.eld b/test/lisp/erc/resources/match/functions/custom.eld
new file mode 100644
index 00000000000..073c1e403bb
--- /dev/null
+++ b/test/lisp/erc/resources/match/functions/custom.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK tester"))
+((user 10 "USER tester 0 * :tester")
+ (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.00 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.17.0")
+ (0.00 ":irc.foonet.org 003 tester :This server was created Sat, 25 Apr 2026 06:08:20 UTC")
+ (0.00 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.17.0 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server")
+ (0.00 ":irc.foonet.org 005 tester KICKLEN=390 MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 MSGREFTYPES=msgid,timestamp NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ SAFELIST SAFERATE STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 :are supported by this server")
+ (0.00 ":irc.foonet.org 005 tester TOPICLEN=390 UTF8ONLY WHOX draft/CHATHISTORY=25 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 tester :There are 0 users and 4 invisible on 1 server(s)")
+ (0.00 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.00 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.10 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0.00 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0.00 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0.00 ":irc.foonet.org 422 tester :MOTD File is missing")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
+((mode 10 "MODE tester +i"))
+((join 10 "JOIN #chan")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":[email protected] JOIN #chan")
+ (0.01 ":irc.foonet.org 353 tester = #chan :@fsbot alice bob tester")
+ (0.00 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.01 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0.00 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0.03 ":[email protected] PRIVMSG #chan :alice: No tearing, lady; I perceive you know it."))
+((mode-chan 10 "MODE #chan")
+ (0.00 ":irc.foonet.org 324 tester #chan +Cnt")
+ (0.00 ":irc.foonet.org 329 tester #chan 1777097304")
+ (0.01 ":[email protected] PRIVMSG #chan :There were none principal; they were all like one another as half-pence are; every one fault seeming monstrous till his fellow fault came to match it."))
+((privmsg-bob 10 "PRIVMSG bob :hi")
+ (0.03 ":[email protected] PRIVMSG tester :As much as ever Coriolanus did."))
+((privmsg-nickserv 10 "PRIVMSG NickServ :help identify")
+ (0.02 ":NickServ!NickServ@localhost NOTICE tester :*** \2NickServ HELP\2 ***")
+ (0.00 ":NickServ!NickServ@localhost NOTICE tester :Syntax: \2IDENTIFY <username> [password]\2")
+ (0.00 ":NickServ!NickServ@localhost NOTICE tester :IDENTIFY lets you login to the given username using either password auth, or")
+ (0.00 ":NickServ!NickServ@localhost NOTICE tester :certfp (your client certificate) if a password is not given.")
+ (0.00 ":NickServ!NickServ@localhost NOTICE tester :*** \2End of NickServ HELP\2 ***")
+ ;;
+ (0.07 ":[email protected] PRIVMSG #chan :alice: Deceive me not now, Navarre is infected.")
+ (0.04 ":[email protected] PRIVMSG #chan :Bear me to prison, where I am committed.")
+ (0.07 ":[email protected] PRIVMSG #chan :Wisely and slow; they stumble that run fast.")
+ (0.03 ":[email protected] PRIVMSG #chan :bob: Thus men may grow wiser every day: it is the first time that ever I heard breaking of ribs was sport for ladies."))
+((quit 10 "QUIT :")
+ (0.01 ":[email protected] QUIT :Quit: \2ERC\2 5.6.2-git (IRC client for GNU Emacs 31.0.50)")
+ (0.00 "ERROR :Quit: \2ERC\2 5.6.2-git (IRC client for GNU Emacs 31.0.50)"))
-- 
2.53.0


--=-=-=
Content-Type: text/x-patch
Content-Disposition: attachment;
 filename=0006-5.7-Use-erc-match-type-API-for-erc-desktop-notificat.patch

From 562186769eef1cde225548ff940ce20fb6fb4197 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sat, 12 Oct 2024 17:44:30 -0700
Subject: [PATCH 6/6] [5.7] Use erc-match-type API for
 erc-desktop-notifications

* etc/ERC-NEWS: New entries for the `erc-match-type' API and options
`erc-notifications-focused-context' and
`erc-desktop-notifications-skip-predicates'.
* lisp/erc/erc-desktop-notifications.el
(erc-notifications-focused-contexts): New option.
(erc-desktop-notifications-skip-predicates): New option.
(erc-notifications-notify): Address ancient comment regarding PRIVP
parameter possibly being unneeded when the current target matches the
nick.
(erc-notifications-PRIVMSG): Deprecate.
(erc-desktop-notifications-skip-untracked)
(erc-desktop-notifications-skip-fools): New functions
(erc-notifications-notify-on-match): Account for new options.
(erc-notifications-mode, erc-notifications-enable)
(erc-notifications-disable): Instead of the "PRIVMSG" response-handler
hook, use the `erc-match-type' API.
(erc-desktop-notifications--setup): New function
(erc-desktop-notifications-match-query-commands): New variable.
(erc-desktop-notifications--match-type-query): New struct type.
(erc-desktop-notifications--query-p): New function.
(erc-desktop-notification--query-notify): New function.
* test/lisp/erc/erc-desktop-notifications-tests.el: New file.
(Bug#73798)
---
 etc/ERC-NEWS                                  |  24 +++
 lisp/erc/erc-desktop-notifications.el         | 102 +++++++++++--
 .../erc/erc-desktop-notifications-tests.el    | 140 ++++++++++++++++++
 3 files changed, 252 insertions(+), 14 deletions(-)
 create mode 100644 test/lisp/erc/erc-desktop-notifications-tests.el

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index de27b8f07e7..041a05a117b 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -14,6 +14,30 @@ GNU Emacs since Emacs version 22.1.
 
 * Changes in ERC 5.7
 
+** An extensibility focused 'match' API.
+Users have often expressed frustration over ERC's lack of a simple API
+for matching, highlighting, and filtering based on a message's content
+and metadata, like the sender or associated IRC command.  While it's
+true that discussions have been ongoing for a more powerful message
+formatting and construction API that will hopefully one day offer access
+to the various parts of a message before they're assembled, users will
+be needing something practical and effective in the interim.  Enter the
+'erc-match-type' API, which is based on a simple hook-like handler
+system.  You subscribe by enrolling a function that takes a special
+'erc-match-type' object with useful fields to help with matching,
+filtering, and applying faces.  See Info node 'Match API' to find out
+more.
+
+** Exercise more control in skipping certain desktop notifications.
+Elect to skip select notifications that ERC would otherwise emit with
+new hook option 'erc-desktop-notifications-skip-predicates'.  Its
+default members skip messages by fools and those in untracked buffers.
+
+** Opt out of desktop notifications from the active buffer.
+Option 'erc-desktop-notifications-focused-contexts' can help spare you
+from seeing desktop alerts for messages you're reading or those inserted
+while you're typing.
+
 ** Changes in the library API.
 
 *** Local modules activate in preferred order instead of in reverse.
diff --git a/lisp/erc/erc-desktop-notifications.el b/lisp/erc/erc-desktop-notifications.el
index e4a62c614a4..cc96961ab22 100644
--- a/lisp/erc/erc-desktop-notifications.el
+++ b/lisp/erc/erc-desktop-notifications.el
@@ -47,6 +47,19 @@ erc-notifications-icon
   "Icon to use for notification."
   :type '(choice (const :tag "No icon" nil) file))
 
+(defcustom erc-desktop-notifications-focused-contexts '(query mention)
+  "Where to notify even if a match appears in the selected window."
+  :package-version '(ERC . "5.7") ; FIXME sync on release
+  :type '(set (const query) (const mention)))
+
+(defcustom erc-desktop-notifications-skip-predicates
+  '(erc-desktop-notifications-skip-untracked
+    erc-desktop-notifications-skip-fools)
+  "Abnormal hook whose members return non-nil to suppress notification.
+Called in match buffer with a matching `erc-match-user' object."
+  :package-version '(ERC . "5.7") ; FIXME sync on release
+  :type 'hook)
+
 (defcustom erc-notifications-bus :session
   "D-Bus bus to use for notification."
   :version "25.1"
@@ -60,16 +73,18 @@ dbus-debug
 (defun erc-notifications-notify (nick msg &optional privp)
   "Notify that NICK send some MSG, where PRIVP should be non-nil for PRIVMSGs.
 This will replace the last notification sent with this function."
-  ;; TODO: can we do this without PRIVP? (by "fixing" ERC's not
-  ;; setting the current buffer to the existing query buffer)
   (dbus-ignore-errors
     (setq erc-notifications-last-notification
-          (let* ((channel (if privp (erc-get-buffer nick) (current-buffer)))
-                 (title (format "%s in %s"
-                                (erc-compat--xml-escape-string nick t)
-                                channel))
-                 (body (erc-compat--xml-escape-string (erc-controls-strip msg)
-                                                      t)))
+          (let* ((channel (or (and privp (not (equal nick (erc-target)))
+                                   (erc-get-buffer nick))
+                              (current-buffer)))
+                 (title (if (or privp (equal nick (erc-target)))
+                            (erc-compat--xml-escape-string nick t)
+                          (format "%s in %s"
+                                  (erc-compat--xml-escape-string nick t)
+                                  channel)))
+                 (body (erc-compat--xml-escape-string
+                        (erc-controls-strip msg) t)))
             (funcall (cond ((featurep 'android)
                             #'android-notifications-notify)
                            ((featurep 'haiku)
@@ -85,6 +100,7 @@ erc-notifications-notify
                                   (pop-to-buffer channel)))))))
 
 (defun erc-notifications-PRIVMSG (_proc parsed)
+  (declare (obsolete "switched to `erc-match-type' API" "31.1"))
   (let ((nick (car (erc-parse-user (erc-response.sender parsed))))
         (target (car (erc-response.command-args parsed)))
         (msg (erc-response.contents parsed)))
@@ -96,23 +112,81 @@ erc-notifications-PRIVMSG
   ;; Return nil to continue processing by ERC
   nil)
 
+(defun erc-desktop-notifications-skip-untracked (_)
+  "Return non-nil when current buffer's target appears in `erc-track-exclude'."
+  (and (boundp 'erc-track-exclude) (member (erc-target) erc-track-exclude)))
+
+(defun erc-desktop-notifications-skip-fools (_)
+  "Return non-nil if the current message has a \"match type\" of `fool'."
+  (erc-match-get-match 'erc-match-opt-fool))
+
 (defun erc-notifications-notify-on-match (match-type nickuserhost msg)
   (when (eq match-type 'current-nick)
     (let ((nick (nth 0 (erc-parse-user nickuserhost))))
       (unless (or (string-match-p "^Server:" nick)
-                  (when (boundp 'erc-track-exclude)
-                    (member nick erc-track-exclude)))
+                  (and (eq (current-buffer) (window-buffer))
+                       (frame-focus-state) ; t or unknown
+                       (not (memq 'mention
+                                  erc-desktop-notifications-focused-contexts)))
+                  (run-hook-with-args-until-success
+                   'erc-desktop-notifications-skip-predicates
+                   erc-match-highlight-matched))
         (erc-notifications-notify nick msg)))))
 
 ;;;###autoload(autoload 'erc-notifications-mode "erc-desktop-notifications" "" t)
 (define-erc-module notifications nil
   "Send notifications on private message reception and mentions."
   ;; Enable
-  ((add-hook 'erc-server-PRIVMSG-functions #'erc-notifications-PRIVMSG)
-   (add-hook 'erc-text-matched-hook #'erc-notifications-notify-on-match))
+  ((unless erc--updating-modules-p
+     (erc-buffer-do #'erc-desktop-notifications--setup))
+   (add-hook 'erc-mode-hook #'erc-desktop-notifications--setup))
   ;; Disable
-  ((remove-hook 'erc-server-PRIVMSG-functions #'erc-notifications-PRIVMSG)
-   (remove-hook 'erc-text-matched-hook #'erc-notifications-notify-on-match)))
+  ((erc-buffer-do #'erc-desktop-notifications--setup)
+   (remove-hook 'erc-mode-hook #'erc-desktop-notifications--setup)))
+
+(defun erc-desktop-notifications--setup ()
+  (if erc-notifications-mode
+      (progn
+        (add-hook 'erc-match-functions
+                ;; Run after default value to detect fools.
+                  #'erc-desktop-notifications--match-type-query 20 t)
+        (add-hook 'erc-text-matched-hook #'erc-notifications-notify-on-match
+                  20 t))
+    (remove-hook 'erc-match-functions
+                 #'erc-desktop-notifications--match-type-query t)
+    (remove-hook 'erc-text-matched-hook
+                 #'erc-notifications-notify-on-match t)))
+
+(defvar erc-desktop-notifications-match-query-commands '(PRIVMSG)
+  "IRC commands considered in query buffers for notification.
+Omits \"NOTICE\"s by default because they're typically reserved for bots
+and services that you interact with directly.")
+
+(cl-defstruct (erc-desktop-notifications--match-type-query
+               (:constructor erc-desktop-notifications--match-type-query)
+               (:include erc-match-user
+                         (category nil)
+                         (data erc-desktop-notifications-match-query-commands)
+                         (predicate #'erc-desktop-notifications--query-p)
+                         (handler #'erc-desktop-notifications--query-notify)))
+  "Notification match type for queries.")
+
+(defun erc-desktop-notifications--query-p (match)
+  "Return non-nil if MATCH object describes a \"PRIVMSG\" query."
+  (and (erc-query-buffer-p)
+       (or (memq 'query erc-desktop-notifications-focused-contexts)
+           (null (frame-focus-state))
+           (not (eq (current-buffer) (window-buffer))))
+       (memq (erc-match-command match) (erc-match-user-data match))
+       (always (cl-assert (erc-match-nick match)))
+       (not (run-hook-with-args-until-success
+             'erc-desktop-notifications-skip-predicates match))))
+
+(defun erc-desktop-notifications--query-notify (match)
+  ;; No need to pass argument PRIVP because current buffer is correct.
+  (erc-notifications-notify (erc-target)
+                            (erc-match-get-message-body match)))
+
 
 (provide 'erc-desktop-notifications)
 
diff --git a/test/lisp/erc/erc-desktop-notifications-tests.el b/test/lisp/erc/erc-desktop-notifications-tests.el
new file mode 100644
index 00000000000..e1b5aa2ea13
--- /dev/null
+++ b/test/lisp/erc/erc-desktop-notifications-tests.el
@@ -0,0 +1,140 @@
+;;; erc-desktop-notifications-tests.el --- Notifications tests  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2025 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs 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.
+;;
+;; GNU Emacs 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 GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;; Code:
+(require 'erc-desktop-notifications)
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-tests-common)))
+
+(defun erc-desktop-notifications-tests--perform (test)
+  (erc-tests-common-make-server-buf)
+  (erc-notifications-mode +1)
+  (setq erc-server-current-nick "tester")
+
+  (unwind-protect
+      (cl-letf* ((calls nil)
+                 ((frame-parameter nil 'last-focus-update)
+                  t)
+                 ((symbol-function 'erc-notifications-notify)
+                  (lambda (&rest r) (push r calls))))
+        (with-current-buffer (erc--open-target "#chan")
+          (funcall test (lambda () (prog1 calls (setq calls nil))))))
+
+    (when noninteractive
+      (erc-notifications-mode -1)
+      (erc-tests-common-kill-buffers))))
+
+(defun erc-desktop-notifications-tests--populate-chan (test)
+  (erc-desktop-notifications-tests--perform
+   (lambda (check)
+     (erc-tests-common-add-cmem "bob")
+     (erc-tests-common-add-cmem "alice")
+
+     (erc-tests-common-simulate-line
+      ":irc.foonet.org 353 tester = #chan :alice bob tester")
+     (erc-tests-common-simulate-line
+      ":irc.foonet.org 366 tester #chan :End of NAMES list")
+     (erc-tests-common-simulate-privmsg "bob" "hi tester")
+
+     (should (equal (current-buffer) (get-buffer "#chan")))
+     (should (not (eq (current-buffer) (window-buffer)))) ; *ert* or *scratch*
+     (funcall test check))))
+
+(ert-deftest erc-desktop-notifications-focused-contexts/default ()
+  (should (equal erc-desktop-notifications-focused-contexts '(query mention)))
+
+  (erc-desktop-notifications-tests--populate-chan
+   (lambda (check)
+
+     ;; A private query triggers a notification.
+     (erc-tests-common-simulate-line ":[email protected] PRIVMSG tester yo")
+     (should (eq (current-buffer) (get-buffer "bob")))
+
+     ;; A NOTICE command doesn't trigger a notification because it's
+     ;; absent from `erc-desktop-notifications-match-query-commands'.
+     (erc-tests-common-simulate-line ":irc.foonet.org NOTICE tester nope")
+
+     (should (equal (funcall check)
+                    '(("bob" "yo")
+                      ("bob" "hi tester\n"))))
+
+     ;; Setting the window to the buffer where insertions are happening
+     ;; makes no difference: notifications are still sent.
+     (erc-tests-common-simulate-line ":[email protected] PRIVMSG tester ho")
+
+     (set-window-buffer nil (set-buffer "#chan"))
+     (erc-tests-common-simulate-privmsg "alice" "hi tester")
+
+     (should (equal (funcall check)
+                    '(("alice" "hi tester\n")
+                      ("bob" "ho")))))))
+
+(ert-deftest erc-desktop-notifications-focused-contexts/unselected ()
+  (should (equal erc-desktop-notifications-focused-contexts '(query mention)))
+
+  (let ((erc-desktop-notifications-focused-contexts))
+
+    (erc-desktop-notifications-tests--populate-chan
+     (lambda (check)
+       (should (equal (funcall check) '(("bob" "hi tester\n"))))
+
+       ;; Buffer #chan is current and displayed in the selected window,
+       ;; so no notification is sent.
+       (set-window-buffer nil "#chan") ; #chan
+       (erc-tests-common-simulate-privmsg "alice" "hi tester")
+
+       ;; A new query comes in for a buffer that doesn't exist.  The
+       ;; option `erc-receive-query-display' tells ERC to switch to that
+       ;; buffer and show it before insertion.  Therefore, no
+       ;; notification is sent.
+       (let ((erc-receive-query-display 'buffer))
+         (erc-tests-common-simulate-line
+          ":[email protected] PRIVMSG tester yo"))
+
+       (should-not (funcall check))))))
+
+(ert-deftest erc-desktop-notifications-skip-predicates/fools ()
+  (erc-desktop-notifications-tests--populate-chan
+   (lambda (check)
+
+     ;; A private query triggers a notification.
+     (erc-tests-common-simulate-line ":[email protected] PRIVMSG tester yo")
+     (should (eq (current-buffer) (get-buffer "bob")))
+
+     (should (equal (funcall check)
+                    '(("bob" "yo")
+                      ("bob" "hi tester\n"))))
+
+     (let ((erc-fools '("bob")))
+
+       ;; A query from is suppressed if bob is a fool.
+       (erc-tests-common-simulate-line ":[email protected] PRIVMSG tester ho")
+       (should-not (funcall check))
+
+       ;; A mention from bob is suppressed if bob is a fool.
+       (with-current-buffer "#chan"
+         (erc-tests-common-simulate-privmsg "bob" "hi tester")
+         (erc-tests-common-simulate-privmsg "alice" "hi tester")
+         (should (equal (funcall check) '(("alice" "hi tester\n")))))))))
+
+;;; erc-desktop-notifications-tests.el ends here
-- 
2.53.0


--=-=-=--