aboutsummaryrefslogtreecommitdiff
path: root/README
blob: 9d0be0afb57211d4ea5020b2468cf42b8bd442bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# -*- mode: org; org-html-head-include-default-style: nil; org-html-postamble: nil; after-save-hook: org-md-export-to-markdown; -*-
#+OPTIONS: toc:nil num:nil
* nx-router
=nx-router= is a declarative URL routing extension for [[https://nyxt.atlas.engineer/][Nyxt]]. In short, it's an abstraction around Nyxt request 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.

The main drive behind =nx-router= is that I initially found Nyxt built-in handlers difficult to reason about and I soon became frustrated with how much imperative logic I had to maintain in my configuration. =nx-router= aims to simplify request resource handling in Nyxt with declarative redirects, blockers, and resource handlers. You may think of it as a more batteries-included =url-dispatching-handler=.

[[https://github.com/kssytsrk/nx-freestance-handler][nx-freestance-handler]] is a similar extension that provides handlers for popular 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://github.com/migalmoreno/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.

If you want to place the extension elsewhere in the system, such as for development purposes, you can configure so via the ASDF source registry mechanism. For this, you'll need to create a file in the source registry directory, =~/.config/common-lisp/source-registry.conf.d/=, and then put the following contents into it, replacing the path with the desired system path.

#+name: 10-personal-lisp.conf
#+begin_src lisp
(:tree "/path/to/user/location")
#+end_src

Then, make sure to refresh the ASDF cache via =asdf:clear-source-registry=. ASDF will now be able to find the extension on the custom path. For more information on this utility, please refer to the [[https://asdf.common-lisp.dev/asdf.html][ASDF manual]].

By default, Nyxt won't read the custom source registry path we provided, so ensure to include a =reset-asdf-registries= invocation in Nyxt's configuration file too.

In your Nyxt configuration file, place the following.

#+begin_src lisp
(define-nyxt-user-system-and-load nyxt-user/router
  :depends-on (nx-router)
  :components ("router.lisp"))
#+end_src

Where =router.lisp= is a custom file that should you should create relative to Nyxt's configuration directory (=*config-file*='s pathname by default) to provide the extension settings after the =nx-router= system has been successfully loaded. Inside this file, place the following.

#+begin_src lisp
(define-configuration web-buffer
  ((default-modes `(router:router-mode ,@%slot-value%))))
#+end_src

In addition, you should add the extension options, explained in the following section.

** Configuration
This shows some example routers you'd set up inside the =router-mode= configuration class:

#+begin_src lisp
(define-configuration router:router-mode
  ((router:routers
    (list
     (make-instance 'router:redirector
                    :route (match-domain "example.org")
                    :redirect
                    '(("https://acme.org/example" . (not ".*/$" ".*/wiki$"))))
     (make-instance 'router:blocker
                    :name 'wonka
                    :route (match-hostname "www.wonka.inc")
                    :instances-builder
                    (make-instance 'router:instances-builder
                                   :source "https://www.wonka.inc/instances.json"
                                   :builder (lambda (instances)
                                              (json:decode-json-from-string
                                               instances)))
                    :blocklist ".*/factory")
     (make-instance 'router:opener
                    :name 'wonka
                    :resource "mpv --video=no ~a")
     (make-instance 'router:redirector
                    :name 'wonka
                    :redirect
                    '(("https://\\1.acme.org/\\2" . ".*/p/(\\w+)/(.*)")))))))
#+end_src

The first router specifies a redirect so that any route of https://example.org that doesn't match the regexps =.*/$= or =.*/wiki$= will get redirected to https://acme.org/example. The second router sets a block-list for the router if its route matches the =.*/factory= PCRE and adds an =instances-builder= which will fetch a list of routes and append them to the router's. Note that the second router has a =name= slot, which will allow subsequent routers to inherit the parent-class slots. The third router instructs a resource to be opened upon route activation, and the fourth router uses regexp interpolation to redirect the =.*/p/(\\w+)/(.*)= regexp to =https://\\1.acme.org/\\2=, where =\\1= and =\\2= will be replaced with the corresponding capture groups.

All routers derive from a =router= parent class that holds common router settings:

- =name= :: a symbol for the name of the router which allows for router composition.
- =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 router is meant to process only top-level requests.

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:router
  ((router:toplevel-p nil)))
#+end_src

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 this on router instantiation:

#+begin_src lisp
(make-instance 'router:redirector
               :route (match-domain "example.org")
               :redirect "acme.org"
               :toplevel-p nil)
#+end_src

=redirector= is a redirect router that takes the following direct slots:

- =redirect= :: a string for a URL hostname to redirect to, a =quri:uri= object for a complete URL to redirect to, a PCRE (used as the replacement string of =ppcre:regex-replace= against =route=), or an association list of redirection rules. In the latter, each entry is a cons of the form =REDIRECT . ROUTES=, where =ROUTES= is a list of regexps from the =route= that will be matched against and redirected to =REDIRECT=. To redirect all paths except =ROUTES= to =REDIRECT=, prefix this list with =not=.
- =reverse= :: 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 blocking router that takes the following direct slots:

- =block-banner-p= (default: =t=) :: whether to display a block banner upon blocking the route.
- =blocklist= :: A PCRE to match against the current route, =t= to block the entire route, or a list of regexps to draw the comparison against. If any single list is prefixed with =not=, the entire route will be blocked except for the specified regexps. If all of the lists are prefixed with =or=, this follows an exception-based blocking where you can specify a more general block target first and bypass it for more specific routes.

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

#+begin_src lisp
(make-instance 'router:redirector
               :route (match-regex ".*://.*google.com/search.*")
               :redirect (quri:uri "http://localhost:5000")
               :reverse (quri:uri "https://www.google.com"))
#+end_src

You can include a =:reverse= slot in the =redirector= router with a =quri:uri= object or a string host to perform a reverse redirection of the route, which can be useful when copying the current page's URL or saving your history so that the original URL is recorded. To enable this you have to wrap or override Nyxt internal methods like this:

#+begin_src lisp
(define-command copy-url ()
  "Save current URL to clipboard."
  (let ((url (render-url (router:trace-url (url (current-buffer))))))
    (copy-to-clipboard url)
    (echo "~a copied to clipboard." url)))

(defmethod nyxt:on-signal-load-finished :around ((mode nyxt/history-mode:history-mode) url)
  (call-next-method mode (router:trace-url url)))
#+end_src


** Examples
Redirect YouTube requests that match certain regexps to their corresponding [[https://github.com/migalmoreno/tubo][Tubo]] counterparts.

#+begin_src lisp
(make-instance 'router:redirector
               :route (match-domain "youtube.com")
               :redirect
               '(("https://tubo.migalmoreno.com/stream?url=\\&" . (".*/watch\\?v.*" ".*/shorts/.*"))
                 ("https://tubo.migalmoreno.com/playlist?list=\\&" . ".*/playlist/.*")
                 ("https://tubo.migalmoreno.com/channel?url=\\&" . ".*/channel/.*")))
#+end_src

Set up a redirect for all Instagram requests except those that match the regexps =.*/tv/.*= or =.*/reels/.*= to https://picuki.com/profile/, and redirect routes that match the regexp =.*/p/(.*)= to =https://picuki.com/media/\\1=, where the replacement string =\\1= will be replaced by the =(.*)= capture group in the URL.

#+begin_src lisp
(make-instance 'router:redirector
               :route (match-regex "https://(www.)?insta.*")
               :redirect
               '(("https://picuki.com/profile/" . (not ".*/tv/.*" ".*/reels/.*"))
                 ("https://pickuki.com/media/\\1" . ".*/p/(.*)")))
#+end_src

Redirect all TikTok requests except the index path, videos, or usernames to https://tok.artemislena.eu/@placeholder/video/, and redirect the rest of routes to =https://tok.artemislena.eu/\\1=, where the replacement string =\\1= will be interpolated with everything following the route's hostname. Additionally, block all the routes except those that contain the =.*/video/.*= or the =.*/t/.*= regexps.

#+begin_src lisp
(list
 (make-instance 'router:redirector
                :name 'tiktok
                :route (match-domain "tiktok.com")
                :redirect
                '(("https://tok.artemislena.eu/@placeholder/video/" . (not ".*/@.*" ".*/t/.*"))
                  ("https://tok.artemislena.eu/\\1" . ".*://[^/]*/(.*)$")))
 (make-instance 'router:blocker
                :name 'tiktok
                :blocklist '(or (not ".*/video/.*") (not ".*/t/.*"))))
  #+end_src

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
               :route "https://([\\w'-]+)\\.fandom.com/wiki/(.*)"
               :redirect "https://breezewiki.com/\\1/wiki/\\2")
#+end_src

Use a =redirector= router to match YouTube-like video URLs and MP3 files and redirect these to https://www.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
(list
 (make-instance 'router:redirector
                :name 'youtube
                :route '((match-regex ".*/watch\\?v=.*")
                         (match-file-extension "mp3"))
                :redirect "www.youtube.com")
 (make-instance 'router:opener
                :name 'youtube
                :resource (lambda (url)
                           (eval-in-emacs `(mpv-start ,url)))))
  #+end_src

Pass an =instances-builder= to generate a list of instances that will be appended to the routes on router instantiation. Also provide =redirect= 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 ()
  "Set the primary Invidious instance."
  (let ((instances
          (remove-if-not
           (lambda (instance)
             (and (string= (alex:assoc-value (second instance)
                                             :region)
                           "DE")
                  (string= (alex:assoc-value (second instance)
                                             :type)
                           "https")))
           (json:with-decoder-simple-list-semantics
             (json:decode-json-from-string
              (dex:get
               "https://api.invidious.io/instances.json"))))))
    (first (car instances))))

(make-instance 'router:redirector
               :route (match-domain "youtube.com" "youtu.be")
               :redirect #'set-invidious-instance
               :instances-builder router:invidious-instances-builder)
  #+end_src

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= as a =quri:uri= object. For example, this sets up a router that redirects Google results to a locally-running [[https://github.com/benbusby/whoogle-search][whoogle-search]] instance:

If you want to randomize your =redirect= between a list of hosts, you can use a service like [[https://sr.ht/~benbusby/farside/][Farside]] and write a router along these lines:

#+begin_src lisp
(make-instance 'router:redirector
               :route (match-domain "twitter.com")
               :redirect '(("https://farside.link/nitter/\\1" . ".*://[^/]*/(.*)$")))
#+end_src

Use a router with an exception-based =blocklist= for https://github.com. These rules allow you to specify two or more predicates to draw the block-list comparison against. In the example below, the first regexp indicates we want to block routes that consist of a single path entry (e.g. =https://github.com/path=) *or* block routes except those that contain the =/pulls= or =/search= paths. This allows you to provide a general block rule and bypass it for specific routes.

#+begin_src lisp
(make-instance 'router:blocker
               :route (match-domain "github.com")
               :blocklist '(or ".*://[^/]*/[^/]*$" (not ".*/pulls.*" ".*/search.*")))
#+end_src

** Contributing
Feel free to open an issue with bug reports or feature requests. PRs are more than welcome too.