emacs-erc
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

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


From: J.P.
Subject: Re: bug#73798: 31.0.50; ERC 5.7: New extensibility focused match API
Date: Thu, 05 Dec 2024 22:54:02 -0800
User-agent: Gnus/5.13 (Gnus v5.13)

v4. Improve examples in manual. Add utility for match predicates and
handlers to retrieve prior (successful) match objects.

"J.P." <jp@neverwas.me> writes:

> In the imagined use cases described up thread, it likely won't be
> uncommon for a module to manage multiple match types, with multiple
> members in `erc-match-functions' and `erc-text-matched-hook'.
>
> Related predicates, handlers, and methods may therefore need to
> communicate across matches, for example, to know whether a match has
> already occurred for the current message. So it may well make sense to
> add another slot to the base type whose value is shared among all
> objects during matching and filtering. An alist probably makes the most
> sense for this.

I didn't end up adding such a slot, although I still think one would
likely be useful.

> If knowledge of prior matches turns out to be desirable and commonplace
> enough, we can keep and expose a record of all successful matches.
> (Doing this might further justify the current "split" design with its
> distinct predicate and handler phases.) One slight challenge here would
> be the necessity for some form of indirection to access such a record
> (because tacking on a list of prior match objects as a visible "has a"
> property of later objects would make printing them somewhat nasty). So,
> instead of another slot, we could maybe offer an object-retrieval
> utility keyed by constructor function.

This I did add, provisionally naming it `erc-match-get-match'.

The revised documentation follows. See attached diff for changes.

  File: erc.info, Node: Match API, Next: Options, Prev: Module Loading,
  Up: Advanced Usage

  5.6 Match API
  =============

  This section describes the low-level ‘match’ API introduced in ERC 5.7.
  For basic, options-oriented usage, please see the doc strings for option
  ‘erc-pal-highlight-type’ and friends in the ‘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 options all use the same
  foundational ‘erc-match’ API, which centers around a ‘cl-defstruct’
  “type” of the same name:

   -- Struct: erc-match predicate spkr-beg spkr-end body-beg sender nick
            command handler

       This is a ‘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
       ‘erc-match’ instance.  To use this interface, you add a
       “constructor”-like function to the hook ‘erc-match-functions’:

        -- User Option: erc-match-functions

            An abnormal hook for which each member accepts the parameters
            named above as an ‘&rest’-style plist and returns a new
            ‘erc-match’ instance.  A function can also be a traditional
            ‘cl-defstruct’-provided constructor belonging to a “subtype”
            you've defined.

       The only slot you definitely need to specify is ‘predicate’.  Both
       it and ‘handler’ are functions that take a single argument: the
       instance itself.  As its name implies, ‘predicate’ must return
       non-‘nil’ if ‘handler’, whose return value ERC ignores, should run.

       A few slots, like ‘spkr-beg’, ‘spkr-end’, and ‘nick’, may surprise
       you.  The first two are ‘nil’ for non-chat messages, like those
       displayed for ‘JOIN’ events.  The ‘nick’ slot can likewise be ‘nil’
       if the sender of the message is a domain-style host name, such as
       ‘irc.example.org’, which it often is for informational messages,
       like ‘*** #chan was created on 2023-12-26 00:36:42’.

       To locate the start of the just-inserted message, use ‘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
       ‘erc-match-get-message-body’:

        -- Function: erc-match-get-message-body match

            Takes an ‘erc-match’ instance and returns a string containing
            the message body, sans trailing newline and any leading
            speaker or decorative component, such as ‘erc-notice-prefix’.

  Although module authors may want to subclass this struct, everyday users
  can just instantiate it directly (it's “concrete”).  This is especially
  handy for one-off tasks or simple customizations in your ‘init.el’.  To
  do this, define a function that invokes its constructor:

       (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)))

  Here, the user could just as well shove the incrementer into the
  ‘predicate’ body, since ‘handler’ is set to ‘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-‘PRIVMSG’ messages via the slot ‘command’.

     In cases where you need a handler to only run when some other match
  type appearing earlier in ‘erc-match-functions’ has _not_ yielded a
  match, use:

   -- Function: erc-match-get-match constructor

       When called from a ‘handler’ or a ‘predicate’ body, this utility
       returns instances of prior ‘erc-match-functions’ that have already
       successfully matched the current message.  Use this for
       deduplication and to share data between match instances.

  For a detailed example of matching for non-highlighting purposes, see
  the ‘jabbycat’ demo module, available on ERC's dev-oriented package
  archive: <https://emacs-erc.gitlab.io/bugs/archive/jabbycat.html>.  If
  you're in a hurry, check out ‘erc-desktop-notifications.el’, which ships
  with ERC, but please 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 do things like add local members to
  ‘erc-match-functions’ in your module's definition.

  5.6.1 Highlighting
  ------------------

  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 ‘match’ to support its
  many high-level options.

       (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)

  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’.

       The complement to the ‘part’ slot is ‘data’, which holds the value
       of the module's option corresponding to the specific type.  For
       example, ERC initializes the ‘data’ slot for the
       ‘erc-match-opt-pal’ type with the value of ‘erc-pals’.

       The default handler, ‘erc-match-highlight’, does its work by
       deferring to a purpose-built “method” meant to handle ‘part’-based
       highlighting:

        -- Method on erc-match-traditional: erc-match-highlight-by-part
                 instance part

            You can override this method by “specializing” on any
            subclassed ‘erc-match-traditional’ type and/or non-reserved
            PART, such as one known only to your ‘init.el’ or (informally)
            associated with your package by its library “namespace”.

  You likely won't be needing these, but just for the record, other
  options-based types similar to ‘erc-match-opt-keyword’ include
  ‘erc-match-opt-current-nick’, ‘erc-match-opt-fool’, ‘erc-match-opt-pal’,
  and ‘erc-match-opt-dangerous-host’.  (If you're familiar with this
  module's user options, you'll notice some parallels here.)

  5.6.1.1 Complete 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 ‘erc-match’-derived type:

       ;;; 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 '("#org")
         "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)))
         'local)

       (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."
         (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

  Finally, here's a slightly more complete demo module: a superficial
  rewrite of ‘erc-colorize.el’ by Sylvain Rousseau
  <https://github.com/thisirs/erc-colorize.git>.

       ;;; 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))
         'local)

       (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."
         (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)))))

       (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

Attachment: 0000-v3-v4.diff
Description: Text Data

Attachment: 0001-5.7-Use-speaker-end-marker-in-ERC-insertion-hooks.patch
Description: Text Data

Attachment: 0002-5.7-Introduce-lower-level-erc-match-API.patch
Description: Text Data

Attachment: 0003-5.7-Use-erc-match-type-API-for-erc-desktop-notificat.patch
Description: Text Data


reply via email to

[Prev in Thread] Current Thread [Next in Thread]