Add homepage, architecture changes

* Create homepage
* Page data is automatically reloaded (except when compiling)
* Entrypoint is breezewiki.rkt for running and dist.rkt for compiling
* Include stack trace when sending error messages
This commit is contained in:
Cadence Ember 2022-08-30 21:33:28 +12:00
parent db4691f56c
commit 301636d597
Signed by: cadence
GPG key ID: BC1C2C61CF521B17
10 changed files with 486 additions and 13 deletions

3
.gitignore vendored
View file

@ -10,6 +10,9 @@
# Compiled
compiled
/breezewiki
/dist
/breezewiki-dist
# Personal
/config.ini

View file

@ -10,23 +10,33 @@
(prefix-in filter: web-server/dispatchers/dispatch-filter)
(prefix-in files: web-server/dispatchers/dispatch-files)
"src/config.rkt"
"src/page-category.rkt"
"src/page-not-found.rkt"
"src/page-proxy.rkt"
"src/page-wiki.rkt"
"src/page-search.rkt")
"src/reloadable.rkt"
"src/server-utils.rkt")
(define-syntax-rule (require-reloadable filename varname)
(define varname
(reloadable-entry-point->procedure
(make-reloadable-entry-point (quote varname) filename))))
(require-reloadable "src/page-category.rkt" page-category)
(require-reloadable "src/page-home.rkt" page-home)
(require-reloadable "src/page-not-found.rkt" page-not-found)
(require-reloadable "src/page-proxy.rkt" page-proxy)
(require-reloadable "src/page-search.rkt" page-search)
(require-reloadable "src/page-wiki.rkt" page-wiki)
(when (not (config-true? 'debug))
(set-reload-poll-interval! #f))
(reload!)
(define-runtime-path path-static "static")
(define mime-types
(hash #".css" #"text/css"
#".svg" #"image/svg+xml"))
(serve/launch/wait
#:listen-ip (if (config-true? 'debug) "127.0.0.1" #f)
#:port (string->number (config-get 'port))
(λ (quit)
(sequencer:make
(pathprocedure:make "/" page-home)
(pathprocedure:make "/proxy" page-proxy)
(filter:make #rx"^/[a-z-]+/wiki/Category:.+$" (lift:make page-category))
(filter:make #rx"^/[a-z-]+/wiki/.+$" (lift:make page-wiki))
@ -38,6 +48,6 @@
(struct-copy url u [path (cdr (url-path u))])))
#:path->mime-type
(lambda (u)
(hash-ref mime-types (path-get-extension u)))
(ext->mime-type (path-get-extension u)))
#:cache-no-cache (config-true? 'debug) #;"browser applies heuristics if unset"))
(lift:make page-not-found))))

43
dist.rkt Normal file
View file

@ -0,0 +1,43 @@
#lang racket/base
(require racket/path
racket/runtime-path
net/url
web-server/servlet-dispatch
web-server/dispatchers/filesystem-map
(prefix-in pathprocedure: web-server/dispatchers/dispatch-pathprocedure)
(prefix-in sequencer: web-server/dispatchers/dispatch-sequencer)
(prefix-in lift: web-server/dispatchers/dispatch-lift)
(prefix-in filter: web-server/dispatchers/dispatch-filter)
(prefix-in files: web-server/dispatchers/dispatch-files)
"src/config.rkt"
"src/server-utils.rkt")
(require (only-in "src/page-category.rkt" page-category))
(require (only-in "src/page-home.rkt" page-home))
(require (only-in "src/page-not-found.rkt" page-not-found))
(require (only-in "src/page-proxy.rkt" page-proxy))
(require (only-in "src/page-search.rkt" page-search))
(require (only-in "src/page-wiki.rkt" page-wiki))
(define-runtime-path path-static "static")
(serve/launch/wait
#:listen-ip (if (config-true? 'debug) "127.0.0.1" #f)
#:port (string->number (config-get 'port))
(λ (quit)
(sequencer:make
(pathprocedure:make "/" page-home)
(pathprocedure:make "/proxy" page-proxy)
(filter:make #rx"^/[a-z-]+/wiki/Category:.+$" (lift:make page-category))
(filter:make #rx"^/[a-z-]+/wiki/.+$" (lift:make page-wiki))
(filter:make #rx"^/[a-z-]+/search$" (lift:make page-search))
(filter:make #rx"^/static/" (files:make
#:url->path
(lambda (u)
((make-url->path path-static)
(struct-copy url u [path (cdr (url-path u))])))
#:path->mime-type
(lambda (u)
(ext->mime-type (path-get-extension u)))
#:cache-no-cache (config-true? 'debug) #;"browser applies heuristics if unset"))
(lift:make page-not-found))))

77
src/page-home.rkt Normal file
View file

@ -0,0 +1,77 @@
#lang racket/base
(require html-writing
web-server/http
"xexpr-utils.rkt"
"config.rkt")
(provide
page-home)
(module+ test
(require rackunit))
(define examples
'(("crosscode" "CrossCode_Wiki")
("minecraft" "Bricks")
("undertale" "Hot_Dog...%3F")
("tardis" "Eleanor_Blake")
("fireemblem" "God-Shattering_Star")
("fallout" "Pip-Boy_3000")))
(define content
`((h2 "BreezeWiki makes wiki pages on Fandom readable")
(p "It removes ads, videos, and suggested content, leaving you with a clean page that doesn't consume all your data.")
(p "If you're looking for an \"alternative\" to Fandom for writing pages, you should look elsewhere. BreezeWiki only lets you read existing pages.")
(p "BreezeWiki can also be called an \"alternative frontend for Fandom\".")
(h2 "Example pages")
(ul
,@(map (λ (x)
`(li (a (@ (href ,(apply format "/~a/wiki/~a" x)))
,(apply format "~a: ~a" x))))
examples))
(h2 "How to use")
(p "While browsing any page on Fandom, you can replace \"fandom.com\" in the address bar with \"breezewiki.com\" to see the BreezeWiki version of that page.")
(p "After that, you can click the links to navigate around the pages.")
(p "To get back to Fandom, click the link that's at the bottom of the page.")))
(define body
`(html
(head
(meta (@ (name ")viewport") (content "width=device-width, initial-scale=1")))
(title "About | BreezeWiki")
(link (@ (rel "stylesheet") (type "text/css") (href "/static/internal.css")))
(link (@ (rel "stylesheet") (type "text/css") (href "/static/main.css"))))
(body (@ (class "skin-fandomdesktop theme-fandomdesktop-light internal"))
(div (@ (class "main-container"))
(div (@ (class "fandom-community-header__background tileBoth header")))
(div (@ (class "page"))
(main (@ (class "page__main"))
(div (@ (class "custom-top"))
(h1 (@ (class "page-title"))
"About BreezeWiki"))
(div (@ (id "content") #;(class "page-content"))
(div (@ (id "mw-content-text"))
,@content))
(footer (@ (class "custom-footer"))
(div (@ (class "internal-footer"))
(img (@ (class "my-logo") (src "/static/breezewiki.svg")))
,(if (config-get 'instance-is-official)
`(p ,(format "This instance is run by the ~a developer, " (config-get 'application-name))
(a (@ (href "https://cadence.moe/contact"))
"Cadence."))
`(p
,(format "This unofficial instance is based off the ~a source code, but is not controlled by the code developer." (config-get 'application-name))))
(p "Text content on wikis run by Fandom is available under the Creative Commons Attribution-Share Alike License 3.0 (Unported), "
(a (@ (href "https://www.fandom.com/licensing")) "see license info.")
" Media files and official Fandom documents have different copying restrictions.")
(p ,(format "Fandom is a trademark of Fandom, Inc. ~a is not affiliated with Fandom." (config-get 'application-name)))))))))))
(module+ test
(check-not-false (xexp->html body)))
(define (page-home req)
(response/output
#:code 200
(λ (out)
(write-html body out))))

View file

@ -69,7 +69,7 @@
(define data (easy:response-json dest-res))
(define body (generate-results-page dest-url wikiname query data))
(when (config-get 'debug)
(when (config-true? 'debug)
; used for its side effects
; convert to string with error checking, error will be raised if xexp is invalid
(xexp->html body))

150
src/reloadable.rkt Normal file
View file

@ -0,0 +1,150 @@
#lang racket/base
;;; Source: https://github.com/tonyg/racket-reloadable/blob/master/reloadable/main.rkt
;;; Source commit: cae2a14 from 24 May 2015
;;; Source license: LGPL 3 or later
(provide (struct-out reloadable-entry-point)
reload-poll-interval
set-reload-poll-interval!
reload-failure-retry-delay
reload!
make-reloadable-entry-point
lookup-reloadable-entry-point
reloadable-entry-point->procedure
make-persistent-state)
(require racket/set)
(require racket/string)
(require racket/match)
(require racket/rerequire)
(define reload-poll-interval 0.5) ;; seconds
(define reload-failure-retry-delay (make-parameter 5)) ;; seconds
(struct reloadable-entry-point (name
module-path
identifier-symbol
on-absent
[value #:mutable])
#:prefab)
(define reloadable-entry-points (make-hash))
(define persistent-state (make-hash))
(define (set-reload-poll-interval! v)
(set! reload-poll-interval v))
(define (reloader-main)
(let loop ()
(match (sync (handle-evt (thread-receive-evt)
(lambda (_) (thread-receive)))
(if reload-poll-interval
(handle-evt (alarm-evt (+ (current-inexact-milliseconds)
(* reload-poll-interval 1000)))
(lambda (_) (list #f 'reload)))
never-evt))
[(list ch 'reload)
(define result (do-reload!))
(when (not result) (sleep (reload-failure-retry-delay)))
(when ch (channel-put ch result))])
(loop)))
(define reloader-thread (thread reloader-main))
(define (reloader-rpc . request)
(define ch (make-channel))
(thread-send reloader-thread (cons ch request))
(channel-get ch))
(define (reload!) (reloader-rpc 'reload))
(define first-load? #t)
(define (say-loading-once! port)
(when first-load?
(display "loading support files" port)
(set! first-load? #f)))
(define (handle-loader-output)
(define i (thread-receive))
(define real-error-port (thread-receive))
(say-loading-once! real-error-port)
(let loop ()
(let ([line (read-line i)])
(cond
[(eof-object? line)
(void)]
[(string-contains? line "[load")
(display "." real-error-port)
(loop)]
[#t
(displayln line real-error-port)
(loop)]))))
;; Only to be called from reloader-main
(define (do-reload!)
(define module-paths (for/set ((e (in-hash-values reloadable-entry-points)))
(reloadable-entry-point-module-path e)))
(with-handlers ((exn:fail?
(lambda (e)
(log-error "*** WHILE RELOADING CODE***\n~a"
(parameterize ([current-error-port (open-output-string)])
((error-display-handler) (exn-message e) e)
(get-output-string (current-error-port))))
#f)))
(for ((module-path (in-set module-paths)))
(let ([real-error-port (current-error-port)])
(define-values (i o) (make-pipe))
(parameterize ([current-error-port o])
(define new-thread (thread handle-loader-output))
(thread-send new-thread i)
(thread-send new-thread real-error-port)
(dynamic-rerequire module-path #:verbosity 'all))))
(for ((e (in-hash-values reloadable-entry-points)))
(match-define (reloadable-entry-point _ module-path identifier-symbol on-absent _) e)
(define new-value (if on-absent
(dynamic-require module-path identifier-symbol on-absent)
(dynamic-require module-path identifier-symbol)))
(set-reloadable-entry-point-value! e new-value))
#t))
(define (make-reloadable-entry-point name module-path [identifier-symbol name]
#:on-absent [on-absent #f])
(define key (list module-path name))
(hash-ref reloadable-entry-points
key
(lambda ()
(define e (reloadable-entry-point name module-path identifier-symbol on-absent #f))
(hash-set! reloadable-entry-points key e)
e)))
(define (lookup-reloadable-entry-point name module-path)
(hash-ref reloadable-entry-points
(list module-path name)
(lambda ()
(error 'lookup-reloadable-entry-point
"Reloadable-entry-point ~a not found in module ~a"
name
module-path))))
(define (reloadable-entry-point->procedure e)
(make-keyword-procedure
(lambda (keywords keyword-values . positionals)
(keyword-apply (reloadable-entry-point-value e)
keywords
keyword-values
positionals))))
(define (make-persistent-state name initial-value-thunk)
(hash-ref persistent-state
name
(lambda ()
(define value (initial-value-thunk))
(define handler
(case-lambda
[() value]
[(new-value)
(set! value new-value)
value]))
(hash-set! persistent-state name handler)
handler)))

16
src/server-utils.rkt Normal file
View file

@ -0,0 +1,16 @@
#lang racket/base
(provide
ext->mime-type)
(module+ test
(require rackunit))
(define hash-ext-mime-type
(hash #".css" #"text/css"
#".svg" #"image/svg+xml"
#".png" #"image/png"))
(define (ext->mime-type ext)
(hash-ref hash-ext-mime-type ext))
(module+ test
(check-equal? (ext->mime-type #".png") #"image/png"))

View file

@ -195,6 +195,7 @@
#:mime-type #"text/plain"
(λ (out)
(for ([port (list (current-output-port) out)])
(displayln "Exception raised in Racket code at response generation time:" port)
(displayln (exn-message e) port)))))])
(parameterize ([current-error-port out])
(displayln "Exception raised in Racket code at response generation time:" (current-error-port))
((error-display-handler) (exn-message e) e))))))])
body ...))

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

173
static/internal.css Normal file
View file

@ -0,0 +1,173 @@
:root {
--theme-body-dynamic-color-1: #fff;
--theme-body-dynamic-color-1--rgb: 255,255,255;
--theme-body-dynamic-color-2: #e6e6e6;
--theme-body-dynamic-color-2--rgb: 230,230,230;
--theme-page-dynamic-color-1: #000;
--theme-page-dynamic-color-1--rgb: 0,0,0;
--theme-page-dynamic-color-1--inverted: #fff;
--theme-page-dynamic-color-1--inverted--rgb: 255,255,255;
--theme-page-dynamic-color-2: #3a3a3a;
--theme-page-dynamic-color-2--rgb: 58,58,58;
--theme-sticky-nav-dynamic-color-1: #000;
--theme-sticky-nav-dynamic-color-1--rgb: 0,0,0;
--theme-sticky-nav-dynamic-color-2: #3a3a3a;
--theme-sticky-nav-dynamic-color-2--rgb: 58,58,58;
--theme-link-dynamic-color-1: #000;
--theme-link-dynamic-color-1--rgb: 0,0,0;
--theme-link-dynamic-color-2: #3a3a3a;
--theme-link-dynamic-color-2--rgb: 58,58,58;
--theme-accent-dynamic-color-1: #fff;
--theme-accent-dynamic-color-1--rgb: 255,255,255;
--theme-accent-dynamic-color-2: #e6e6e6;
--theme-accent-dynamic-color-2--rgb: 230,230,230;
--theme-body-background-color: #286cab;
--theme-body-background-color--rgb: 40,108,171;
--theme-body-text-color: #fff;
--theme-body-text-color--rgb: 255,255,255;
--theme-body-text-color--hover: #cccccc;
--theme-sticky-nav-background-color: #ffffff;
--theme-sticky-nav-background-color--rgb: 255,255,255;
--theme-sticky-nav-text-color: #000;
--theme-sticky-nav-text-color--hover: #333333;
--theme-page-background-color: #ffffff;
--theme-page-background-color--rgb: 255,255,255;
--theme-page-background-color--secondary: #f2f2f2;
--theme-page-background-color--secondary--rgb: 242,242,242;
--theme-page-text-color: #3a3a3a;
--theme-page-text-color--rgb: 58,58,58;
--theme-page-text-color--hover: #6d6d6d;
--theme-page-text-mix-color: #9d9d9d;
--theme-page-text-mix-color-95: #f5f5f5;
--theme-page-accent-mix-color: #b6b6b6;
--theme-page-headings-font: 'Rubik';
--theme-link-color: #8a8a8a;
--theme-link-color--rgb: 138,138,138;
--theme-link-color--hover: #565656;
--theme-link-label-color: #000;
--theme-accent-color: #6c6c6c;
--theme-accent-color--rgb: 108,108,108;
--theme-accent-color--hover: #9f9f9f;
--theme-accent-label-color: #fff;
--theme-border-color: #cecece;
--theme-border-color--rgb: 206,206,206;
--theme-alert-color: #bf0017;
--theme-alert-color--rgb: 191,0,23;
--theme-alert-color--hover: #59000a;
--theme-alert-color--secondary: #bf0017;
--theme-alert-label: #fff;
--theme-warning-color: #cf721c;
--theme-warning-color--rgb: 207,114,28;
--theme-warning-color--secondary: #ce711b;
--theme-warning-label: #000;
--theme-success-color: #0c742f;
--theme-success-color--rgb: 12,116,47;
--theme-success-color--secondary: #0c742f;
--theme-success-label: #fff;
--theme-message-color: #753369;
--theme-message-label: #fff;
--theme-community-header-color: #000000;
--theme-community-header-color--hover: #333333;
--theme-background-image-opacity: 100%;
--theme-page-text-opacity-factor: 0.85;
--theme-body-text-opacity-factor: 0.7;
}
.skin-fandomdesktop .CodeMirror{
--codemirror-yellow: #a88d00;
--codemirror-light-blue: #0096fb;
--codemirror-blue: #08f;
--codemirror-green: #290;
--codemirror-red: #f50;
--codemirror-dark-red: #a11;
--codemirror-purple: #80c;
--codemirror-pink: #e0e;
--codemirror-light-gray: #929292;
--codemirror-gray: #789797;
}
.mw-highlight {
--pygments-background: #f4f3f4;
--pygments-err: #f00;
--pygments-c: #408080;
--pygments-k: #008000;
--pygments-o: #666;
--pygments-ch: #408080;
--pygments-cm: #408080;
--pygments-cp: #b17300;
--pygments-cpf: #408080;
--pygments-c1: #408080;
--pygments-cs: #408080;
--pygments-gd: #a00000;
--pygments-gr: #f00;
--pygments-gh: #000080;
--pygments-gi: #009500;
--pygments-go: #808080;
--pygments-gp: #000080;
--pygments-gu: #800080;
--pygments-gt: #04d;
--pygments-kc: #008000;
--pygments-kd: #008000;
--pygments-kn: #008000;
--pygments-kp: #008000;
--pygments-kr: #008000;
--pygments-kt: #b00040;
--pygments-m: #666;
--pygments-s: #ba2121;
--pygments-na: #768826;
--pygments-nb: #008000;
--pygments-nc: #00f;
--pygments-no: #800;
--pygments-nd: #a2f;
--pygments-ni: #7f7f7f;
--pygments-ne: #d2413a;
--pygments-nf: #00f;
--pygments-nl: #818100;
--pygments-nn: #00f;
--pygments-nt: #008000;
--pygments-nv: #19177c;
--pygments-ow: #a2f;
--pygments-w: #808080;
--pygments-mb: #666;
--pygments-mf: #666;
--pygments-mh: #666;
--pygments-mi: #666;
--pygments-mo: #666;
--pygments-sa: #ba2121;
--pygments-sb: #ba2121;
--pygments-sc: #ba2121;
--pygments-dl: #ba2121;
--pygments-sd: #ba2121;
--pygments-s2: #ba2121;
--pygments-se: #b62;
--pygments-sh: #ba2121;
--pygments-si: #b68;
--pygments-sx: #008000;
--pygments-sr: #b68;
--pygments-s1: #ba2121;
--pygments-ss: #19177c;
--pygments-bp: #008000;
--pygments-fm: #00f;
--pygments-vc: #19177c;
--pygments-vg: #19177c;
--pygments-vi: #19177c;
--pygments-vm: #19177c;
--pygments-il: #666;
}
body {
background: linear-gradient(to bottom, rgba(225, 133, 155, 0) 15%, rgba(225, 133, 155, 0.8)), url(/static/internal-background.png);
min-height: 100vh;
padding: 4px;
margin: 0;
}
.page__main {
border-radius: 3px;
padding: 24px 36px;
}
.internal-footer {
max-width: 700px;
}