|
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
0000-v3-v4.diff
Description: Text Data
0001-5.7-Use-speaker-end-marker-in-ERC-insertion-hooks.patch
Description: Text Data
0002-5.7-Introduce-lower-level-erc-match-API.patch
Description: Text Data
0003-5.7-Use-erc-match-type-API-for-erc-desktop-notificat.patch
Description: Text Data
[Prev in Thread] | Current Thread | [Next in Thread] |