~soapdog/fafi-browser

755fea7fe0aba7b6221b3f68265d6cea82a5d8fd — Andre Alves Garzia 2 years ago 08ceb8f
Added support for "text://" URLs.

Cue https://textprotocol.org
M multi-renderer.rkt => multi-renderer.rkt +7 -4
@@ 6,6 6,7 @@
  "renderers/markdown-renderer.rkt"
  "renderers/image-renderer.rkt"
  "renderers/feed-renderer.rkt"
  "config.rkt"
  "utils.rkt")

   


@@ 68,13 69,15 @@
      (for-each (lambda (r) (send r hide)) renderers)) ;; hide all children.
                                 

    (define/public (get-renderer-for-mime mime [content #f])
    (define/public (get-renderer-for-mime mime [content #f] [scheme "wrong"]) 
      (findf (lambda (r)
               (send r can-handle-mime? mime content))
               (when (debug-mode?)
                  (printf "checking if ~a can handle schema '~a://' url with mime: '~a'~n" r scheme mime))
               (send r can-handle-mime? mime content scheme))
             renderers))

    (define/public (render mime content)
      (define renderer (get-renderer-for-mime mime))
    (define/public (render mime content scheme)
      (define renderer (get-renderer-for-mime mime content scheme))
      (if (false? renderer)
          (on-error (format "Error: can't render mime: ~a" mime))
          (begin

M multi-transport.rkt => multi-transport.rkt +7 -0
@@ 5,6 5,7 @@
  "transports/gemini-transport.rkt"
  "transports/web-transport.rkt"
  "transports/mercury-transport.rkt"
  "transports/text-transport.rkt"
  "utils.rkt")
 
   


@@ 36,6 37,12 @@
       (new mercury-transport%
            [on-status-change on-status-change]
            [on-error on-error]
            [on-request-complete on-request-complete])
       ;; Text transport can handle mercury protocol
       ;; Schemas: text://
       (new text-transport%
            [on-status-change on-status-change]
            [on-error on-error]
            [on-request-complete on-request-complete])))

    (define/public (get-transport-for-url url)

A protocols/text-protocol.rkt => protocols/text-protocol.rkt +173 -0
@@ 0,0 1,173 @@
#lang racket

(require net/url
         racket/tcp
         racket/pretty
         racket/struct)

(define text-default-port 1961)

(struct text-response (status meta body)
  #:mutable 
  #:methods gen:custom-write
  [(define write-proc
     (make-constructor-style-printer
      (lambda (obj) 'text-response)
      (lambda (obj) (list (text-response-status obj) (text-response-meta obj) (text-response-body obj)))))])

(define (make-text-response status meta [body #f])
  (text-response status meta body))

(struct text-conn (host port in out) #:mutable)
(define (make-text-conn) (text-conn #f #f #f #f))

(define (text-conn-live? gc)
  (define in (text-conn-in gc))
  (define out (text-conn-out gc))
  (and in (port-closed? in)
       out (port-closed? out)
       #t))
 
(define (ensure-default-port url)
  (if (not (url-port url))
      (begin
        (set-url-port! url text-default-port)
        url)
      url))

(define (ensure-text-protocol url)
  (cond
    ;; I don't think the case below can actually happen... 
    [(false? (url-host url)) (error 'ensure-text-protocol "No host in url: ~a" (url->string url))]
    ;; Below is a very complicated dance to prepend mercury:// if the URL contains no protocol.
    [(false? (url-scheme url)) (let*
                                   ([url-as-string (url->string url)]
                                    [url-as-string-with-protocol (string-append "mercury://" url-as-string)])
                                 (ensure-text-protocol (string->url url-as-string-with-protocol)))]
    [(not (equal? (url-scheme url) "text")) (error 'ensure-text-protocol "can't request non-text protocol: ~a" (url-scheme url))]
   
    [else url]))

(define (text-conn-open requested-url)
  (define url ((compose1 ensure-default-port ensure-text-protocol string->url) requested-url))
  (define-values (in out) (tcp-connect/enable-break (url-host url) (url-port url)))
  (define gc (make-text-conn))
  (set-text-conn-in! gc in)
  (set-text-conn-out! gc out)
  (set-text-conn-host! gc (url-host url))
  (set-text-conn-port! gc (url-port url))
  gc)

(define (text-conn-close! gc)
  (match-define (text-conn host port in out) gc)
  (when out
    (close-output-port out)
    (set-text-conn-out! gc #f))
  (when in
    (close-input-port in)
    (set-text-conn-in! gc #f)))

(define (write-out out str)
  (fprintf out "~a\r\n" str)
  (flush-output out))

(define (text-recv! gc)
  (define-values (status meta) (text-conn-header! gc))
  (text-response
   status
   meta
   (text-conn-body! gc status meta)))

(define (text-conn-header! gc)
  (define header-line (read-line (text-conn-in gc) 'return-linefeed))
  (define header-list (regexp-split #rx" +" header-line))
  (values (string->number (car header-list)) (string-join (cdr header-list))))


(define (text-conn-body! gc status meta)
  (cond
    [(and (equal? status 20) (string-prefix? meta "text/plain")) (text-conn-body/parse-text! gc)]
    [(and (equal? status 20) (string-prefix? meta "text/xml")) (text-conn-body/parse-binary! gc)]
    [(and (equal? status 20) (string-prefix? meta "text/")) (text-conn-body/parse-generic! gc)]
    [(and (equal? status 20) (string-prefix? meta "image/")) (text-conn-body/parse-binary! gc)]
    [(equal? status 20) (text-conn-body/parse-binary! gc)] ;; Catch all for successful transfers. Just delegate to multi-renderer.
    [else #f]))
  

(define (text-conn-body/parse-generic! gc)
  (define (read-loop acc)
    (define (read) (read-line (text-conn-in gc) 'any))
    (define line (read))
    (if (not (eof-object? line))
        (read-loop (cons line acc))
        (string-join (reverse acc) "\n")))
  (read-loop '()))

(define (text-conn-body/parse-binary! gc)
  (port->bytes (text-conn-in gc)))

(define (text-conn-body/parse-text! gc)
  (define (read-loop acc)
    (define (read) (read-line (text-conn-in gc) 'any))
    (define line (read))
    (if (not (eof-object? line))
        (read-loop (cons (text-process-line line) acc))
        (reverse acc)))
  (read-loop '()))

(define line-toggled #f) ;; I don't think this is thread-safe. Ideally, this can be a part of conn.

(define (text-process-line line)
  (define first-word (car (regexp-split #rx" " line)))
  (define (link? w) (equal? w "=>"))
  (define (heading? w) (and (> (string-length w) 0) (equal? (substring w 0 1) "#")))
  (define (list? w) (equal? w "*"))
  (define (toggle? w) (equal? w "```"))
  (define (make-link line)
    (define lst (string-split line))
    `(link ,(cadr lst) ,(string-join (cddr lst))))
  (define (make-heading line)
    (define lst (string-split line))
    `(heading ,(string-length (car lst)) ,(string-join (cdr lst))))
  (define (make-list line)
    (define lst (string-split line))
    `(list ,(string-join (cdr lst))))
  (cond
    [(link? first-word) (make-link line)]
    [(heading? first-word) (make-heading line)]
    [(list? first-word) (make-list line)]
    [(equal? line "") `(break)]
    [(toggle? first-word) 
     (begin
       (set! line-toggled (not line-toggled))
       `(preformatted ,line-toggled))]
    [else `(text ,line)]))

(define (text-request url #:follow-redirects [fr #f] [how-many-redirects-so-far 0])
  (when  (> how-many-redirects-so-far 4)
    (error 'text-request "too many redirects"))
  (define gc (text-conn-open url))
  (write-out (text-conn-out gc) url)
  (define response
    (begin0
      (text-recv! gc)
      (text-conn-close! gc)))
  (if (and (member (text-response-status response) (list 30)) fr)
      (text-request (text-response-meta response) #:follow-redirects #t (add1 how-many-redirects-so-far))
      response))

(define (text-valid-status-code status)
  (list? (member status '(20 30 40))))

(define (gemtext->gemexpr str)
  (map (lambda (l) (text-process-line l)) (string-split str "\n"))) 

(provide
 text-request
 text-valid-status-code
 ensure-text-protocol
 gemtext->gemexpr)

(provide (struct-out text-response))

  

M renderers/feed-renderer.rkt => renderers/feed-renderer.rkt +1 -1
@@ 150,7 150,7 @@
      (render-html-to-text (feed->html content) text #t #f)
      (send pane show #t))
    
    (define/public (can-handle-mime? mime content)
    (define/public (can-handle-mime? mime content schema)
      (or
       (matches-mime "application/rss+xml" mime)
       (matches-mime "application/atom+xml" mime)

M renderers/gemini-renderer.rkt => renderers/gemini-renderer.rkt +4 -2
@@ 170,8 170,10 @@
      (display-gemtext content)
      (send pane show #t))

    (define/public (can-handle-mime? mime content)
      (matches-mime "text/gemini" mime))
    (define/public (can-handle-mime? mime content schema)
      (or 
        (matches-mime "text/gemini" mime)
        (and (equal? schema "text") (matches-mime "text/plain" mime))))

    ))


M renderers/image-renderer.rkt => renderers/image-renderer.rkt +1 -1
@@ 46,7 46,7 @@
      (send msg set-label img)
      (send pane show #t))
    
    (define/public (can-handle-mime? mime content)
    (define/public (can-handle-mime? mime content schema)
      (string-prefix? mime "image/"))
    
    ))

M renderers/markdown-renderer.rkt => renderers/markdown-renderer.rkt +1 -1
@@ 348,7 348,7 @@
      (send text hide-caret #t)
      (send pane show #t))
    
    (define/public (can-handle-mime? mime content)
    (define/public (can-handle-mime? mime content schema)
      (or (matches-mime "text/plain" mime) (matches-mime "text/markdown" mime)))))

(provide markdown-renderer%)

A transports/text-transport.rkt => transports/text-transport.rkt +48 -0
@@ 0,0 1,48 @@
#lang racket/gui

(require
  net/url
  "../protocols/text-protocol.rkt"
  "../utils.rkt")

    
   
(define text-transport%
  (class object%
    (super-new)
        
    (init-field
     [on-status-change (void)]
     [on-request-complete (void)]
     [on-error (void)])

    (define/public (can-handle-url? url)
      (define u (string->url url))
      (equal? (url-scheme u) "text"))

    (define/public (fetch new-url [current-url #f])
      (begin
        (when (not (non-empty-string? new-url))
          (error 'set-url "Can't load empty URL"))
        (when (not (false? current-url))
          (define new-url-url (string->url new-url))
          (set! new-url (url->string
                         (ensure-absolute-url
                          new-url
                          current-url
                          (url-scheme new-url-url)))))
          
        (on-status-change (format "Requesting ~a" new-url))
        
        (define last-response (text-request new-url #:follow-redirects #t))
        (define status (text-response-status last-response))
        (define meta (text-response-meta last-response))
        (define body (text-response-body last-response))
        (cond
            [(equal? status 51) (on-error (format "Error: ~a not found" new-url))]
            [(not (text-valid-status-code status)) (on-error (format "Error: invalid status code: ~a" status))]
            [else (on-request-complete new-url
                              meta
                              body)])))))

(provide text-transport%)

M window.rkt => window.rkt +13 -8
@@ 18,7 18,7 @@
(define history-backward '())
(define history-forward '())

(struct tab (url mime content)
(struct tab (url mime content scheme)
  #:methods gen:custom-write
  [(define write-proc
     (make-constructor-style-printer


@@ 80,7 80,7 @@
           (send this on-system-menu-char event)
           (send this on-traverse-char event))))

   [label (format "Fafi (~a) is a Gemini Browser" program-version)]
   [label (format "Fafi (~a) is a Smol Browser" program-version)]
   [width 900]
   [height 700]
   [x 100]


@@ 183,7 183,7 @@
  (send browser-tabs set-selection (sub1 (send browser-tabs get-number)))
  (when (not (false? url))
    (set! url (url->string (ensure-absolute-url url current-url (url-scheme (string->url current-url))))))
  (set! tabs (append tabs (list (tab url #f #f))))
  (set! tabs (append tabs (list (tab url #f #f (url-scheme (string->url current-url))))))
  (when (false? url)
    (send address-bar focus))
  (switch-tab (send browser-tabs get-selection)))


@@ 202,9 202,9 @@
      (send address-bar set-value (tab-url tab-n))
      (send address-bar set-value ""))

  (cond [(not (false? (tab-content tab-n))) (send multi-renderer render (tab-mime tab-n) (tab-content tab-n))]
  (cond [(not (false? (tab-content tab-n))) (send multi-renderer render (tab-mime tab-n) (tab-content tab-n) (tab-scheme tab-n))]
        [(not (false? (tab-url tab-n))) (send multi-transport fetch (tab-url tab-n))]
        [else (send multi-renderer render "text/gemini" (new-tab-template))]))
        [else (send multi-renderer render "text/gemini" (new-tab-template) "fafi")]))

(define (close-tab n)
  (when (> (length tabs) 1)


@@ 228,10 228,11 @@
(define (request-complete url mime content)
  (when (debug-mode?)
    (displayln (format "loaded ~a" url)))
  (define scheme (url-scheme (string->url url)))
  (send address-bar set-value url)
  (send browser-tabs set-item-label (send browser-tabs get-selection) url)

  (set! tabs (list-set tabs (send browser-tabs get-selection) (tab url mime content)))
  (set! tabs (list-set tabs (send browser-tabs get-selection) (tab url mime content scheme)))

  (when (or (empty? history-backward) (not (equal? url (car history-backward))))
    (set! history-backward (cons url history-backward)))


@@ 239,7 240,11 @@
  (set! current-url url)
  (status-change (format "loaded: ~a mime: ~a" url mime))

  (send multi-renderer render mime content))
  (when (debug-mode?)
    (printf "window looking for renderer for scheme: ~a~n" scheme))


  (send multi-renderer render mime content scheme))

(define (error-handler msg)
  (status-change "An error has happened.")


@@ 439,5 444,5 @@
    (send frame show #t)
    (send frame create-status-line)
    (send browser-tabs set-item-label 0 capsule)
    (set! tabs (append tabs (list (tab capsule #f #f))))
    (set! tabs (append tabs (list (tab capsule #f #f #f))))
    (send multi-transport fetch capsule)))