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

From: "J.P." <[email-masked]>
Date: Thu, 31 Oct 2024 22:22:41 -0700

"J.P." <[email-masked]> writes:

>      For a detailed example showing how to use this API for more involved
>   matching that doesn't involve highlighting, see the ‘notifications’
>   module, which lives in ‘erc-desktop-notifications.el’.  Ignore the parts
>   that involve adapting the global setup (and teardown) business to a
>   buffer-local context.  Since your module is declared ‘local’, as per the
>   modern convention, you won't be needing such code, so feel free to use
>   utility functions like ‘erc-match-add-local-type’ directly in your
>   module's definition.

Actually, I'm not so sure it's wise to hold up the `notifications'
module as a shining example of how to use this API. That's because it
currently suffers from some potentially confusing quirks, some of which
I'd like to address using internal functions that aren't really meant
for third parties.

>   Here, the user leverages a handy subtype of ‘erc-match’, called
>   ‘erc-match-opt-keyword’, which actually descends directly from another,
>   intermediate ‘erc-match’ type:
>
>    -- Struct: erc-match-traditional category face data part
>
>        Use this type or one of its descendants (see below) if you want
>        ‘erc-text-matched-hook’ to run alongside (after) the ‘handler’
>        slot's default highlighter, ‘erc-match-highlight’, on every match
>        for which the ‘category’ slot's value is non-‘nil’ (it becomes the
>        argument provided for the hook's MATCH-TYPE parameter).
>
>        Much more important, however, is ‘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
>        ‘nick’, ‘message’, ‘all’, ‘keyword’, ‘nick-or-keyword’, and
>        ‘nick-or-mention’.

This should probably also mention what the `data' slot does because it
features in both examples.

>      And, finally, here's a more elaborate, module-like example demoing
>   highlighting based on the ‘erc-match-traditional’ type:
>
>        ;; -*- lexical-binding: t; -*-
>
>        (require 'erc-match)
>        (require 'erc-button)
>
>        (defvar my-keywords
>          `((foonet ("#chan" ,(rx bow (or "foo" "bar" "baz") eow)))))
>
>        (defface my-keyword '((t (:underline (:color "tomato" :style wave))))
>          "My face.")
>
>        (defun my-get-keyword ()
>          (and-let* ((chans (alist-get (erc-network) my-keywords))
>                     ((cdr (assoc (erc-target) chans))))))
>
>        (cl-defstruct (my-match (:include erc-match-opt-keyword
>                                          (part 'keyword)

There's no need to override `part' here because `keyword' is already the
default.

>                                          (data (my-get-keyword))
>                                          (face 'my-keyword))
>                                (:constructor my-match)))
>
>        (setopt erc-match-types (add-to-list 'erc-match-types #'my-match))
>
>        (cl-defmethod erc-match-highlight-by-part ((instance my-match)
>                                                   (_ (eql keyword)))
>          "Highlight keywords by merging instead of clobbering."
>          (dolist (pat (my-match-data instance))
>            (goto-char (my-match-body-beg instance))
>            (while (re-search-forward pat nil t)
>              (erc-button-add-face (match-beginning 0) (match-end 0)
>                                   (my-match-face instance)))))
>

One thing that's possibly unclear about this example is why the
`my-match' definition overrides `face' and `data' only to use their
accessors in the body of the method. IOW, readers may wonder why it
doesn't just use these init forms directly inline. Moreover, while
`data' holds an opaque object that users are technically invited to
repurpose as needed, doing so only really makes sense if they're
subclassing a traditional options-based type. Indeed, for novel
purposes, it's much saner for users to just define their own slot and
use it for the usual reasons, e.g., to share processed data in various
stages of refinement. This example would do well to incorporate such
usage.

In any case, it's become clear to me that a practical demonstration of
this API might be necessary to fully grasp its facets and trade offs. To
that end, I offer the following full-featured demo module:

  https://emacs-erc.gitlab.io/bugs/archive/jabbycat.html

To try it out, you can start Emacs like

  $ HOME=$(mktemp -d) ./src/emacs

and then eval

  (require 'package)

  (push '("erc-bugs" . "https://emacs-erc.gitlab.io/bugs/archive/")
        package-archives)

  (package-install 'erc-73798)
  (sit-for 1) ;; see [1] below
  (package-install 'jabbycat)

  (setopt jabbycat-server "xmpp.myvps.example.org:5222"
          jabbycat-recipient "[email-masked]"
          jabbycat-username "[email-masked]"
          jabbycat-password "changeme"
          erc-jabbycat-match-patterns '((Libera.Chat ("#test" . ".")))
          erc-modules (add-to-list 'erc-modules 'jabbycat))

  (erc-tls)

If not already obvious, you either need to apply the latest patch set
from this bug or (as shown above) run the bug-specific version of ERC
with those changes pre-applied.

The source code is currently hosted at

  https://gitlab.com/emacs-erc/jabbycat

(though a possibly less objectionable mirror may be provided eventually).

Thanks.

[1] For some reason, without the pause, `url-insert-file-contents' gives

    signal(file-error ("https://fake.example.org/foo.tar" "No Data"))
    package--with-response-buffer-1("https://fake.example.org/"
                                    #f(compiled-function () ...>)
                                    :file "foo.tar" :async nil
                                    :error-function #<subr ...> :noerror nil)
    package-install-from-archive(#s(package-desc :name foo ... :signed nil))
    package-download-transaction((#s(package-desc ... :signed nil)))
    package-install(foo)