" + "
BLah" + " italic bold ened still < bold " + "
But not done yet...")))
+ (racketresultblock
+ (*TOP* (html (head (title) (title "whatever"))
+ (body "\n"
+ (a (@ (href "url")) "link")
+ (p (@ (align "center"))
+ (ul (@ (compact) (style "aa")) "\n"))
+ (p "BLah"
+ (*COMMENT* " comment
") `(*TOP* (hr)))
+ (test (html->xexp "
") `(*TOP* (hr)))
+ (test (html->xexp "
") `(*TOP* (hr)))
+
+ (test (html->xexp "
")
+ `(*TOP* (hr (@ (noshade)))))
+ (test (html->xexp "
")
+ `(*TOP* (hr (@ (noshade)))))
+ (test (html->xexp "
")
+ `(*TOP* (hr (@ (noshade)))))
+ (test (html->xexp "
")
+ `(*TOP* (hr (@ (noshade)))))
+ (test (html->xexp "
")
+ `(*TOP* (hr (@ (noshade "1")))))
+ (test (html->xexp "
")
+ `(*TOP* (hr (@ (noshade "1/")))))
+
+ (test (html->xexp "aaabbb
ccc
BLah italic bold ened " + " still < bold
But not done yet..."))
+ `(*TOP*
+ (html (head (title) (title "whatever"))
+ (body (a (@ (href "url")) "link")
+ (p (@ (align "center"))
+ (ul (@ (compact) (style "aa"))))
+ (p "BLah"
+ (*COMMENT* " comment b a&z a&aposz a<z a>z a"z a&rarrz AT&T Bell Labs a&z a'z a<z a>z a"z B D text in details foo bar")
+ '(*TOP* (html (p "foo") (p "bar"))))
+
+ (test 'non-p-parent-in-which-p-shouldnt-nest
+ (html->xexp " bar")
+ '(*TOP* (html (xyz "foo") (p "bar"))))
+
+ (test 'p-shouldnt-nest-even-without-html-element
+ (html->xexp " foo bar")
+ '(*TOP* (p "foo") (p "bar"))
+ #:fail "see code comment about parent-constraints when no top elements"))
+
+ (test-section 'break-the-world-with-h-elem-mid-90s-handling
+
+ (test 'initial
+ (html->xexp " foo words ")
+ " was used as a separator or terminator, rather than a
+start delimeter. There is a chance that change this will break a real-world
+scraper or other tool.")))
+
+ (#:planet 7:1 #:date "2022-04-02"
+ (itemlist
+ (item "Include a test case #:fail that was unsaved in DrRacket.")))
+
+ (#:planet 7:0 #:date "2022-04-02"
+ (itemlist
+
+ (item "Fixed a quirks-handling bug in which "
+ (code "p")
+ " elements would be (directly or indirectly) nested under other "
+ (code "p")
+ " elements in cases in which there was no "
+ (code "body")
+ " element, but there was an "
+ (code "html")
+ " element. (Thanks to Jonathan Simpson for reporting.)")))
+
+ (#:planet 6:1 #:date "2022-01-22"
+ (itemlist
+ (item "Permit "
+ (code "details")
+ " element to be parent of "
+ (code "p")
+ " element in quirks handling. (Thanks to Jacder for reporting.)")))
+
+ (#:planet 6:0 #:date "2018-05-22"
+ (itemlist
+ (item "Fix to permit "
+ (code "p")
+ " elements as children of "
+ (code "blockquote")
+ " elements. Incrementing major version number because
+this is a breaking change of 17 years, but seems an appropriate change for
+modern HTML, and fixes a particular real-world problem. (Thanks to
+Sorawee Porncharoenwase for reporting.)")))
+
+ (#:planet 5:0 #:date "2018-05-15"
+ (itemlist
+ (item "In a breaking change of handing invalid HTML, most named
+character entity references that are invalid because (possibly among multiple
+reasons) they are not terminated by semicolon, now are treated as literal
+strings (including the ampersand), rather than as named character entites. For
+example, parser input string "
+ (racket " A&B Co. ")
+ " will now parse as "
+ (racket (p "A&B Co."))
+ " rather than as "
+ (racket (p "A" (& B) " Co."))
+ ". (Thanks for Greg Hendershott for suggesting this, and discussing.)")
+ (item "For support of historical quirks handling, five early
+HTML named character entities (specifically, "
+ (tt "amp")
+ ", "
+ (tt "apos")
+ ", "
+ (tt "lt")
+ ", "
+ (tt "gt")
+ ", "
+ (tt "quot")
+ ") do not need to be terminated with a semicolon, and
+will even be recognized if followed immediately by an alphabetic. For
+example, "
+ (racket " a<z (.*?) Caption text. to ([^<]*) Caption text.
F
")
+ '(*TOP* (blockquote (tr (td (blockquote (p)))
+ (div))))))
+
+ (test-section 'p-elem-can-be-child-of-details-elem
+
+ (test 'initial-jacder-example-modified
+ (html->xexp "bar
")
+ '(*TOP* (html (p "foo") (h3 "bar")))))
+
+ (test-section 'twrpme-h2-in-li-elements
+
+ (test 'simple
+ (html->xexp "
")
+ '(*TOP* (html (body (ul (li (h2 "My Header Item"))
+ (li "My Non-Header Item"))))))
+
+ (test 'simon-budinsky-example
+ (html->xexp
+ (string-append
+ ""
+ ""
+ ""
+ "My Header Item
"
+ "
"
+ ""
+ ""))
+ '(*TOP* (html (head)
+ (body (ul (@ (class "post-list"))
+ (li (span (@ (class "post-meta"))
+ "Mar 10, 2022")
+ (h2 (a (@ (class "post-link")
+ (href "/site/update/2022/03/10/twrp-3.6.1-released.html"))
+ "TWRP 3.6.1 Released")))
+
+ (li (span (@ (class "post-meta"))
+ "Mar 10, 2022")
+ (h2 (a (@ (class "post-link")
+ (href "/site/update/2022/03/10/twrp-3.6.1-released.html"))
+ "TWRP 3.6.1 Released")))))))))
+
+ (test-section 'area-in-span-in-microformats
+
+ (test 'area-from-jacob-hall
+ (html->xexp ""
+ "TWRP 3.6.1 Released"
+ "
"
+ ""
+ "TWRP 3.6.1 Released"
+ "
"
+ "")
+ '(*TOP* (img (@ (src "https://example.com/images?src=Blah.jpg&width=150")))))
+
+ (test 'attribute-html-entities-2
+ (html->xexp "https://example.com/phpbb/viewtopic.php?f=31&t=1125")
+ '(*TOP* (a (@ (href "https://example.com/phpbb/viewtopic.php?f=31&t=1125"))
+ "https://example.com/phpbb/viewtopic.php?f=31&t=1125"))))
+
+ ;; TODO: Document this.
+ ;;
+ ;; (define html-1 "
\n \n
"))
+ '#hasheq((links . ((a (@ (target "_blank") (rel "nofollow noreferrer noopener") (class "external text") (href "https://sirdanielfortesque.proboards.com/")) "Forum"))))))
+
+(define (table->links table)
+ (define v (hash-ref table 'links #f))
+ (cond/var
+ [(not v) (values null '("Data table must have a \"Links\" column"))]
+ (var links (filter (λ (a) (and (pair? a) (eq? (car a) 'a))) v)) ; elements
+ [(null? links) (values null '("Links column must have at least one link"))]
+ [#t (values links null)]))
+
+(define (table->logo table)
+ (define logo (hash-ref table 'logo #f))
+ (cond/var
+ [(not logo) (values #f '("Data table must have a \"Logo\" column"))]
+ [(null? logo) (values #f '("Logo table column must have a link"))]
+ (var href (get-attribute 'href (bits->attributes (car (hash-ref table 'logo)))))
+ (var src (get-attribute 'src (bits->attributes (car (hash-ref table 'logo)))))
+ (var true-src (or href src))
+ [(not true-src) (values #f '("Logo table column must have a link"))]
+ [#t (values true-src null)]))
+
+(define (get-api-endpoint wiki)
+ (define main-page (third wiki))
+ (define override (fifth wiki))
+ (or override
+ (match main-page
+ [(regexp #rx"/$") (string-append main-page "api.php")]
+ [(regexp #rx"^(.*)/wiki/" (list _ domain)) (string-append domain "/w/api.php")]
+ [(regexp #rx"^(.*)/w/" (list _ domain)) (string-append domain "/api.php")]
+ [_ (error 'get-api-endpoint "unknown url format: ~a" main-page)])))
+
+(define (get-search-page wiki)
+ (define main-page (third wiki))
+ (define override (fourth wiki))
+ (or override
+ (match main-page
+ [(regexp #rx"/$") (string-append main-page "Special:Search")]
+ [(regexp #rx"^(.*/(?:en|w[^./]*)/)" (list _ wiki-prefix)) (string-append wiki-prefix "Special:Search")]
+ [_ (error 'get-search-page "unknown url format: ~a" main-page)])))
+
+(define/memoize (get-redirect-content wikiname) #:hash hash
+ (define wiki (hash-ref wikis-hash wikiname #f))
+ (cond
+ [wiki
+ (define display-name (cadr wiki))
+ (define endpoint (string-append (get-api-endpoint wiki) "?action=parse&page=MediaWiki:BreezeWikiRedirect&prop=text&formatversion=2&format=json"))
+ (define res (get endpoint))
+ (define html (jp "/parse/text" (response-json res)))
+ (define content ((query-selector (λ (t a c) (has-class? "mw-parser-output" a))
+ (html->xexp html))))
+ (define body (for/list ([p (in-producer (query-selector (λ (t a c) (eq? t 'p)) content) #f)]) p))
+ (define table (parse-table ((query-selector (λ (t a c) (eq? t 'table)) content))))
+ (define-values (links links-errors) (table->links table))
+ (define-values (logo logo-errors) (table->logo table))
+ (define construct-errors (append links-errors logo-errors))
+ (λ (title)
+ (define go
+ (string-append (get-search-page wiki)
+ "?"
+ (params->query `(("search" . ,title)
+ ("go" . "Go")))))
+ `(aside (@ (class "niwa__notice"))
+ (h1 (@ (class "niwa__header")) ,display-name " has its own website separate from Fandom.")
+ (div (@ (class "niwa__cols"))
+ (div (@ (class "niwa__left"))
+ (a (@ (class "niwa__go") (href ,go)) "Read " ,title " on " ,display-name " →")
+ ,@body
+ (p "This external wiki is a helpful alternative to Fandom. You should "
+ (a (@ (href ,go)) "check it out now!")))
+ ,(if logo
+ `(div (@ (class "niwa__right"))
+ (img (@ (class "niwa__logo") (src ,logo))))
+ ""))
+ ,(if (pair? links)
+ `(p (@ (class "niwa__feedback"))
+ ,@(add-between links " / "))
+ "")
+ ,(if (pair? construct-errors)
+ `(ul
+ ,@(for/list ([error construct-errors])
+ `(li ,error)))
+ "")))]
+ [#t #f]))
+(module+ test
+ (check-not-false ((get-redirect-content "gallowmere") "MediEvil Wiki")))
diff --git a/src/fandom-request.rkt b/src/fandom-request.rkt
new file mode 100644
index 00000000..c306b049
--- /dev/null
+++ b/src/fandom-request.rkt
@@ -0,0 +1,74 @@
+#lang typed/racket/base
+(require racket/format
+ racket/string
+ "config.rkt"
+ "../lib/url-utils.rkt")
+(define-type Headers (HashTable Symbol (U Bytes String)))
+(require/typed net/http-easy
+ [#:opaque Timeout-Config timeout-config?]
+ [#:opaque Response response?]
+ [#:opaque Session session?]
+ [response-status-code (Response -> Natural)]
+ [current-session (Parameter Session)]
+ [current-user-agent (Parameter (U Bytes String))]
+ [make-timeout-config ([#:lease Positive-Real] [#:connect Positive-Real] -> Timeout-Config)]
+ [get ((U Bytes String)
+ [#:close? Boolean]
+ [#:headers Headers]
+ [#:timeouts Timeout-Config]
+ [#:max-attempts Exact-Positive-Integer]
+ [#:max-redirects Exact-Nonnegative-Integer]
+ [#:user-agent (U Bytes String)]
+ -> Response)])
+
+(provide
+ fandom-get
+ fandom-get-api
+ timeouts)
+
+(unless (string-contains? (~a (current-user-agent)) "BreezeWiki")
+ (current-user-agent
+ (format "BreezeWiki/1.0 (~a) ~a"
+ (if (config-true? 'canonical_origin)
+ (config-get 'canonical_origin)
+ "local")
+ (current-user-agent))))
+
+(define timeouts (make-timeout-config #:lease 5 #:connect 5))
+
+(: last-failure Flonum)
+(define last-failure 0.0)
+(: stored-failure (Option Response))
+(define stored-failure #f)
+(define failure-persist-time 30000)
+
+(: no-headers Headers)
+(define no-headers '#hasheq())
+
+(: fandom-get (String String [#:headers (Option Headers)] -> Response))
+(define (fandom-get wikiname path #:headers [headers #f])
+ (or
+ (and ((current-inexact-milliseconds) . < . (+ last-failure failure-persist-time)) stored-failure)
+ (let ()
+ (define dest-url (string-append "https://www.fandom.com" path))
+ (define host (string-append wikiname ".fandom.com"))
+ (log-outgoing wikiname path)
+ (define res
+ (get dest-url
+ #:timeouts timeouts
+ #:headers (hash-set (or headers no-headers) 'Host host)))
+ (when (memq (response-status-code res) '(403 406))
+ (set! last-failure (current-inexact-milliseconds))
+ (set! stored-failure res))
+ res)))
+
+(: fandom-get-api (String (Listof (Pair String String)) [#:headers (Option Headers)] -> Response))
+(define (fandom-get-api wikiname params #:headers [headers #f])
+ (fandom-get wikiname
+ (string-append "/api.php?" (params->query params))
+ #:headers headers))
+
+(: log-outgoing (String String -> Void))
+(define (log-outgoing wikiname path)
+ (when (config-true? 'log_outgoing)
+ (printf "out: ~a ~a~n" wikiname path)))
diff --git a/src/log.rkt b/src/log.rkt
new file mode 100644
index 00000000..047c8aa0
--- /dev/null
+++ b/src/log.rkt
@@ -0,0 +1,63 @@
+#lang typed/racket/base
+(require racket/file
+ racket/path
+ racket/port
+ racket/string
+ typed/srfi/19
+ "config.rkt")
+
+(provide
+ log-page-request
+ log-styles-request
+ log-set-settings-request)
+
+(define last-flush 0)
+(define flush-every-millis 60000)
+
+;; anytime-path macro expansion only works in an untyped submodule for reasons I cannot comprehend
+(module define-log-dir racket/base
+ (require racket/path
+ "../lib/syntax.rkt")
+ (provide log-dir)
+ (define log-dir (anytime-path ".." "storage/logs")))
+(require/typed (submod "." define-log-dir)
+ [log-dir Path])
+
+(define log-file (build-path log-dir "access-0.log"))
+(define log-port
+ (if (config-true? 'access_log::enabled)
+ (begin
+ (make-directory* log-dir)
+ (open-output-file log-file #:exists 'append))
+ (open-output-nowhere)))
+
+(: get-date-iso8601 (-> String))
+(define (get-date-iso8601)
+ (date->string (current-date 0) "~5"))
+
+(: offline-string (Boolean -> String))
+(define (offline-string offline?)
+ (if offline? "---" "ooo"))
+
+(: log (String * -> Void))
+(define (log . entry)
+ ;; create log entry string
+ (define full-entry (cons (get-date-iso8601) entry))
+ ;; write to output port
+ (displayln (string-join full-entry ";") log-port)
+ ;; flush output port to file (don't do this too frequently)
+ (when ((- (current-milliseconds) last-flush) . >= . flush-every-millis)
+ (flush-output log-port)
+ (set! last-flush (current-milliseconds))))
+
+(: log-page-request (Boolean String String (U 'light 'dark 'default) -> Void))
+(define (log-page-request offline? wikiname title theme)
+ (log "page" (offline-string offline?) wikiname title (symbol->string theme)))
+
+(: log-styles-request (Boolean String String -> Void))
+(define (log-styles-request offline? wikiname basename)
+ (log "style" (offline-string offline?) wikiname basename))
+
+(: log-set-settings-request (Symbol -> Void))
+(define (log-set-settings-request theme)
+ (log "settings" (symbol->string theme)))
diff --git a/src/page-category.rkt b/src/page-category.rkt
index 83f0f030..e1fe659a 100644
--- a/src/page-category.rkt
+++ b/src/page-category.rkt
@@ -1,72 +1,128 @@
#lang racket/base
(require racket/dict
racket/list
+ racket/match
racket/string
(prefix-in easy: net/http-easy)
; html libs
+ html-parsing
html-writing
; web server libs
net/url
web-server/http
(only-in web-server/dispatchers/dispatch next-dispatcher)
#;(only-in web-server/http/redirect redirect-to)
- "config.rkt"
"application-globals.rkt"
- "url-utils.rkt"
- "xexpr-utils.rkt")
+ "config.rkt"
+ "data.rkt"
+ "fandom-request.rkt"
+ "page-wiki.rkt"
+ "../lib/syntax.rkt"
+ "../lib/thread-utils.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
(provide
page-category)
(module+ test
- (require rackunit)
+ (require rackunit
+ "test-utils.rkt")
(define category-json-data
'#hasheq((batchcomplete . #t) (continue . #hasheq((cmcontinue . "page|4150504c45|41473") (continue . "-||"))) (query . #hasheq((categorymembers . (#hasheq((ns . 0) (pageid . 25049) (title . "Item (entity)")) #hasheq((ns . 0) (pageid . 128911) (title . "3D")) #hasheq((ns . 0) (pageid . 124018) (title . "A Very Fine Item")) #hasheq((ns . 0) (pageid . 142208) (title . "Amethyst Shard")) #hasheq((ns . 0) (pageid . 121612) (title . "Ankle Monitor")))))))))
-(define (generate-results-page dest-url wikiname prefixed-category data)
- (define members (jp "/query/categorymembers" data))
+(define (generate-results-page
+ #:req req
+ #:source-url source-url
+ #:wikiname wikiname
+ #:title title
+ #:members-data members-data
+ #:page page
+ #:head-data [head-data #f]
+ #:siteinfo [siteinfo #f])
+ (define members (jp "/query/categorymembers" members-data))
(generate-wiki-page
- dest-url
- wikiname
- prefixed-category
- `(div (@ (class "mw-parser-output"))
- (ul (@ (class "my-category-list"))
- ,@(map
- (λ (result)
- (define title (jp "/title" result))
- (define page-path (regexp-replace* #rx" " title "_"))
- `(li
- (a (@ (href ,(format "/~a/wiki/~a" wikiname page-path)))
- ,title)))
- members)))))
+ #:req req
+ #:source-url source-url
+ #:wikiname wikiname
+ #:title title
+ #:head-data head-data
+ #:siteinfo siteinfo
+ `(div
+ ,(update-tree-wiki page wikiname)
+ (hr)
+ (h2 ,(format "All Pages in ~a" title))
+ (div (@ (class "mw-parser-output"))
+ (ul (@ (class "my-category-list"))
+ ,@(map
+ (λ (result)
+ (define title (jp "/title" result))
+ (define page-path (page-title->path title))
+ `(li
+ (a (@ (href ,(format "/~a/wiki/~a" wikiname page-path)))
+ ,title)))
+ members))))))
(define (page-category req)
(response-handler
(define wikiname (path/param-path (first (url-path (request-uri req)))))
- (define prefixed-category (path/param-path (caddr (url-path (request-uri req)))))
-
+ (define prefixed-category (string-join (map path/param-path (cddr (url-path (request-uri req)))) "/"))
(define origin (format "https://~a.fandom.com" wikiname))
- (define dest-url (format "~a/api.php?~a"
- origin
- (params->query `(("action" . "query")
- ("list" . "categorymembers")
- ("cmtitle" . ,prefixed-category)
- ("cmlimit" . "max")
- ("formatversion" . "2")
- ("format" . "json")))))
- (printf "out: ~a~n" dest-url)
- (define dest-res (easy:get dest-url #:timeouts timeouts))
+ (define source-url (format "~a/wiki/~a" origin prefixed-category))
- (define data (easy:response-json dest-res))
- (define body (generate-results-page dest-url wikiname prefixed-category data))
- (when (config-get 'debug)
+ (define-values (members-data page-data siteinfo)
+ (thread-values
+ (λ ()
+ (easy:response-json
+ (fandom-get-api
+ wikiname
+ `(("action" . "query")
+ ("list" . "categorymembers")
+ ("cmtitle" . ,prefixed-category)
+ ("cmlimit" . "max")
+ ("formatversion" . "2")
+ ("format" . "json")))))
+ (λ ()
+ (easy:response-json
+ (fandom-get-api
+ wikiname
+ `(("action" . "parse")
+ ("page" . ,prefixed-category)
+ ("prop" . "text|headhtml|langlinks")
+ ("formatversion" . "2")
+ ("format" . "json")))))
+ (λ ()
+ (siteinfo-fetch wikiname))))
+
+ (define title (preprocess-html-wiki (jp "/parse/title" page-data prefixed-category)))
+ (define page-html (preprocess-html-wiki (jp "/parse/text" page-data "")))
+ (define page (html->xexp page-html))
+ (define head-data ((head-data-getter wikiname) page-data))
+ (define body (generate-results-page
+ #:req req
+ #:source-url source-url
+ #:wikiname wikiname
+ #:title title
+ #:members-data members-data
+ #:page page
+ #:head-data head-data
+ #:siteinfo siteinfo))
+
+ (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))
(response/output
#:code 200
+ #:headers (build-headers always-headers)
(λ (out)
(write-html body out)))))
(module+ test
(check-not-false ((query-selector (attribute-selector 'href "/test/wiki/Ankle_Monitor")
- (generate-results-page "" "test" "Category:Items" category-json-data)))))
+ (generate-results-page
+ #:req test-req
+ #:source-url ""
+ #:wikiname "test"
+ #:title "Category:Items"
+ #:members-data category-json-data
+ #:page '(div "page text"))))))
diff --git a/src/page-file.rkt b/src/page-file.rkt
new file mode 100644
index 00000000..5151f1d1
--- /dev/null
+++ b/src/page-file.rkt
@@ -0,0 +1,174 @@
+#lang racket/base
+(require racket/dict
+ racket/list
+ racket/match
+ racket/string
+ (prefix-in easy: net/http-easy)
+ ; html libs
+ html-parsing
+ html-writing
+ ; web server libs
+ net/url
+ web-server/http
+ (only-in web-server/dispatchers/dispatch next-dispatcher)
+ #;(only-in web-server/http/redirect redirect-to)
+ "application-globals.rkt"
+ "config.rkt"
+ "data.rkt"
+ "fandom-request.rkt"
+ "page-wiki.rkt"
+ "../lib/syntax.rkt"
+ "../lib/thread-utils.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
+
+(provide page-file)
+
+(module+ test
+ (require rackunit
+ "test-utils.rkt")
+ (define test-media-detail
+ '#hasheq((fileTitle . "Example file")
+ (videoEmbedCode . "")
+ (imageUrl . "https://static.wikia.nocookie.net/examplefile")
+ (rawImageUrl . "https://static.wikia.nocookie.net/examplefile")
+ (userName . "blankie")
+ (isPostedIn . #t)
+ (smallerArticleList . (#hasheq((titleText . "Test:Example article"))))
+ (articleListIsSmaller . 0)
+ (exists . #t)
+ (imageDescription . #f))))
+
+(define (url-content-type url)
+ (define dest-res (easy:head url))
+ (easy:response-headers-ref dest-res 'content-type))
+
+(define (get-media-html url content-type)
+ (define maybe-proxied-url (if (config-true? 'strict_proxy) (u-proxy-url url) url))
+ (cond
+ [(eq? content-type #f) `""]
+ [(regexp-match? #rx"(?i:^image/)" content-type) `(img (@ (src ,maybe-proxied-url)))]
+ [(regexp-match? #rx"(?i:^audio/|^application/ogg(;|$))" content-type)
+ `(audio (@ (src ,maybe-proxied-url) (controls)))]
+ [(regexp-match? #rx"(?i:^video/)" content-type) `(video (@ (src ,maybe-proxied-url) (controls)))]
+ [else `""]))
+
+(define (generate-results-page #:req req
+ #:source-url source-url
+ #:wikiname wikiname
+ #:title title
+ #:media-detail media-detail
+ #:image-content-type image-content-type
+ #:siteinfo [siteinfo #f])
+ (define video-embed-code (jp "/videoEmbedCode" media-detail ""))
+ (define raw-image-url (jp "/rawImageUrl" media-detail))
+ (define image-url (jp "/imageUrl" media-detail raw-image-url))
+ (define username (jp "/userName" media-detail))
+ (define is-posted-in (jp "/isPostedIn" media-detail #f))
+ (define smaller-article-list (jp "/smallerArticleList" media-detail))
+ (define article-list-is-smaller (jp "/articleListIsSmaller" media-detail))
+ (define image-description (jp "/imageDescription" media-detail #f))
+ (define maybe-proxied-raw-image-url
+ (if (config-true? 'strict_proxy) (u-proxy-url raw-image-url) raw-image-url))
+ (generate-wiki-page
+ #:req req
+ #:source-url source-url
+ #:wikiname wikiname
+ #:title title
+ #:siteinfo siteinfo
+ `(div ,(if (non-empty-string? video-embed-code)
+ (update-tree-wiki (html->xexp (preprocess-html-wiki video-embed-code)) wikiname)
+ (get-media-html image-url image-content-type))
+ (p ,(if (non-empty-string? video-embed-code)
+ `""
+ `(span (a (@ (href ,maybe-proxied-raw-image-url)) "View original file") ". "))
+ "Uploaded by "
+ (a (@ (href ,(format "/~a/wiki/User:~a" wikiname username))) ,username)
+ ".")
+ ,(if (string? image-description)
+ (update-tree-wiki (html->xexp (preprocess-html-wiki image-description)) wikiname)
+ ; file license info might be displayed in the description, example: /lgbtqia/wiki/File:Rainbow_Flag1.svg
+ `(p "This file is likely protected by copyright. Consider the file's license and fair use law before reusing it."))
+ ,(if is-posted-in
+ `(p "This file is used in "
+ ,@(map (λ (article)
+ (define title (jp "/titleText" article))
+ (define page-path (regexp-replace* #rx" " title "_"))
+ `(span ,(if (eq? (car smaller-article-list) article) "" ", ")
+ (a (@ (href ,(format "/~a/wiki/~a" wikiname page-path)))
+ ,title)))
+ smaller-article-list)
+ ,(if (eq? article-list-is-smaller 1) "…" "."))
+ `""))))
+
+(define (page-file req)
+ (response-handler
+ (define wikiname (path/param-path (first (url-path (request-uri req)))))
+ (define prefixed-title (path/param-path (caddr (url-path (request-uri req)))))
+ (define source-url (format "https://~a.fandom.com/wiki/~a" wikiname prefixed-title))
+
+ (define-values (media-detail siteinfo)
+ (thread-values
+ (λ ()
+ (define dest-res
+ (fandom-get
+ wikiname
+ (format "/wikia.php?~a"
+ (params->query `(("format" . "json") ("controller" . "Lightbox")
+ ("method" . "getMediaDetail")
+ ("fileTitle" . ,prefixed-title))))))
+ (easy:response-json dest-res))
+ (λ ()
+ (siteinfo-fetch wikiname))))
+ (if (not (jp "/exists" media-detail #f))
+ (next-dispatcher)
+ (response-handler
+ (define file-title (jp "/fileTitle" media-detail ""))
+ (define title
+ (if (non-empty-string? file-title) (format "File:~a" file-title) prefixed-title))
+ (define image-content-type
+ (if (non-empty-string? (jp "/videoEmbedCode" media-detail ""))
+ #f
+ (url-content-type (jp "/imageUrl" media-detail))))
+ (define body
+ (generate-results-page #:req req
+ #:source-url source-url
+ #:wikiname wikiname
+ #:title title
+ #:media-detail media-detail
+ #:image-content-type image-content-type
+ #:siteinfo siteinfo))
+ (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))
+ (response/output #:code 200
+ #:headers (build-headers always-headers)
+ (λ (out) (write-html body out)))))))
+(module+ test
+ (parameterize ([(config-parameter 'strict_proxy) "true"])
+ (check-equal? (get-media-html "https://static.wikia.nocookie.net/a" "image/jpeg")
+ `(img (@ (src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fa"))))
+ (check-equal? (get-media-html "https://static.wikia.nocookie.net/b" "audio/mp3")
+ `(audio (@ (src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fb")
+ (controls)))))
+ (parameterize ([(config-parameter 'strict_proxy) "false"])
+ (check-equal? (get-media-html "https://static.wikia.nocookie.net/c" "application/ogg")
+ `(audio (@ (src "https://static.wikia.nocookie.net/c")
+ (controls))))
+ (check-equal? (get-media-html "https://static.wikia.nocookie.net/d" "video/mp4")
+ `(video (@ (src "https://static.wikia.nocookie.net/d")
+ (controls)))))
+ (check-equal? (get-media-html "https://example.com" "who knows") `"")
+ (check-equal? (get-media-html "https://example.com" #f) `""))
+(module+ test
+ (parameterize ([(config-parameter 'strict_proxy) "true"])
+ (check-not-false
+ ((query-selector
+ (attribute-selector 'src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fexamplefile")
+ (generate-results-page #:req test-req
+ #:source-url ""
+ #:wikiname "test"
+ #:title "File:Example file"
+ #:media-detail test-media-detail
+ #:image-content-type "image/jpeg"))))))
diff --git a/src/page-global-search.rkt b/src/page-global-search.rkt
new file mode 100644
index 00000000..08bfd13b
--- /dev/null
+++ b/src/page-global-search.rkt
@@ -0,0 +1,32 @@
+#lang racket/base
+(require racket/dict
+ ; web server libs
+ net/url
+ web-server/http
+ "application-globals.rkt"
+ "data.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
+
+(provide
+ page-global-search)
+
+(define (page-global-search req)
+ (define wikiname (dict-ref (url-query (request-uri req)) 'wikiname #f))
+ (define q (dict-ref (url-query (request-uri req)) 'q #f))
+ (response-handler
+ (cond
+ [(not wikiname)
+ (response/output
+ #:code 400
+ #:mime-type #"text/plain"
+ (λ (out)
+ (displayln "Requires wikiname and q parameters." out)))]
+ [(or (not q) (equal? q ""))
+ (define siteinfo (siteinfo-fetch wikiname))
+ (define dest (format "/~a/wiki/~a" wikiname (or (siteinfo^-basepage siteinfo) "Main_Page")))
+ (generate-redirect dest)]
+ [#t
+ (generate-redirect (format "/~a/search?~a"
+ wikiname
+ (params->query `(("q" . ,q)))))])))
diff --git a/src/page-home.rkt b/src/page-home.rkt
index 4a87a1b1..24f7393b 100644
--- a/src/page-home.rkt
+++ b/src/page-home.rkt
@@ -1,8 +1,13 @@
#lang racket/base
-(require html-writing
+(require net/url
+ html-writing
web-server/http
- "xexpr-utils.rkt"
+ "application-globals.rkt"
+ "data.rkt"
+ "static-data.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt"
"config.rkt")
(provide
@@ -12,70 +17,82 @@
(require rackunit))
(define examples
- '(("crosscode" "CrossCode_Wiki")
- ("minecraft" "Bricks")
- ("undertale" "Hot_Dog...%3F")
- ("tardis" "Eleanor_Blake")
- ("fireemblem" "God-Shattering_Star")
- ("fallout" "Pip-Boy_3000")))
+ '(("minecraft" "Bricks")
+ ("crosscode" "CrossCode Wiki")
+ ("undertale" "Hot Dog...?")
+ ("tardis" "Eleanor Blake")
+ ("zelda" "Boomerang")))
(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\".")
+ (p "It removes ads, videos, and suggested content, leaving you with a clean page that doesn't slow down your device or use up your data.")
+ (p ,(format "To use BreezeWiki, just replace \"fandom.com\" with \"~a\", and you'll instantly be teleported to a better world."
+ (if (config-true? 'canonical_origin)
+ (url-host (string->url (config-get 'canonical_origin)))
+ "breezewiki.com")))
+ (p "If you'd like to be automatically sent to BreezeWiki every time in the future, "
+ ,@(if (config-member? 'promotions::indie_wiki_buddy "home")
+ `((a (@ (href "https://getindie.wiki")) "get our affiliated browser extension (NEW!)")
+ " or ")
+ null)
+ (a (@ (href "https://docs.breezewiki.com/Automatic_Redirection.html")) "check out the tutorial in the manual."))
+ (p "BreezeWiki is available on several different websites called " (a (@ (href "https://en.wikipedia.org/wiki/Mirror_site")) "mirrors") ". Each is independently run. If one mirror is offline, the others still work. "
+ (a (@ (href "https://docs.breezewiki.com/Links.html#%28part._.Mirrors%29")) "See the list."))
+ (h2 "Find a page")
+ (form (@ (action "/search"))
+ (label (@ (class "paired__label"))
+ "Wiki name"
+ (input (@ (name "wikiname") (class "paired__input") (type "text") (placeholder "pokemon") (required))))
+ (label (@ (class "paired__label"))
+ "Search query"
+ (input (@ (name "q") (class "paired__input") (type "text") (placeholder "Eevee"))))
+ (button "Search"))
(h2 "Example pages")
(ul
,@(map (λ (x)
- `(li (a (@ (href ,(apply format "/~a/wiki/~a" x)))
+ `(li (a (@ (href ,(format "/~a/wiki/~a" (car x) (page-title->path (cadr 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.")))
+ (h2 "Testimonials")
+ (p (@ (class "testimonial")) ">so glad someone introduced me to a F*ndom alternative (BreezeWiki) because that x-factorized spillway of an ad-infested radioactive dumpsite can go die in a fire —RB")
+ (p (@ (class "testimonial")) ">apparently there are thousands of people essentially running our company " (em "for free") " right now, creating tons of content, and we just put ads on top of it and they're not even employees. thousands of people we can't lay off. thousands! —" (a (@ (href "https://hard-drive.net/fandom-ceo-frustrated-its-impossible-to-lay-off-unpaid-users-who-update-wikias-for-fun/?utm_source=breezewiki") (target "_blank")) "Perkins Miller, Fandom CEO"))
+ (p (@ (class "testimonial")) ">attempting to go to a wiki's forum page with breezewiki doesn't work, which is based honestly —Tom Skeleton")
+ (p (@ (class "testimonial")) ">Fandom pages crashing and closing, taking forever to load and locking up as they load the ads on the site... they are causing the site to crash because they are trying to load video ads both at the top and bottom of the site as well as two or three banner ads, then a massive top of site ad and eventually my anti-virus shuts the whole site down because it's literally pulling more resources than WoW in ultra settings... —Anonymous")
+ (p (@ (class "testimonial")) ">reblogs EXTREMELY appreciated I want that twink* (*fandom wiki) obliterated —footlong")
+
+ (h2 "What BreezeWiki isn't")
+ (p "BreezeWiki isn't an \"alternative\" to Fandom, and it doesn't let you edit or write new pages.")
+ (p "If you want to create your own wiki, try Miraheze!")))
(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)
- `(div
- (p ,(format "This instance is run by the ~a developer, " (config-get 'application-name))
- (a (@ (href "https://cadence.moe/contact"))
- "Cadence."))
- (p "Hosting generously provided by "
- (a (@ (href "http://alphamethyl.barr0w.net/"))
- "alphamethyl.")))
- `(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)))))))))))
+ `(*TOP*
+ (*DECL* DOCTYPE html)
+ (html
+ (head
+ (meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))
+ (title "About | BreezeWiki")
+ (link (@ (rel "stylesheet") (type "text/css") (href ,(get-static-url "internal.css"))))
+ (link (@ (rel "stylesheet") (type "text/css") (href ,(get-static-url "main.css"))))
+ (link (@ (rel "icon") (href ,(head-data^-icon-url head-data-default)))))
+ (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))
+ ,(application-footer #f))))))))
(module+ test
(check-not-false (xexp->html body)))
(define (page-home req)
(response/output
#:code 200
+ #:headers (build-headers always-headers)
(λ (out)
(write-html body out))))
diff --git a/src/page-it-works.rkt b/src/page-it-works.rkt
new file mode 100644
index 00000000..ce9e05f3
--- /dev/null
+++ b/src/page-it-works.rkt
@@ -0,0 +1,15 @@
+#lang racket/base
+(require racket/dict
+ net/url
+ web-server/http
+ web-server/dispatchers/dispatch
+ "application-globals.rkt")
+
+(provide
+ page-it-works)
+
+(define (page-it-works req)
+ (define b? (dict-ref (url-query (request-uri req)) 'b #f))
+ (if b?
+ (generate-redirect "/stampylongnose/wiki/It_Works")
+ (next-dispatcher)))
diff --git a/src/page-proxy.rkt b/src/page-proxy.rkt
index f494e283..cd94603a 100644
--- a/src/page-proxy.rkt
+++ b/src/page-proxy.rkt
@@ -8,8 +8,9 @@
net/url
web-server/http
(only-in web-server/dispatchers/dispatch next-dispatcher)
- "url-utils.rkt"
- "xexpr-utils.rkt")
+ "application-globals.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
(provide
page-proxy)
@@ -18,13 +19,17 @@
(match (dict-ref (url-query (request-uri req)) 'dest #f)
[(? string? dest)
(if (is-fandom-url? dest)
- (response-handler
+ (response-handler ; catches and reports errors
(let ([dest-r (easy:get dest #:stream? #t)])
- (response/output
- #:code (easy:response-status-code dest-r)
- #:mime-type (easy:response-headers-ref dest-r 'content-type)
- (λ (out)
- (copy-port (easy:response-output dest-r) out)
- (easy:response-close! dest-r)))))
+ (with-handlers ([exn:fail? (λ (e) ; cleans up and re-throws
+ (easy:response-close! dest-r)
+ (raise e))])
+ (response/output
+ #:code (easy:response-status-code dest-r)
+ #:mime-type (easy:response-headers-ref dest-r 'content-type)
+ #:headers (build-headers always-headers)
+ (λ (out)
+ (copy-port (easy:response-output dest-r) out)
+ (easy:response-close! dest-r))))))
(next-dispatcher))]
[#f (next-dispatcher)]))
diff --git a/src/page-redirect-wiki-home.rkt b/src/page-redirect-wiki-home.rkt
new file mode 100644
index 00000000..255f625e
--- /dev/null
+++ b/src/page-redirect-wiki-home.rkt
@@ -0,0 +1,17 @@
+#lang racket/base
+(require net/url
+ web-server/http
+ "application-globals.rkt"
+ "data.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
+
+(provide
+ redirect-wiki-home)
+
+(define (redirect-wiki-home req)
+ (response-handler
+ (define wikiname (path/param-path (car (url-path (request-uri req)))))
+ (define siteinfo (siteinfo-fetch wikiname))
+ (define dest (format "/~a/wiki/~a" wikiname (or (siteinfo^-basepage siteinfo) "Main_Page")))
+ (generate-redirect dest)))
diff --git a/src/page-search.rkt b/src/page-search.rkt
index 7b0abe39..39f361a7 100644
--- a/src/page-search.rkt
+++ b/src/page-search.rkt
@@ -2,7 +2,6 @@
(require racket/dict
racket/list
racket/string
- (prefix-in easy: net/http-easy)
; html libs
html-writing
; web server libs
@@ -10,73 +9,78 @@
web-server/http
(only-in web-server/dispatchers/dispatch next-dispatcher)
#;(only-in web-server/http/redirect redirect-to)
- "config.rkt"
"application-globals.rkt"
- "url-utils.rkt"
- "xexpr-utils.rkt")
+ "config.rkt"
+ "data.rkt"
+ "search-provider-fandom.rkt"
+ "search-provider-solr.rkt"
+ "../lib/syntax.rkt"
+ "../lib/thread-utils.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
(provide
page-search)
-(module+ test
- (require rackunit)
- (define search-json-data
- '#hasheq((batchcomplete . #t) (query . #hasheq((search . (#hasheq((ns . 0) (pageid . 219) (size . 1482) (snippet . "") (timestamp . "2022-08-21T08:54:23Z") (title . "Gacha Capsule") (wordcount . 214)) #hasheq((ns . 0) (pageid . 201) (size . 1198) (snippet . "") (timestamp . "2022-07-11T17:52:47Z") (title . "Badges") (wordcount . 181)))))))))
+(define search-providers
+ (hash "fandom" search-fandom
+ "solr" search-solr))
-(define (generate-results-page dest-url wikiname query data)
- (define search-results (jp "/query/search" data))
+;; this takes the info we gathered from fandom and makes the big fat x-expression page
+(define (generate-results-page req source-url wikiname query results-content #:siteinfo [siteinfo #f])
+ ;; this is *another* helper that builds the wiki page UI and lets me put the search results (or whatever else) in the middle
(generate-wiki-page
- dest-url
- wikiname
- "Search Results"
- `(div (@ (class "mw-parser-output"))
- (p ,(format "~a results found for " (length search-results))
- (strong ,query))
- (ul ,@(map
- (λ (result)
- (let* ([title (jp "/title" result)]
- [page-path (regexp-replace* #rx" " title "_")]
- [timestamp (jp "/timestamp" result)]
- [wordcount (jp "/wordcount" result)]
- [size (jp "/size" result)])
- `(li (@ (class "my-result"))
- (a (@ (class "my-result__link") (href ,(format "/~a/wiki/~a" wikiname page-path)))
- ,title)
- (div (@ (class "my-result__info"))
- "last edited "
- (time (@ (datetime ,timestamp)) ,(list-ref (string-split timestamp "T") 0))
- ,(format ", ~a words, ~a kb"
- wordcount
- (exact->inexact (/ (round (/ size 100)) 10)))))))
- search-results)))))
+ ;; so I provide my helper function with the necessary context...
+ #:req req
+ #:source-url source-url
+ #:wikiname wikiname
+ #:title query
+ #:siteinfo siteinfo
+ ;; and here's the actual results to display in the wiki page layout
+ results-content))
+;; will be called when the web browser asks to load the page
(define (page-search req)
+ ;; this just means, catch any errors and display them in the browser. it's a function somewhere else
(response-handler
+ ;; the URL will look like "/minecraft/wiki/Special:Search?q=Spawner"
+ ;; grab the first part to use as the wikiname, in this case, "minecraft"
(define wikiname (path/param-path (first (url-path (request-uri req)))))
- (define query (dict-ref (url-query (request-uri req)) 'q #f))
+ ;; grab a dict of url search params
+ (define params (url-query (request-uri req)))
+ ;; grab the part after ?q= which is the search terms
+ (define query (dict-ref params 'q #f))
+ ;; figure out which search provider we're going to use
+ (define search-provider (hash-ref search-providers (config-get 'feature_offline::search)
+ (λ () (error 'search-provider "unknown search provider configured"))))
- (define origin (format "https://~a.fandom.com" wikiname))
- (define dest-url (format "~a/api.php?~a"
- origin
- (params->query `(("action" . "query")
- ("list" . "search")
- ("srsearch" . ,query)
- ("formatversion" . "2")
- ("format" . "json")))))
- (printf "out: ~a~n" dest-url)
- (define dest-res (easy:get dest-url #:timeouts timeouts))
+ ;; external special:search url to link at the bottom of the page as the upstream source
+ (define external-search-url
+ (format "https://~a.fandom.com/wiki/Special:Search?~a"
+ wikiname
+ (params->query `(("query" . ,query)
+ ("search" . "internal")))))
- (define data (easy:response-json dest-res))
+ ;; simultaneously get the search results, as well as information about the wiki as a whole (its license, icon, name)
+ (define-values (results-content siteinfo)
+ (thread-values
+ (λ ()
+ (search-provider wikiname query params)) ;; call the search provider (see file "search-provider-fandom.rkt")
+ (λ ()
+ (siteinfo-fetch wikiname)))) ;; helper function in another file to get information about the wiki
- (define body (generate-results-page dest-url wikiname query data))
+ ;; calling my generate-results-page function with the information so far in order to get a big fat x-expression
+ ;; big fat x-expression goes into the body variable
+ (define body (generate-results-page req external-search-url wikiname query results-content #:siteinfo siteinfo))
+ ;; error checking
(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))
+ ;; convert body to HTML and send to browser
(response/output
#:code 200
+ #:headers (build-headers always-headers)
(λ (out)
(write-html body out)))))
-(module+ test
- (check-not-false ((query-selector (attribute-selector 'href "/test/wiki/Gacha_Capsule")
- (generate-results-page "" "test" "Gacha" search-json-data)))))
+
diff --git a/src/page-set-user-settings.rkt b/src/page-set-user-settings.rkt
new file mode 100644
index 00000000..b9491423
--- /dev/null
+++ b/src/page-set-user-settings.rkt
@@ -0,0 +1,20 @@
+#lang racket/base
+(require racket/dict
+ net/url
+ web-server/http
+ "application-globals.rkt"
+ "data.rkt"
+ "log.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
+
+(provide
+ page-set-user-settings)
+
+(define (page-set-user-settings req)
+ (response-handler
+ (define next-location (dict-ref (url-query (request-uri req)) 'next_location))
+ (define new-settings (read (open-input-string (dict-ref (url-query (request-uri req)) 'new_settings))))
+ (log-set-settings-request (user-cookies^-theme new-settings))
+ (define headers (user-cookies-setter new-settings))
+ (generate-redirect next-location #:headers headers)))
diff --git a/src/page-static-archive.rkt b/src/page-static-archive.rkt
new file mode 100644
index 00000000..501bda79
--- /dev/null
+++ b/src/page-static-archive.rkt
@@ -0,0 +1,94 @@
+#lang racket/base
+(require racket/file
+ racket/path
+ racket/port
+ racket/string
+ net/url
+ web-server/http
+ web-server/servlet-dispatch
+ web-server/dispatchers/filesystem-map
+ (only-in web-server/dispatchers/dispatch next-dispatcher)
+ "../archiver/archiver.rkt"
+ "../lib/mime-types.rkt"
+ "../lib/syntax.rkt"
+ "../lib/xexpr-utils.rkt"
+ "config.rkt"
+ "log.rkt")
+
+(provide
+ page-static-archive)
+
+(define path-archive (anytime-path ".." "storage/archive"))
+
+(define ((replacer wikiname) whole url)
+ (format
+ "url(~a)"
+ (if (or (equal? url "")
+ (equal? url "'")
+ (string-contains? url "/resources-ucp/")
+ (string-contains? url "/fonts/")
+ (string-contains? url "/drm_fonts/")
+ (string-contains? url "//db.onlinewebfonts.com/")
+ (string-contains? url "//bits.wikimedia.org/")
+ (string-contains? url "mygamercard.net/")
+ (string-contains? url "dropbox")
+ (string-contains? url "only=styles")
+ (string-contains? url "https://https://")
+ (regexp-match? #rx"^%20|^'" url)
+ (regexp-match? #rx"^\"?data:" url)
+ (regexp-match? #rx"^file:" url))
+ url
+ (let* ([norm-url
+ (cond
+ [(string-prefix? url "https://") url]
+ [(string-prefix? url "http://") (regexp-replace #rx"http:" url "https:")]
+ [(string-prefix? url "httpshttps://") (regexp-replace #rx"httpshttps://" url "https://")]
+ [(string-prefix? url "//") (string-append "https:" url)]
+ [(string-prefix? url "/") (format "https://~a.fandom.com~a" wikiname url)]
+ [else (error 'replace-style-for-images "unknown URL format: ~a" url)])])
+ (define p (image-url->values norm-url))
+ ;; (printf "hashed: ~a~n -> ~a~n #-> ~a~n" url (car p) (cdr p))
+ (format "/archive/~a/images/~a" wikiname (cdr p))))))
+
+(define (replace-style-for-images wikiname path)
+ (define content (file->string path))
+ (regexp-replace* #rx"url\\(\"?'?([^)]*)'?\"?\\)" content (replacer wikiname)))
+
+(define (handle-style wikiname dest)
+ (when (config-true? 'debug)
+ (printf "using offline mode for style ~a ~a~n" wikiname dest))
+ (log-styles-request #t wikiname dest)
+ (define fs-path (build-path path-archive wikiname "styles" dest))
+ (unless (file-exists? fs-path)
+ (next-dispatcher))
+ (response-handler
+ (define new-content (replace-style-for-images wikiname fs-path))
+ (response/output
+ #:code 200
+ #:headers (list (header #"Content-Type" #"text/css")
+ (header #"Referrer-Policy" #"same-origin"))
+ (λ (out) (displayln new-content out)))))
+
+(define (handle-image wikiname dest) ;; dest is the hash with no extension
+ (unless ((string-length dest) . >= . 40) (next-dispatcher))
+ (response-handler
+ (define dir (build-path path-archive wikiname "images" (substring dest 0 1) (substring dest 0 2)))
+ (unless (directory-exists? dir) (next-dispatcher))
+ (define candidates (directory-list dir))
+ (define target (path->string (findf (λ (f) (string-prefix? (path->string f) dest)) candidates)))
+ (unless target (next-dispatcher))
+ (define ext (substring target 41))
+ (response/output
+ #:code 200
+ #:headers (list (header #"Content-Type" (ext->mime-type (string->bytes/latin-1 ext))))
+ (λ (out)
+ (call-with-input-file (build-path dir target)
+ (λ (in)
+ (copy-port in out)))))))
+
+(define (page-static-archive req)
+ (define path (url-path (request-uri req)))
+ (define-values (_ wikiname kind dest) (apply values (map path/param-path path)))
+ (cond [(equal? kind "styles") (handle-style wikiname dest)]
+ [(equal? kind "images") (handle-image wikiname dest)]
+ [else (response-handler (raise-user-error "page-static-archive: how did we get here?" kind))]))
diff --git a/src/page-static.rkt b/src/page-static.rkt
index e684c745..03112299 100644
--- a/src/page-static.rkt
+++ b/src/page-static.rkt
@@ -7,6 +7,8 @@
web-server/dispatchers/filesystem-map
(only-in web-server/dispatchers/dispatch next-dispatcher)
(prefix-in files: web-server/dispatchers/dispatch-files)
+ "../lib/mime-types.rkt"
+ "../lib/syntax.rkt"
"config.rkt")
(provide
@@ -16,52 +18,61 @@
(require rackunit))
(define-runtime-path path-static "../static")
+(define path-archive (anytime-path ".." "storage/archive"))
(define hash-ext-mime-type
(hash #".css" #"text/css"
+ #".js" #"text/javascript"
#".png" #"image/png"
#".svg" #"image/svg+xml"
+ #".woff2" #"font/woff2"
#".txt" #"text/plain"))
-(define (ext->mime-type ext)
- (hash-ref hash-ext-mime-type ext))
-(module+ test
- (check-equal? (ext->mime-type #".png") #"image/png"))
-
(define (make-path segments)
(map (λ (seg) (path/param seg '())) segments))
(module+ test
(check-equal? (make-path '("static" "main.css"))
(list (path/param "static" '()) (path/param "main.css" '()))))
+;; given a request path, return a rewritten request path and the source directory on the filesystem to serve based on
(define (path-rewriter p)
(cond
; url is ^/static/... ?
[(equal? (path/param-path (car p)) "static")
; rewrite to ^/... which will be treated as relative to static/ on the filesystem
- (cdr p)]
+ (values (cdr p) path-static)]
+ ; url is ^/archive/... ?
+ [(equal? (path/param-path (car p)) "archive")
+ ; rewrite req to ^/ Links Forum to make the parser happy
- ; usage: /fallout/wiki/Fallout:_New_Vegas_achievements_and_trophies
- (curry rr* #rx"(
]*>\n?)( \\2")
- ; change
\n \n
+ ${suggestions.value.map(hit => html`<${Suggestion} ...${hit} />`)}
+
`
+}
+
+render(html`<${SuggestionList} />`, eSuggestions)
+
+// input view
+
+effect(() => {
+ query.value // dependency should always be tracked
+ if (st.peek() === "accepted") return // lock results from changing during navigation
+ st.value = "loading"
+ fetchSuggestions(query.value).then(res => {
+ suggestions.value = res
+ if (hitsDone.size === hitsPromise.size) {
+ st.value = "ready"
+ }
+ })
+})
+
+window.addEventListener("pageshow", () => {
+ st.value = "ready" // unlock results from changing after returning to page
+})
+
+function SuggestionInput() {
+ return html`
+ query.value = e.target.value} value=${query.value} class=${classNames(["bw-ss__input", `bw-ss__input--${st.value}`])} />`
+}
+
+render(html`<${SuggestionInput} />`, eInput)
+
+// form focus
+
+eForm.addEventListener("focusin", () => focus.value = true)
+eForm.addEventListener("focusout", event => {
+ if (eForm.contains(event.relatedTarget)) {
+ // event fired when changing from one form element to the other
+ focus.value = true
+ } else {
+ // event fired when moving out of the form element
+ focus.value = false
+ }
+})
diff --git a/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2 b/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2
new file mode 100644
index 00000000..199eac2b
Binary files /dev/null and b/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2 differ
diff --git a/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2 b/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2
new file mode 100644
index 00000000..ccc64f7c
Binary files /dev/null and b/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2 differ
diff --git a/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2 b/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2
new file mode 100644
index 00000000..dd4ac826
Binary files /dev/null and b/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2 differ
diff --git a/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2 b/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2
new file mode 100644
index 00000000..bc9c203b
Binary files /dev/null and b/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2 differ
diff --git a/static/tabs.js b/static/tabs.js
new file mode 100644
index 00000000..718b48ec
--- /dev/null
+++ b/static/tabs.js
@@ -0,0 +1,40 @@
+"use strict";
+
+const tabFromHash = location.hash.length > 1 ? location.hash.substring(1) : null
+
+for (const tabber of document.body.querySelectorAll(".wds-tabber")) {
+ for (const [tab, content] of getTabberTabs(tabber)) {
+ // set up click listener on every tab
+ tab.addEventListener("click", e => {
+ setCurrentTab(tabber, tab, content)
+ e.preventDefault()
+ })
+
+ // re-open a specific tab on page load based on the URL hash
+ if (tab.dataset.hash === tabFromHash) {
+ setCurrentTab(tabber, tab, content)
+ tab.scrollIntoView()
+ }
+ }
+}
+
+function getTabberTabs(tabber) {
+ // need to scope the selector to handle nested tabs. see /unturned/wiki/Crate for an example
+ const tabs = [...tabber.querySelectorAll(":scope > .wds-tabs__wrapper .wds-tabs__tab")]
+ const contents = [...tabber.querySelectorAll(":scope > .wds-tab__content")]
+ return tabs.map((_, index) => [tabs[index], contents[index]]) // transpose arrays into [[tab, content], ...]
+}
+
+function setCurrentTab(tabber, tab, content) {
+ // clear currently selected tab
+ getTabberTabs(tabber).flat().forEach(e => e.classList.remove("wds-is-current"))
+
+ // select new tab
+ tab.classList.add("wds-is-current")
+ content.classList.add("wds-is-current")
+ if (tab.dataset.hash) {
+ history.replaceState(null, "", `#${tab.dataset.hash}`)
+ }
+}
+
+document.body.classList.remove("bw-tabs-nojs")
diff --git a/static/three-balloons.png b/static/three-balloons.png
new file mode 100644
index 00000000..616d527e
Binary files /dev/null and b/static/three-balloons.png differ