Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor scheme architecture #3348

Merged
merged 19 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion nyxt.asd
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@
:components ((:file "renderer-package")
(:file "renderer-offline/set-url")
(:file "renderer-offline/execute-command-eval")
(:file "renderer-offline/nyxt-url-security")
(:file "renderer-offline/custom-schemes")
(:file "renderer-offline/search-buffer")
;; See https://github.com/atlas-engineer/nyxt/issues/3172
;; (:file "renderer-online/set-url")
Expand Down
2 changes: 1 addition & 1 deletion source/changelog.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

(define-version "4.0.0"
(:ul
(:li "Refactor lisp schemes URLs API.")
(:li "Refactor custom schemes URLs API.")
(:li "Deprecate slot " (:code "status-buffer-position") "in favour of"
(:nxref :slot 'placement :class-name 'status-buffer) ".")
(:li "Deprecate slot " (:code "prompt-buffer-open-height") " since "
Expand Down
4 changes: 4 additions & 0 deletions source/foreign-interface.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ When URL is non-nil, relative URLs are resolved against it.")
Like `ffi-buffer-load-html', except that it doesn't influence the BUFFER history
or CSS/HTML cache.")

(define-ffi-generic ffi-register-custom-scheme (scheme)
"Register internal custom SCHEME.
See `scheme'.")

(define-ffi-generic ffi-buffer-evaluate-javascript (buffer javascript &optional world-name)
"Evaluate JAVASCRIPT, encoded as a string, in BUFFER.")
(define-ffi-generic ffi-buffer-evaluate-javascript-async (buffer javascript &optional world-name)
Expand Down
43 changes: 11 additions & 32 deletions source/manual.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -382,39 +382,18 @@ Nyxt provides. For example, one can use the "
"Bookmark Link")))

(:nsection :title "Custom URL schemes"
(:p "If there's a scheme that Nyxt doesn't support, but you want it to, you can
always define the handler for this scheme so that it's Nyxt-openable.")
(:p "As a totally hypothetical example, you can define a nonsense scheme "
(:code "bleep") " to generate a page with random text:")
(:p "Nyxt can register custom schemes that run a handler on URL load.")
(:p "The example below defines a scheme " (:code "hello") " that replies
accordingly when loading URLs " (:code "hello:world") " and "
(:code "hello:mars") ".")
(:ncode
'(define-internal-scheme "bleep"
(lambda (url buffer)
(values
(spinneret:with-html-string
(:h1 "Bleep bloop?")
(:p (loop repeat (parse-integer (quri:uri-path (url url)) :junk-allowed t)
collect (:li (elt '("bleep" "bloop") (random 2))))))
"text/html;charset=utf8"))
:local-p t))
(:p "What this piece of code does is")
(:ul
(:li "Define a new scheme.")
(:li "Make a handler for it that takes the URL (as a string) and a buffer it's being
opened in.")
(:li "Read the path (the part after the bleep:) of the URL and interpret it as a number.")
(:ul
(:li "(Note that you need to wrap the URL into a " (:nxref :function 'url)
" call so that it turns into a " (:nxref :class-name 'quri:uri)
" for the convenience of path (and other elements) fetching.)"))
(:li "Generate a random list of \"bleep\" and \"bloop\".")
(:li "Return it as a " (:code "text/html") " content."))
(:p "The next time you run Nyxt and open " (:code "bleep:20")
", you'll see a list of twenty bleeps and bloops.")
(:p "Internal schemes can return any type of content (both strings and arrays of
bytes are recognized), and they are capable of being "
(:nxref :class-name 'scheme :slot 'cors-enabled-p "CORS-enabled")
", " (:nxref :class-name 'scheme :slot 'local-p "protected")
", and are in general capable of whatever the renderer-provided schemes do.")
'(define-internal-scheme "hello"
(lambda (url)
(if (string= (quri:uri-path (url url)) "world")
(spinneret:with-html-string (:p "Hello, World!"))
(spinneret:with-html-string (:p "Please instruct me on how to greet you!"))))))
(:p "Note that scheme privileges, such as enabling the Fetch API or
enabling CORS requests are renderer-specific.")

(:nsection :title "nyxt: URLs and internal pages"
(:p "You can create pages out of Lisp commands, and make arbitrary computations for
Expand Down
9 changes: 3 additions & 6 deletions source/mode/document.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,9 @@ Otherwise, create a dummy buffer with URL to get its source."
(ffi-buffer-delete buffer)))))

(define-internal-scheme "view-source"
(lambda (url buffer)
(declare (ignore buffer))
(values
(get-url-source (quri:url-decode (quri:uri-path (quri:uri url))))
"text/plain"))
:no-access-p t)
(lambda (url)
(values (get-url-source (quri:url-decode (quri:uri-path (quri:uri url))))
"text/plain")))

(define-command-global view-source (&key (url (url (current-buffer))))
"View source of the URL (by default current page) in a separate buffer."
Expand Down
7 changes: 5 additions & 2 deletions source/mode/editor.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ See `describe-class editor-mode' for details."))
(uiop:parse-native-namestring (quri:uri-path (url buffer))))

(define-internal-scheme "editor"
(lambda (url buffer)
(markup (find-submode 'editor-mode buffer)
(lambda (url)
(markup (find-submode 'editor-mode)
jmercouris marked this conversation as resolved.
Show resolved Hide resolved
(uiop:read-file-string (quri:uri-path (quri:uri url))))))

(defmethod editor ((editor-buffer editor-buffer))
Expand Down Expand Up @@ -157,3 +157,6 @@ BUFFER is of type `editor-buffer'."
(prompt1 :prompt "Edit user file"
:sources 'nyxt::user-file-source)))))
(edit-file file-path))

(define-auto-rule '(match-scheme "editor")
:included '(nyxt/mode/editor:editor-mode))
4 changes: 3 additions & 1 deletion source/mode/plaintext-editor.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
It renders the file in a single textarea HTML element. Enabled by default for
`editor-buffer's."
((visible-in-status-p nil)
(rememberable-p nil)
(style (theme:themed-css (theme *browser*)
`("body"
:margin 0)
Expand Down Expand Up @@ -44,3 +43,6 @@ It renders the file in a single textarea HTML element. Enabled by default for
(defmethod get-content ((editor plaintext-editor-mode))
(ps-eval :buffer (buffer editor)
(ps:chain (nyxt/ps:qs document "#editor") value)))

(define-auto-rule '(match-scheme "editor")
:included '(nyxt/mode/editor:plaintext-editor-mode))
93 changes: 45 additions & 48 deletions source/mode/small-web.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ Gemini support is a bit more brittle, but you can override `line->html' for
(url :documentation "The URL being opened.")
(model :documentation "The contents of the current page.")
(redirections nil :documentation "The list of redirection Gemini URLs.")
(allowed-redirections-count
(max-redirections
5
:documentation "The number of redirections that Gemini resources are allowed to make.")
:documentation "The maximum number of times a redirection is attempted.")
(style (theme:themed-css (nyxt::theme *browser*)
`(body
:background-color ,theme:background)
Expand Down Expand Up @@ -187,22 +187,19 @@ Implies that `small-web-mode' is enabled."))
(defmethod gopher-render ((line cl-gopher:gif)) (render-binary-content line "image/gif"))
(defmethod gopher-render ((line cl-gopher:png)) (render-binary-content line "image/png"))

;; TODO: :display-isolated-p? Gopher's behavior implies inability to embed it
;; into pages of the bigger Web, which is exactly what display-isolated means.
(define-internal-scheme "gopher"
(lambda (url buffer)
(lambda (url)
(handler-case
(let* ((line (if (uiop:emptyp (quri:uri-path (quri:uri url)))
(buffer-load (str:concat url "/") :buffer buffer)
(cl-gopher:parse-gopher-uri url))))
(let ((line (if (uiop:emptyp (quri:uri-path (quri:uri url)))
(buffer-load (str:concat url "/"))
(cl-gopher:parse-gopher-uri url))))
(if (and (typep line 'cl-gopher:search-line)
(uiop:emptyp (cl-gopher:terms line)))
(progn (setf (cl-gopher:terms line)
(prompt1 :prompt (format nil "Search query for ~a" url)
:sources 'prompter:raw-source))
(buffer-load (cl-gopher:uri-for-gopher-line line) :buffer buffer))
(with-current-buffer buffer
(gopher-render line))))
(buffer-load (cl-gopher:uri-for-gopher-line line)))
(with-current-buffer (current-buffer) (gopher-render line))))
(cl-gopher:bad-submenu-error ()
(error-help (format nil "Malformed line at ~s" url)
(format nil "One of the lines on this page has an improper format.
Expand Down Expand Up @@ -266,7 +263,7 @@ Please, check URL correctness and try again.")))
(:br)))

(export-always 'gemtext-render)
(defun gemtext-render (gemtext buffer)
(defun gemtext-render (gemtext &optional (buffer (current-buffer)))
"Renders the Gemtext (Gemini markup format) to HTML.

Implies that `small-web-mode' is enabled."
Expand All @@ -282,46 +279,46 @@ Implies that `small-web-mode' is enabled."
collect (:raw (nyxt/mode/small-web:line->html element))))
"text/html;charset=utf8")))

;; TODO: :secure-p t? Gemini is encrypted, so it can be considered secure.
(define-internal-scheme "gemini"
(lambda (url buffer)
(lambda (url)
(handler-case
(sera:mvlet* ((status meta body (gemini:request url)))
(unless (member status '(:redirect :permanent-redirect))
(setf (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)) nil))
(case status
((:input :sensitive-input)
(let ((text (quri:url-encode
(handler-case
(prompt1 :prompt meta
:sources 'prompter:raw-source
:invisible-input-p (eq status :sensitive-input))
(nyxt::prompt-buffer-canceled () "")))))
(buffer-load (str:concat url "?" text) :buffer buffer)))
(:success
(if (str:starts-with-p "text/gemini" meta)
(gemtext-render body buffer)
(values body meta)))
((:redirect :permanent-redirect)
(push url (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))
(if (< (length (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))
(nyxt/mode/small-web:allowed-redirections-count (find-submode 'small-web-mode)))
(buffer-load (quri:merge-uris (quri:uri meta) (quri:uri url)) :buffer buffer)
(error-help
"Error"
(format nil "The server has caused too many (~a+) redirections.~& ~a~{ -> ~a~}"
(nyxt/mode/small-web:allowed-redirections-count (find-submode 'small-web-mode))
(alex:lastcar (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))
(butlast (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))))))
((:temporary-failure :server-unavailable :cgi-error :proxy-error
:permanent-failure :not-found :gone :proxy-request-refused :bad-request)
(error-help "Error" meta))
(:slow-down
(error-help
"Slow down error"
(format nil "Try reloading the page in ~a seconds." meta)))
((:client-certificate-required :certificate-not-authorised :certificate-not-valid)
(error-help "Certificate error" meta))))
(setf (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)) nil))
(case status
((:input :sensitive-input)
(let ((text (quri:url-encode
(handler-case
(prompt1 :prompt meta
:sources 'prompter:raw-source
:invisible-input-p (eq status :sensitive-input))
(nyxt::prompt-buffer-canceled () "")))))
(buffer-load (str:concat url "?" text))
nil))
(:success
(if (str:starts-with-p "text/gemini" meta)
(gemtext-render body)
(values body meta)))
((:redirect :permanent-redirect)
(push url (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))
(if (< (length (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))
(nyxt/mode/small-web:max-redirections (find-submode 'small-web-mode)))
(progn (buffer-load (quri:merge-uris (quri:uri meta) (quri:uri url))) nil)
(error-help
"Error"
(format nil "The server has caused too many (~a+) redirections.~& ~a~{ -> ~a~}"
(nyxt/mode/small-web:max-redirections (find-submode 'small-web-mode))
(alex:lastcar (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))
(butlast (nyxt/mode/small-web:redirections (find-submode 'small-web-mode)))))))
((:temporary-failure :server-unavailable :cgi-error :proxy-error
:permanent-failure :not-found :gone :proxy-request-refused :bad-request)
(error-help "Error" meta))
(:slow-down
(error-help
"Slow down error"
(format nil "Try reloading the page in ~a seconds." meta)))
((:client-certificate-required :certificate-not-authorised :certificate-not-valid)
(error-help "Certificate error" meta))))
(gemini::malformed-response (e)
(error-help
"Malformed response"
Expand Down
15 changes: 2 additions & 13 deletions source/renderer-script.lisp
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,7 @@ Overwrites the whole HTML document (head and body elements included)."
(export-always 'match-internal-page)
(defun match-internal-page (symbol)
"Return a predicate for URL designators matching the page of SYMBOL name."
#'(lambda (url)
(and (str:starts-with-p "nyxt:" (render-url url))
(eq (parse-nyxt-url url)
symbol))))
#'(lambda (url) (eq (internal-page-name url) symbol)))

(define-class internal-page (command)
((dynamic-title ; Not `title' so that it does not clash with other `title' methods.
Expand Down Expand Up @@ -333,22 +330,14 @@ See `find-internal-page-buffer'."))
(t
(format nil "*~a*" (string-downcase (name page)))))))

(defun internal-page-name (url)
(when (string= "nyxt" (quri:uri-scheme url))
(uiop:safe-read-from-string
(str:upcase (quri:uri-path url)) :package :nyxt)))

;; (-> find-internal-page-buffer (internal-page-symbol) (maybe buffer))
(defun find-internal-page-buffer (name) ; TODO: Test if CCL can catch bad calls at compile-time.
"Return first buffer which URL is a NAME `internal-page'."
(find name (buffer-list) :key (compose #'internal-page-name #'url)))

(defun find-url-internal-page (url)
"Return the `internal-page' to which URL corresponds."
(and (equal "nyxt" (quri:uri-scheme url))
(gethash
(internal-page-name url)
*nyxt-url-commands*)))
(gethash (internal-page-name url) *nyxt-url-commands*))

(export-always 'buffer-load-internal-page-focus)
(defun buffer-load-internal-page-focus (name &rest args)
Expand Down
Loading