aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel Ángel Moreno <mail@migalmoreno.com>2023-06-12 00:44:40 +0200
committerMiguel Ángel Moreno <mail@migalmoreno.com>2023-06-12 16:59:20 +0200
commit1f5ffe38e9dbf6c0b7c9a531a5f8f1a316253e63 (patch)
treef32e6e39a55238525a6276a180d1be1c43766590
parent210978214ba512ae8174cfd1889f0424592eecbc (diff)
doc: Update for new API
-rw-r--r--README200
-rw-r--r--README.md192
2 files changed, 151 insertions, 241 deletions
diff --git a/README b/README
index 9e35481..6ee0a2f 100644
--- a/README
+++ b/README
@@ -1,19 +1,17 @@
# -*- mode: org; org-html-head-include-default-style: nil; org-html-postamble: nil; -*-
#+OPTIONS: toc:nil num:nil
* nx-router
-=nx-router= is a URL routing extension for [[https://nyxt.atlas.engineer/][Nyxt]]. In short, it's an abstraction around Nyxt request resource handlers that introduces the concept of =route= objects.
+=nx-router= is a URL routing extension for [[https://nyxt.atlas.engineer/][Nyxt]]. In short, it's an abstraction around Nyxt resource handlers that uses =router= objects to make it more convenient to handle routes. See [[*Examples][Examples]] for a walk-through on how to set up routers.
-Over time, I found the built-in functionality in Nyxt for resource handling becomes difficult to maintain and reason about. I started by using plain handlers to achieve what I needed, but became frustrated with the amount of duplicate logic I found myself writing and the fact I had to come up with everything by myself when I wanted more complex logic.
+The main drive behind =nx-router= was I initially found built-in handlers difficult to reason and I soon became frustrated with the amount of duplicate logic I had to maintain. =nx-router= tries to tackle common needs in resource handling with a redirector, a site blocker, and a resource opener. You can think of it as a more batteries-included =url-dispatching-handler=.
-=nx-router= is built with the aim of finding a common ground to the most common needs in resource handling. It currently provides five routes: a general-purpose redirector, a general-purpose site blocker, a resource opener, a media toggler, and a =web-route= that ties all of these together for more complex requirements.
-
-If you're coming from standard browsers, you can think of =nx-router= as similar to existing solutions like [[https://github.com/einaregilsson/Redirector][Redirector]] and [[https://github.com/proginosko/LeechBlockNG][LeechblockNG]] put together and on steroids. If you're privacy-minded and already use Nyxt, you might have stumbled upon [[https://github.com/kssytsrk/nx-freestance-handler][nx-freestance-handler]], an extension akin to [[https://github.com/libredirect/libredirect][LibRedirect]] which redirects popular sites to their privacy-friendly front-ends. The problem I see with such extensions is they limit the user to a few predefined sites and create an implicit dependency on their maintainer to update the extension each time one of these goes down or changes its URL structure. See [[*Examples][Examples]] for a walk-through on how to set routes up, including how to replicate all of the =nx-freestance-handler= functionality.
+[[https://github.com/kssytsrk/nx-freestance-handler][nx-freestance-handler]] is a similar extension that redirects popular sites to privacy-friendly front-ends. However, this has the limitation that it only works with a few sites and makes you reliant on its maintainer updating the extension to add new handlers or modify existing ones.
** Installation
To install the extension, you need to download the source and place it in Nyxt's extensions path, given by the value of =nyxt-source-registry= (by default =~/.local/share/nyxt/extensions=).
#+begin_src sh
-git clone https://git.sr.ht/~conses/nx-router ~/.local/share/nyxt/extensions/nx-router
+git clone https://git.mianmoreno.com/nx-router ~/.local/share/nyxt/extensions/nx-router
#+end_src
The extension works with *Nyxt 3 onward* but it's encouraged to use it with the latest version of Nyxt master for the time being.
@@ -47,161 +45,152 @@ Where =router.lisp= is a custom file that should you should create relative to N
In addition, you should add the extension options, explained in the following section.
** Configuration
-You must begin by customizing =router-mode=, a mode that takes all of the user routes and applies them to the currently-running browsing session.
+This shows some example routers you'd set up inside the =router-mode= configuration class:
#+begin_src lisp
(define-configuration router:router-mode
- ((router:routes
+ ((router:routers
(list
(make-instance 'router:redirector
- :trigger (match-domain "example.org")
+ :route (match-domain "example.org")
:redirect-url "acme.org"
- :redirect-rule '(("/example" . (not "/" "wiki"))))
+ :redirect-rule '(("/example" . (not "/" "/wiki"))))
(make-instance 'router:blocker
- :trigger (match-hostname "www.wonka.inc")
- :blocklist t)
- (make-instance 'router:media-toggler
- :trigger (match-url "https://gekko.com/gallery")
+ :name 'wonka
+ :route (match-hostname "www.wonka.inc")
:instances-builder
(make-instance
'router:instances-builder
- :source "https://gekko.com/instances.json"
+ :source "https://www.wonka.inc/instances.json"
:builder (lambda (instances)
(json:decode-json-from-string
- instances))
- :media-p nil))
+ instances)))
+ :blocklist "/factory")
(make-instance 'router:opener
- :trigger (match-extension "mp3")
+ :name 'wonka
:resource "mpv --video=no ~a")
- (make-instance 'router:web-route
- :trigger (match-domain "stark.net")
+ (make-instance 'router:redirector
+ :name 'wonka
:redirect-rule "https://stark.net/products/(\\w+)/(.*)"
- :redirect-url (quri:uri "https://\\1.acme.org/\\2")
- :blocklist '(:path (:ends (not ".html")
- :contains "acme"))
- :resource (lambda (url)
- (add-to-csv url))
- :media-p nil)))))
+ :redirect-url (quri:uri "https://\\1.acme.org/\\2"))))))
#+end_src
-As you might notice from above, routes can go from really simple ones to ones with really complex logic baked in. As mentioned before, there are currently five predefined routes, with one of them being a combination of the other four.
+The first router simply redirects all requests that match the domain https://example.org to https://acme.org, and adds a redirect rule so that any path of https://example.org that doesn't match =/= or =/wiki= will get redirected to https://acme.org/example. The second router sets a blocklist for the =/factory= PCRE, and the rest of the routers are a composition of the second router, so they inherit its settings.
-All routes derive from a =route= parent class that holds the shared settings. It includes the following slots:
+All routers derive from a =router= parent class that holds common router settings:
-- =trigger= :: the trigger for =route= activation, akin to the predicates used in Nyxt =auto-rules=. One of =match-domain=, =match-host=, =match-regex=, =match-port=, a user-defined function, or a PCRE.
+- =route= :: the route to match for =router= activation, akin to the predicates used in Nyxt =auto-rules=. One of =match-domain=, =match-host=, =match-regex=, =match-port=, a user-defined function, or a PCRE.
- =instances-builder= :: this takes an =instances-builder= object, which in turn takes a source to retrieve instances from and a builder which assembles them into a list.
-- =toplevel-p= (default: =t=) :: whether the =route= is meant to process only top-level requests.
+- =toplevel-p= (default: =t=) :: whether the router is meant to process only top-level requests.
-The default settings for the above can be changed in whatever granularity level you desire.
-
-Due to the nature of WebkitGTK, click events are obscured, which means some requests might not get through, and =iframes= cannot be redirected. To alleviate this, you can control whether only top-level requests should be processed . For example, let's say you decide you want all requests to be processed, including non top-level ones, for all routes by default. You can do so by configuring the =route= user-class in your configuration.
+Do note the WebkitGTK renderer poses some limitations with requests. For example, some of them might not get processed because click events are obscured, and =iframes= cannot be redirected at all. To alleviate this, you can control whether only top-level requests should be processed . If you want all requests to be processed, including non top-level ones, for all routers by default you can configure the =router= user-class like this:
#+begin_src lisp
-(define-configuration router:route
+(define-configuration router:router
((router:toplevel-p nil)))
#+end_src
-If you'd like to process non top-level requests only for =redirector= and =opener= routes by default, you can configure them to do so.
+If you'd like to process non top-level requests only for =redirector= and =opener= routers by default, you can configure them like this:
#+begin_src lisp
(define-configuration (router:redirector router:opener)
((router:toplevel-p nil)))
#+end_src
-Finally, if you'd like to process non top-level requests only for a given instance of a =redirector= class, add the corresponding slot on the =route= instantiation.
+Finally, if you'd like to process non top-level requests only for a given instance of a =redirector= class, add this on router instantiation:
#+begin_src lisp
(make-instance 'router:redirector
- :trigger (match-domain "example.org")
+ :route (match-domain "example.org")
:redirect-url "acme.org"
:toplevel-p nil)
#+end_src
-=redirector= is a general-purpose redirect =route= that takes the following direct slots:
+=redirector= is a redirect router that takes the following direct slots:
-- =redirect-url= :: a string for a URL host to redirect to or a =quri:uri= object for a complete URL to redirect to. If =redirect-rule= or =trigger= is a PCRE, you can use this as the replacement string and include special sub-strings like those used in =ppcre:regex-replace= (e.g. =\N=, =\'=, =\`=, etc.).
+- =redirect-url= :: a string for a URL host to redirect to or a =quri:uri= object for a complete URL to redirect to. If =redirect-rule= or =route= is a PCRE, you can use this as the replacement string and include special sub-strings like those used in =ppcre:regex-replace= (e.g. =\N=, =\'=, =\`=, etc.).
-- =redirect-rule= :: a PCRE to match against the current URL or an association list of redirection rules for paths. If the latter, each entry is a cons of the form =REDIRECT-PATH . TRIGGER-PATHS=, where =TRIGGER-PATHS= is a list of paths from the trigger URL that will be redirected to =REDIRECT-PATH= in the =redirect-url= . To redirect all paths except =TRIGGER-PATHS= to =REDIRECT-PATH=, prefix this list with =not=.
+- =redirect-rule= :: a PCRE to match against the current URL or an association list of redirection rules for paths. If the latter, each entry is a cons of the form =REDIRECT . ROUTES=, where =ROUTES= is a list of paths from the =route= that will be redirected to =REDIRECT= in =redirect-url=. To redirect all paths except =ROUTES= to =REDIRECT=, prefix this list with =not=.
-- =original-url= :: takes either a string for the route's original host or a =quri:uri= object for the original complete URL. This is useful for storage purposes (bookmarks, history, etc.) so that the original URL is recorded instead of the redirect's URL.
+- =original-url= :: takes either a string for the router>'s original host or a =quri:uri= object for the original complete URL. This is useful for storage purposes (bookmarks, history, etc.) so that the original URL is recorded instead of the redirect's URL.
-=blocker= is a general-purpose block =route= that takes the following direct slots:
+=blocker= is a blocking router that takes the following direct slots:
-- =block-banner-p= (default: =t=) :: whether to display a block banner upon blocking the =route=.
+- =block-banner-p= (default: =t=) :: whether to display a block banner upon blocking the route.
-- =blocklist= :: A PCRE to match against the current URL, =t= to block the entire route, or a property list of blocking conditions in the form of =TYPE VALUE=, where =TYPE= is one of =:path= or =:host=. =VALUE= is another plist of the form =PRED RULES=, where =PRED= is either =:starts=, =:ends=, or =:contains= and =RULES= is a list of strings to draw the comparison against according to the current =TYPE=. If =RULES= is prefixed with =not=, the entire route will be blocked except for the specified =RULES=.
+- =blocklist= :: A PCRE to match against the current route, =t= to block the entire route, or a property list of blocking conditions in the form of =TYPE VALUE=, where =TYPE= is one of =:path= or =:host=. =VALUE= is another plist of the form =PRED RULES=, where =PRED= is either =:starts=, =:ends=, or =:contains= and =RULES= is a list of strings to draw the comparison against according to the current =TYPE=. If =RULES= is prefixed with =not=, the entire route will be blocked except for the specified =RULES=.
You can also pass an integer as =VALUE= to indicate the number of URL /sections/ (e.g. =https://example.com/<section1>/<section2>=) to block in case the blocking condition value is not known. Combined =RULES= (specified via =:or=) allow you to specify two or more predicates that you wish to draw the path comparison against, useful if you want to specify a more general block rule first and bypass it for certain scenarios.
-=opener= is a general-purpose resource-opener =route= that instructs resources to be opened externally. It takes the following direct slots:
+=opener= is a router that instructs resources to be opened externally. It takes the following direct slots:
- =resource= :: a resource can be either a function form, in which case it takes a single parameter URL and can invoke arbitrary Lisp forms with it. If it's a string, it runs the specified command via =uiop:run-program= with the current URL as argument, and can be given in a =format=-like syntax.
-=media-toggler= is a =route= that toggles the display status of media elements in the page. It takes the following direct slots:
-
-- =media-p= :: whether to display media in the =route=.
-
-As previously mentioned, =web-route= is a =route= that combines all of the aforementioned routes and takes all their slots. It's helpful when you want to invoke lots of behavior types inside a single =route=.
-
** Examples
-Set up all Instagram requests to redirect to the hostname =www.picuki.com= and additionally redirect all the paths that don't start with =/=, =/p/=, or =/tv/= to =/profile/= paths, and all paths that do start with =/p/= to =/media/=. This goes in line with the URL structure that Picuki uses. Additionally, block all the paths that don't contain the sub-string =/video=.
+Set up all Instagram requests to redirect to the hostname =www.picuki.com= and additionally redirect all the paths that don't start with =/=, =/p/=, or =/tv/= to =/profile/= paths, and all paths that do start with =/p/= to =/media/=. This goes in line with the URL structure that Picuki uses.
#+begin_src lisp
-(make-instance 'router:web-route
- :trigger (match-regex "https://(www.)?insta.*")
+(make-instance 'router:redirector
+ :route (match-regex "https://(www.)?insta.*")
:redirect-url "www.picuki.com"
:redirect-rule '(("/profile/" . (not "/" "/p/" "/tv/"))
- ("/media/" . "/p/"))
- :blocklist '(:path (:contains (not "/video"))))
- #+end_src
+ ("/media/" . "/p/")))
+#+end_src
Redirect all TikTok requests except the index path, videos, or usernames to =/@placeholder/video/=. This is what [[https://github.com/pablouser1/ProxiTok][ProxiTok]] uses to proxy TikTok shared links.
#+begin_src lisp
(make-instance 'router:redirector
- :trigger (match-domain "tiktok.com")
+ :route (match-domain "tiktok.com")
:redirect-url "proxitok.herokuapp.com"
:redirect-rule '(("/@placeholder/video/" . (not "/" "/@" "/t"))))
#+end_src
-Redirect all Reddit requests to your preferred [[https://codeberg.org/teddit/teddit][Teddit]] instance and additionally block all of the paths belonging to this route except those that contain the =/comments= sub-string. This would essentially limit you to only being able to access Teddit publications and not being able to access URL sections such as the main feed.
+Redirect all Reddit requests to your preferred [[https://codeberg.org/teddit/teddit][Teddit]] instance and additionally block all of the paths belonging to this route except those that contain the =/comments= sub-string.
#+begin_src lisp
-(make-instance 'router:web-route
- :trigger (match-domain "reddit.com")
- :redirect-url "teddit.namazso.eu"
- :original-url "www.reddit.com"
- :blocklist '(:path (:contains (not "/comments"))))
+(list
+ (make-instance 'router:redirector
+ :name 'reddit
+ :route (match-domain "reddit.com")
+ :redirect-url "teddit.namazso.eu"
+ :original-url "www.reddit.com")
+ (make-instance 'router:blocker
+ :name 'reddit
+ :blocklist '(:path (:contains (not "/comments")))))
#+end_src
-As shown above, you can pass an =:original-url= slot to the =redirector= route so that if you wrap Nyxt internal methods like shown below, history entries will get recorded with the original URL, meaning an inverse redirection will be applied to figure out the original URL structure.
+You can pass an =:original-url= slot to the =redirector= router to perform a reverse redirection of the route, which can be useful when recording your history, for instance. For this, you have to wrap Nyxt internal methods like this:
#+begin_src lisp
(defmethod nyxt:on-signal-load-finished :around ((mode nyxt/history-mode:history-mode) url)
(call-next-method mode (router:trace-url url)))
#+end_src
-Use a =redirector= route with a PCRE trigger to redirect all Fandom routes to your preferred [[https://breezewiki.com/][BreezeWiki]] instance.
+Use a =redirector= router with a PCRE trigger to redirect all Fandom routes to your preferred [[https://breezewiki.com/][BreezeWiki]] instance.
#+begin_src lisp
(make-instance 'router:redirector
- :trigger "https://([\\w'-]+)\\.fandom.com/wiki/(.*)"
+ :route "https://([\\w'-]+)\\.fandom.com/wiki/(.*)"
:redirect-url "https://breezewiki.com/\\1/wiki/\\2")
#+end_src
-Match YouTube video URLs, videos hosted on its alternative front-ends such as [[https://invidious.io/][Invidious]], and MP3 files, redirecting all of these to =youtube.com=, and dispatching an =opener= route that invokes an external program with the matched URL, in this case launching an [[https://mpv.io/][mpv]] player IPC client process through [[https://github.com/kljohann/mpv.el][mpv.el]] to control the player from Emacs. You can also pass a one-placeholder format string such as =mpv --video=no ~a= to the =:resource= slot if you'd rather not use a Lisp form, where =~a= represents the matched URL. Note how a route's trigger can also consist of a list of predicates for which to match URLs, meaning that on the route below it will match either URLs that contain the video regexp or those that contain the =.mp3= file extension.
+Use a =redirector= router to match on YouTube video URLs and MP3 files and redirect these to =youtube.com=, and dispatch an =opener= router that launches an [[https://mpv.io/][mpv]] player IPC client process through [[https://github.com/kljohann/mpv.el][mpv.el]] to control the player from Emacs. You can also pass a one-placeholder format string such as =mpv --video=no ~a= to the =resource= slot if you'd rather not use a Lisp form, where =~a= represents the matched URL.
#+begin_src lisp
-(make-instance 'router:web-route
- :trigger '((match-regex ".*/watch\\?v=.*")
- (match-file-extension "mp3"))
- :redirect-url "youtube.com"
- :resource (lambda (url)
- (eval-in-emacs
- `(mpv-start ,url))))
+(list
+ (make-instance 'router:redirector
+ :name 'youtube
+ :route '((match-regex ".*/watch\\?v=.*")
+ (match-file-extension "mp3"))
+ :redirect-url "youtube.com")
+ (make-instance 'router:opener
+ :name 'youtube
+ :resource (lambda (url)
+ (eval-in-emacs `(mpv-start ,url)))))
#+end_src
-The route below provides an =:instances-builder= slot, which takes an =instances-builder= object that can be customized by the user to generate a list of instances. This is only useful if the service provider of =:redirect-url= hosts an external endpoint where these are stored. Instances will be added to the route's =:trigger= on route instantiation. =:redirect-url= slots can also take an arbitrary function which will compute the redirect hostname to use. See [[file:instances.lisp][instances.lisp]] for a few predefined builders for common alternative front-end providers.
+Pass the router an =instances-builder= to generate a list of instances that will be appended to the routes on router instantiation. Also provide =redirect-url= as a function to compute the redirect hostname to use. See [[file:instances.lisp][instances.lisp]] for some predefined builders for front-end providers.
#+begin_src lisp
(defun set-invidious-instance ()
@@ -221,74 +210,37 @@ The route below provides an =:instances-builder= slot, which takes an =instances
"https://api.invidious.io/instances.json"))))))
(first (car instances))))
-(make-instance 'router:web-route
- :trigger (match-domain "youtube.com" "youtu.be")
- :blocklist '(:path (:starts "/c/"))
- :redirect-url 'set-invidious-instance
- :instances-builder
- ;; or router:invidious-instances-builder
- (make-instance
- 'instances-builder
- :source "https://api.invidious.io/instances.json"
- :builder
- (lambda (instances)
- (mapcar
- 'first
- (json:with-decoder-simple-list-semantics
- (json:decode-json-from-string
- instances))))))
+(make-instance 'router:redirector
+ :route (match-domain "youtube.com" "youtu.be")
+ :redirect-url #'set-invidious-instance
+ :instances-builder router:invidious-instances-builder)
#+end_src
-In case you'd like to specify a different URL scheme than HTTPS or a different port for =:redirect-url=, you should supply a redirect in the form of a =quri:uri= object. For instance, the following sets up a =redirector= route that redirects Google search results to a locally-running instance of [[https://github.com/benbusby/whoogle-search][Whoogle]], and where results will appear as if they were searched in Google.
+If you'd like to redirect a route to a URL with a scheme other than HTTPS or a non-standard port, you need to supply =redirect-url= as a =quri:uri= object. For instance, this sets up a router that redirects Google results to a locally-running [[https://github.com/benbusby/whoogle-search][whoogle-search]] instance and where results will appear as if they were searched in Google:
#+begin_src lisp
(make-instance 'router:redirector
- :trigger (match-regex "https://whoogle.*" "https://.*google.com/search.*")
+ :route (match-regex ".*://whoogle.*" ".*://.*google.com/search.*")
:redirect-url (quri:uri "http://localhost:5000")
:original-url (quri:uri "https://www.google.com"))
#+end_src
-If you'd like to randomize =:redirect-url= to a list of the healthiest instances, you can use a service like [[https://sr.ht/~benbusby/farside/][Farside]]. Then, include the following =redirector= route, which redirects all Twitter URLs to the corresponding Farside endpoint.
+If you want to randomize =redirect-url= between a list of hosts, you can use a service like [[https://sr.ht/~benbusby/farside/][Farside]] and include a router along these lines:
#+begin_src lisp
(make-instance 'router:redirector
- :trigger (match-domain "twitter.com")
+ :route (match-domain "twitter.com")
:redirect-url "farside.link"
:redirect-rule '(("/nitter/" . "/")))
#+end_src
-The following showcases the use of a =blocker= route that prevents the user from accessing Amazon URLs unless their hostname begins with the =smile= sub-string.
-
-#+begin_src lisp
-(make-instances 'router:blocker
- :trigger (match-domain "amazon.com")
- :blocklist '(:host (:starts (not "smile"))))
- #+end_src
-
-Use a =blocker= route to block certain paths of the =lemmy.ml= route. Namely, block paths that contain the =post= sub-string or paths that start with the =/u/= sub-string. This blocks all publications and user profiles on the site.
+Use a router with a combined =blocklist= path rule for https://github.com. These rules allow you to specify two or more predicates to draw the path comparison against. In the example below, an integer indicates we want to block paths that consist of a single path sub-section (e.g. =https://github.com/<sub-section>=), /or/ block all paths except those that contain the =pulls= or =search= sub-strings. This allows you to provide a general block rule and bypass it for specific routes.
#+begin_src lisp
(make-instance 'router:blocker
- :trigger (match-domain "lemmy.ml")
- :blocklist '(:path (:contains "post" :starts "/u/")))
- #+end_src
-
-Use a =blocker= route with a combined =:blocklist= path rule for =github.com= requests. Combined path rules (specified via =:or=) allow you to specify two or more predicates that you wish to draw the path comparison against. In the case below, an integer indicates we want to block paths that consist of a single path sub-section (e.g. =https://github.com/<sub-section>=), /or/ block all paths except those that contain the =pulls= or =search= sub-strings. This allows you to specify a more general block rule and bypass it for certain scenarios. In the case below, it blocks all single sub-section paths on =github.com= (such as profiles, the marketplace and so on) but at the same time allows you to use GitHub's search engine and see your listed pull requests.
-
-#+begin_src lisp
-(make-instance 'router:blocker
- :trigger (match-domain "github.com")
+ :route (match-domain "github.com")
:blocklist '(:path (:or 1 (:contains (not "pulls" "search")))))
- #+end_src
-
-To block one or more triggers, you can use a =blocker= route with a =:blocklist= set to =t=.
-
-#+begin_src lisp
-(make-instance 'router:blocker
- :trigger (match-domain "timewastingsite1.com"
- "timewastingsite2.com")
- :blocklist t)
- #+end_src
+#+end_src
** Contributing
You can send feedback, patches, or bug reports to [[mailto:public@mianmoreno.com][public@mianmoreno.com]].
diff --git a/README.md b/README.md
index 326f369..1c6727f 100644
--- a/README.md
+++ b/README.md
@@ -2,20 +2,18 @@
# nx-router
-`nx-router` is a URL routing extension for [Nyxt](https://nyxt.atlas.engineer/). In short, it's an abstraction around Nyxt request resource handlers that introduces the concept of `route` objects.
+`nx-router` is a URL routing extension for [Nyxt](https://nyxt.atlas.engineer/). In short, it's an abstraction around Nyxt resource handlers that uses `router` objects to make it more convenient to handle routes. See [Examples](#orgc88d3b8) for a walk-through on how to set up routers.
-Over time, I found the built-in functionality in Nyxt for resource handling becomes difficult to maintain and reason about. I started by using plain handlers to achieve what I needed, but became frustrated with the amount of duplicate logic I found myself writing and the fact I had to come up with everything by myself when I wanted more complex logic.
+The main drive behind `nx-router` was I initially found built-in handlers difficult to reason and I soon became frustrated with the amount of duplicate logic I had to maintain. `nx-router` tries to tackle common needs in resource handling with a redirector, a site blocker, and a resource opener. You can think of it as a more batteries-included `url-dispatching-handler`.
-`nx-router` is built with the aim of finding a common ground to the most common needs in resource handling. It currently provides five routes: a general-purpose redirector, a general-purpose site blocker, a resource opener, a media toggler, and a `web-route` that ties all of these together for more complex requirements.
-
-If you're coming from standard browsers, you can think of `nx-router` as similar to existing solutions like [Redirector](https://github.com/einaregilsson/Redirector) and [LeechblockNG](https://github.com/proginosko/LeechBlockNG) put together and on steroids. If you're privacy-minded and already use Nyxt, you might have stumbled upon [nx-freestance-handler](https://github.com/kssytsrk/nx-freestance-handler), an extension akin to [LibRedirect](https://github.com/libredirect/libredirect) which redirects popular sites to their privacy-friendly front-ends. The problem I see with such extensions is they limit the user to a few predefined sites and create an implicit dependency on their maintainer to update the extension each time one of these goes down or changes its URL structure. See [Examples](#orge0ea95e) for a walk-through on how to set routes up, including how to replicate all of the `nx-freestance-handler` functionality.
+[nx-freestance-handler](https://github.com/kssytsrk/nx-freestance-handler) is a similar extension that redirects popular sites to privacy-friendly front-ends. However, this has the limitation that it only works with a few sites and makes you reliant on its maintainer updating the extension to add new handlers or modify existing ones.
## Installation
To install the extension, you need to download the source and place it in Nyxt's extensions path, given by the value of `nyxt-source-registry` (by default `~/.local/share/nyxt/extensions`).
- git clone https://git.sr.ht/~conses/nx-router ~/.local/share/nyxt/extensions/nx-router
+ git clone https://git.mianmoreno.com/nx-router ~/.local/share/nyxt/extensions/nx-router
The extension works with **Nyxt 3 onward** but it's encouraged to use it with the latest version of Nyxt master for the time being.
@@ -43,143 +41,134 @@ In addition, you should add the extension options, explained in the following se
## Configuration
-You must begin by customizing `router-mode`, a mode that takes all of the user routes and applies them to the currently-running browsing session.
+This shows some example routers you'd set up inside the `router-mode` configuration class:
(define-configuration router:router-mode
- ((router:routes
+ ((router:routers
(list
(make-instance 'router:redirector
- :trigger (match-domain "example.org")
+ :route (match-domain "example.org")
:redirect-url "acme.org"
- :redirect-rule '(("/example" . (not "/" "wiki"))))
+ :redirect-rule '(("/example" . (not "/" "/wiki"))))
(make-instance 'router:blocker
- :trigger (match-hostname "www.wonka.inc")
- :blocklist t)
- (make-instance 'router:media-toggler
- :trigger (match-url "https://gekko.com/gallery")
+ :name 'wonka
+ :route (match-hostname "www.wonka.inc")
:instances-builder
(make-instance
'router:instances-builder
- :source "https://gekko.com/instances.json"
+ :source "https://www.wonka.inc/instances.json"
:builder (lambda (instances)
(json:decode-json-from-string
- instances))
- :media-p nil))
+ instances)))
+ :blocklist "/factory")
(make-instance 'router:opener
- :trigger (match-extension "mp3")
+ :name 'wonka
:resource "mpv --video=no ~a")
- (make-instance 'router:web-route
- :trigger (match-domain "stark.net")
+ (make-instance 'router:redirector
+ :name 'wonka
:redirect-rule "https://stark.net/products/(\\w+)/(.*)"
- :redirect-url (quri:uri "https://\\1.acme.org/\\2")
- :blocklist '(:path (:ends (not ".html")
- :contains "acme"))
- :resource (lambda (url)
- (add-to-csv url))
- :media-p nil)))))
+ :redirect-url (quri:uri "https://\\1.acme.org/\\2"))))))
-As you might notice from above, routes can go from really simple ones to ones with really complex logic baked in. As mentioned before, there are currently five predefined routes, with one of them being a combination of the other four.
+The first router simply redirects all requests that match the domain <https://example.org> to <https://acme.org>, and adds a redirect rule so that any path of <https://example.org> that doesn't match `/` or `/wiki` will get redirected to <https://acme.org/example>. The second router sets a blocklist for the `/factory` PCRE, and the rest of the routers are a composition of the second router, so they inherit its settings.
-All routes derive from a `route` parent class that holds the shared settings. It includes the following slots:
+All routers derive from a `router` parent class that holds common router settings:
-- **`trigger`:** the trigger for `route` activation, akin to the predicates used in Nyxt `auto-rules`. One of `match-domain`, `match-host`, `match-regex`, `match-port`, a user-defined function, or a PCRE.
+- **`route`:** the route to match for `router` activation, akin to the predicates used in Nyxt `auto-rules`. One of `match-domain`, `match-host`, `match-regex`, `match-port`, a user-defined function, or a PCRE.
- **`instances-builder`:** this takes an `instances-builder` object, which in turn takes a source to retrieve instances from and a builder which assembles them into a list.
-- **`toplevel-p` (default: `t`):** whether the `route` is meant to process only top-level requests.
-
-The default settings for the above can be changed in whatever granularity level you desire.
+- **`toplevel-p` (default: `t`):** whether the router is meant to process only top-level requests.
-Due to the nature of WebkitGTK, click events are obscured, which means some requests might not get through, and `iframes` cannot be redirected. To alleviate this, you can control whether only top-level requests should be processed . For example, let's say you decide you want all requests to be processed, including non top-level ones, for all routes by default. You can do so by configuring the `route` user-class in your configuration.
+Do note the WebkitGTK renderer poses some limitations with requests. For example, some of them might not get processed because click events are obscured, and `iframes` cannot be redirected at all. To alleviate this, you can control whether only top-level requests should be processed . If you want all requests to be processed, including non top-level ones, for all routers by default you can configure the `router` user-class like this:
- (define-configuration router:route
+ (define-configuration router:router
((router:toplevel-p nil)))
-If you'd like to process non top-level requests only for `redirector` and `opener` routes by default, you can configure them to do so.
+If you'd like to process non top-level requests only for `redirector` and `opener` routers by default, you can configure them like this:
(define-configuration (router:redirector router:opener)
((router:toplevel-p nil)))
-Finally, if you'd like to process non top-level requests only for a given instance of a `redirector` class, add the corresponding slot on the `route` instantiation.
+Finally, if you'd like to process non top-level requests only for a given instance of a `redirector` class, add this on router instantiation:
(make-instance 'router:redirector
- :trigger (match-domain "example.org")
+ :route (match-domain "example.org")
:redirect-url "acme.org"
:toplevel-p nil)
-`redirector` is a general-purpose redirect `route` that takes the following direct slots:
+`redirector` is a redirect router that takes the following direct slots:
-- **`redirect-url`:** a string for a URL host to redirect to or a `quri:uri` object for a complete URL to redirect to. If `redirect-rule` or `trigger` is a PCRE, you can use this as the replacement string and include special sub-strings like those used in `ppcre:regex-replace` (e.g. `\N`, `\'`, `` \` ``, etc.).
+- **`redirect-url`:** a string for a URL host to redirect to or a `quri:uri` object for a complete URL to redirect to. If `redirect-rule` or `route` is a PCRE, you can use this as the replacement string and include special sub-strings like those used in `ppcre:regex-replace` (e.g. `\N`, `\'`, `` \` ``, etc.).
-- **`redirect-rule`:** a PCRE to match against the current URL or an association list of redirection rules for paths. If the latter, each entry is a cons of the form `REDIRECT-PATH . TRIGGER-PATHS`, where `TRIGGER-PATHS` is a list of paths from the trigger URL that will be redirected to `REDIRECT-PATH` in the `redirect-url` . To redirect all paths except `TRIGGER-PATHS` to `REDIRECT-PATH`, prefix this list with `not`.
+- **`redirect-rule`:** a PCRE to match against the current URL or an association list of redirection rules for paths. If the latter, each entry is a cons of the form `REDIRECT . ROUTES`, where `ROUTES` is a list of paths from the `route` that will be redirected to `REDIRECT` in `redirect-url`. To redirect all paths except `ROUTES` to `REDIRECT`, prefix this list with `not`.
-- **`original-url`:** takes either a string for the route's original host or a `quri:uri` object for the original complete URL. This is useful for storage purposes (bookmarks, history, etc.) so that the original URL is recorded instead of the redirect's URL.
+- **`original-url`:** takes either a string for the router>'s original host or a `quri:uri` object for the original complete URL. This is useful for storage purposes (bookmarks, history, etc.) so that the original URL is recorded instead of the redirect's URL.
-`blocker` is a general-purpose block `route` that takes the following direct slots:
+`blocker` is a blocking router that takes the following direct slots:
-- **`block-banner-p` (default: `t`):** whether to display a block banner upon blocking the `route`.
+- **`block-banner-p` (default: `t`):** whether to display a block banner upon blocking the route.
-- **`blocklist`:** A PCRE to match against the current URL, `t` to block the entire route, or a property list of blocking conditions in the form of `TYPE VALUE`, where `TYPE` is one of `:path` or `:host`. `VALUE` is another plist of the form `PRED RULES`, where `PRED` is either `:starts`, `:ends`, or `:contains` and `RULES` is a list of strings to draw the comparison against according to the current `TYPE`. If `RULES` is prefixed with `not`, the entire route will be blocked except for the specified `RULES`.
+- **`blocklist`:** A PCRE to match against the current route, `t` to block the entire route, or a property list of blocking conditions in the form of `TYPE VALUE`, where `TYPE` is one of `:path` or `:host`. `VALUE` is another plist of the form `PRED RULES`, where `PRED` is either `:starts`, `:ends`, or `:contains` and `RULES` is a list of strings to draw the comparison against according to the current `TYPE`. If `RULES` is prefixed with `not`, the entire route will be blocked except for the specified `RULES`.
You can also pass an integer as `VALUE` to indicate the number of URL *sections* (e.g. `https://example.com/<section1>/<section2>`) to block in case the blocking condition value is not known. Combined `RULES` (specified via `:or`) allow you to specify two or more predicates that you wish to draw the path comparison against, useful if you want to specify a more general block rule first and bypass it for certain scenarios.
-`opener` is a general-purpose resource-opener `route` that instructs resources to be opened externally. It takes the following direct slots:
+`opener` is a router that instructs resources to be opened externally. It takes the following direct slots:
- **`resource`:** a resource can be either a function form, in which case it takes a single parameter URL and can invoke arbitrary Lisp forms with it. If it's a string, it runs the specified command via `uiop:run-program` with the current URL as argument, and can be given in a `format`-like syntax.
-`media-toggler` is a `route` that toggles the display status of media elements in the page. It takes the following direct slots:
-
-- **`media-p`:** whether to display media in the `route`.
-
-As previously mentioned, `web-route` is a `route` that combines all of the aforementioned routes and takes all their slots. It's helpful when you want to invoke lots of behavior types inside a single `route`.
-
## Examples
-Set up all Instagram requests to redirect to the hostname `www.picuki.com` and additionally redirect all the paths that don't start with `/`, `/p/`, or `/tv/` to `/profile/` paths, and all paths that do start with `/p/` to `/media/`. This goes in line with the URL structure that Picuki uses. Additionally, block all the paths that don't contain the sub-string `/video`.
+Set up all Instagram requests to redirect to the hostname `www.picuki.com` and additionally redirect all the paths that don't start with `/`, `/p/`, or `/tv/` to `/profile/` paths, and all paths that do start with `/p/` to `/media/`. This goes in line with the URL structure that Picuki uses.
- (make-instance 'router:web-route
- :trigger (match-regex "https://(www.)?insta.*")
+ (make-instance 'router:redirector
+ :route (match-regex "https://(www.)?insta.*")
:redirect-url "www.picuki.com"
:redirect-rule '(("/profile/" . (not "/" "/p/" "/tv/"))
- ("/media/" . "/p/"))
- :blocklist '(:path (:contains (not "/video"))))
+ ("/media/" . "/p/")))
Redirect all TikTok requests except the index path, videos, or usernames to `/@placeholder/video/`. This is what [ProxiTok](https://github.com/pablouser1/ProxiTok) uses to proxy TikTok shared links.
(make-instance 'router:redirector
- :trigger (match-domain "tiktok.com")
+ :route (match-domain "tiktok.com")
:redirect-url "proxitok.herokuapp.com"
:redirect-rule '(("/@placeholder/video/" . (not "/" "/@" "/t"))))
-Redirect all Reddit requests to your preferred [Teddit](https://codeberg.org/teddit/teddit) instance and additionally block all of the paths belonging to this route except those that contain the `/comments` sub-string. This would essentially limit you to only being able to access Teddit publications and not being able to access URL sections such as the main feed.
+Redirect all Reddit requests to your preferred [Teddit](https://codeberg.org/teddit/teddit) instance and additionally block all of the paths belonging to this route except those that contain the `/comments` sub-string.
- (make-instance 'router:web-route
- :trigger (match-domain "reddit.com")
- :redirect-url "teddit.namazso.eu"
- :original-url "www.reddit.com"
- :blocklist '(:path (:contains (not "/comments"))))
+ (list
+ (make-instance 'router:redirector
+ :name 'reddit
+ :route (match-domain "reddit.com")
+ :redirect-url "teddit.namazso.eu"
+ :original-url "www.reddit.com")
+ (make-instance 'router:blocker
+ :name 'reddit
+ :blocklist '(:path (:contains (not "/comments")))))
-As shown above, you can pass an `:original-url` slot to the `redirector` route so that if you wrap Nyxt internal methods like shown below, history entries will get recorded with the original URL, meaning an inverse redirection will be applied to figure out the original URL structure.
+You can pass an `:original-url` slot to the `redirector` router to perform a reverse redirection of the route, which can be useful when recording your history, for instance. For this, you have to wrap Nyxt internal methods like this:
(defmethod nyxt:on-signal-load-finished :around ((mode nyxt/history-mode:history-mode) url)
(call-next-method mode (router:trace-url url)))
-Use a `redirector` route with a PCRE trigger to redirect all Fandom routes to your preferred [BreezeWiki](https://breezewiki.com/) instance.
+Use a `redirector` router with a PCRE trigger to redirect all Fandom routes to your preferred [BreezeWiki](https://breezewiki.com/) instance.
(make-instance 'router:redirector
- :trigger "https://([\\w'-]+)\\.fandom.com/wiki/(.*)"
+ :route "https://([\\w'-]+)\\.fandom.com/wiki/(.*)"
:redirect-url "https://breezewiki.com/\\1/wiki/\\2")
-Match YouTube video URLs, videos hosted on its alternative front-ends such as [Invidious](https://invidious.io/), and MP3 files, redirecting all of these to `youtube.com`, and dispatching an `opener` route that invokes an external program with the matched URL, in this case launching an [mpv](https://mpv.io/) player IPC client process through [mpv.el](https://github.com/kljohann/mpv.el) to control the player from Emacs. You can also pass a one-placeholder format string such as `mpv --video=no ~a` to the `:resource` slot if you'd rather not use a Lisp form, where `~a` represents the matched URL. Note how a route's trigger can also consist of a list of predicates for which to match URLs, meaning that on the route below it will match either URLs that contain the video regexp or those that contain the `.mp3` file extension.
+Use a `redirector` router to match on YouTube video URLs and MP3 files and redirect these to `youtube.com`, and dispatch an `opener` router that launches an [mpv](https://mpv.io/) player IPC client process through [mpv.el](https://github.com/kljohann/mpv.el) to control the player from Emacs. You can also pass a one-placeholder format string such as `mpv --video=no ~a` to the `resource` slot if you'd rather not use a Lisp form, where `~a` represents the matched URL.
- (make-instance 'router:web-route
- :trigger '((match-regex ".*/watch\\?v=.*")
- (match-file-extension "mp3"))
- :redirect-url "youtube.com"
- :resource (lambda (url)
- (eval-in-emacs
- `(mpv-start ,url))))
+ (list
+ (make-instance 'router:redirector
+ :name 'youtube
+ :route '((match-regex ".*/watch\\?v=.*")
+ (match-file-extension "mp3"))
+ :redirect-url "youtube.com")
+ (make-instance 'router:opener
+ :name 'youtube
+ :resource (lambda (url)
+ (eval-in-emacs `(mpv-start ,url)))))
-The route below provides an `:instances-builder` slot, which takes an `instances-builder` object that can be customized by the user to generate a list of instances. This is only useful if the service provider of `:redirect-url` hosts an external endpoint where these are stored. Instances will be added to the route's `:trigger` on route instantiation. `:redirect-url` slots can also take an arbitrary function which will compute the redirect hostname to use. See [instances.lisp](instances.lisp) for a few predefined builders for common alternative front-end providers.
+Pass the router an `instances-builder` to generate a list of instances that will be appended to the routes on router instantiation. Also provide `redirect-url` as a function to compute the redirect hostname to use. See [instances.lisp](instances.lisp) for some predefined builders for front-end providers.
(defun set-invidious-instance ()
"Set the primary Invidious instance."
@@ -198,62 +187,31 @@ The route below provides an `:instances-builder` slot, which takes an `instances
"https://api.invidious.io/instances.json"))))))
(first (car instances))))
- (make-instance 'router:web-route
- :trigger (match-domain "youtube.com" "youtu.be")
- :blocklist '(:path (:starts "/c/"))
- :redirect-url 'set-invidious-instance
- :instances-builder
- ;; or router:invidious-instances-builder
- (make-instance
- 'instances-builder
- :source "https://api.invidious.io/instances.json"
- :builder
- (lambda (instances)
- (mapcar
- 'first
- (json:with-decoder-simple-list-semantics
- (json:decode-json-from-string
- instances))))))
-
-In case you'd like to specify a different URL scheme than HTTPS or a different port for `:redirect-url`, you should supply a redirect in the form of a `quri:uri` object. For instance, the following sets up a `redirector` route that redirects Google search results to a locally-running instance of [Whoogle](https://github.com/benbusby/whoogle-search), and where results will appear as if they were searched in Google.
+ (make-instance 'router:redirector
+ :route (match-domain "youtube.com" "youtu.be")
+ :redirect-url #'set-invidious-instance
+ :instances-builder router:invidious-instances-builder)
+
+If you'd like to redirect a route to a URL with a scheme other than HTTPS or a non-standard port, you need to supply `redirect-url` as a `quri:uri` object. For instance, this sets up a router that redirects Google results to a locally-running [whoogle-search](https://github.com/benbusby/whoogle-search) instance and where results will appear as if they were searched in Google:
(make-instance 'router:redirector
- :trigger (match-regex "https://whoogle.*" "https://.*google.com/search.*")
+ :route (match-regex ".*://whoogle.*" ".*://.*google.com/search.*")
:redirect-url (quri:uri "http://localhost:5000")
:original-url (quri:uri "https://www.google.com"))
-If you'd like to randomize `:redirect-url` to a list of the healthiest instances, you can use a service like [Farside](https://sr.ht/~benbusby/farside/). Then, include the following `redirector` route, which redirects all Twitter URLs to the corresponding Farside endpoint.
+If you want to randomize `redirect-url` between a list of hosts, you can use a service like [Farside](https://sr.ht/~benbusby/farside/) and include a router along these lines:
(make-instance 'router:redirector
- :trigger (match-domain "twitter.com")
+ :route (match-domain "twitter.com")
:redirect-url "farside.link"
:redirect-rule '(("/nitter/" . "/")))
-The following showcases the use of a `blocker` route that prevents the user from accessing Amazon URLs unless their hostname begins with the `smile` sub-string.
-
- (make-instances 'router:blocker
- :trigger (match-domain "amazon.com")
- :blocklist '(:host (:starts (not "smile"))))
-
-Use a `blocker` route to block certain paths of the `lemmy.ml` route. Namely, block paths that contain the `post` sub-string or paths that start with the `/u/` sub-string. This blocks all publications and user profiles on the site.
-
- (make-instance 'router:blocker
- :trigger (match-domain "lemmy.ml")
- :blocklist '(:path (:contains "post" :starts "/u/")))
-
-Use a `blocker` route with a combined `:blocklist` path rule for `github.com` requests. Combined path rules (specified via `:or`) allow you to specify two or more predicates that you wish to draw the path comparison against. In the case below, an integer indicates we want to block paths that consist of a single path sub-section (e.g. `https://github.com/<sub-section>`), *or* block all paths except those that contain the `pulls` or `search` sub-strings. This allows you to specify a more general block rule and bypass it for certain scenarios. In the case below, it blocks all single sub-section paths on `github.com` (such as profiles, the marketplace and so on) but at the same time allows you to use GitHub's search engine and see your listed pull requests.
+Use a router with a combined `blocklist` path rule for <https://github.com>. These rules allow you to specify two or more predicates to draw the path comparison against. In the example below, an integer indicates we want to block paths that consist of a single path sub-section (e.g. `https://github.com/<sub-section>`), *or* block all paths except those that contain the `pulls` or `search` sub-strings. This allows you to provide a general block rule and bypass it for specific routes.
(make-instance 'router:blocker
- :trigger (match-domain "github.com")
+ :route (match-domain "github.com")
:blocklist '(:path (:or 1 (:contains (not "pulls" "search")))))
-To block one or more triggers, you can use a `blocker` route with a `:blocklist` set to `t`.
-
- (make-instance 'router:blocker
- :trigger (match-domain "timewastingsite1.com"
- "timewastingsite2.com")
- :blocklist t)
-
## Contributing