" + "
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 0000000..c306b04
--- /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 0000000..047c8aa
--- /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 6c0a733..e1fe659 100644
--- a/src/page-category.rkt
+++ b/src/page-category.rkt
@@ -15,33 +15,38 @@
"application-globals.rkt"
"config.rkt"
"data.rkt"
+ "fandom-request.rkt"
"page-wiki.rkt"
- "syntax.rkt"
- "url-utils.rkt"
- "xexpr-utils.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
+ #:req req
#:source-url source-url
#:wikiname wikiname
#:title title
#:members-data members-data
#:page page
- #:body-class [body-class #f]
+ #:head-data [head-data #f]
#:siteinfo [siteinfo #f])
(define members (jp "/query/categorymembers" members-data))
(generate-wiki-page
+ #:req req
#:source-url source-url
#:wikiname wikiname
#:title title
- #:body-class body-class
+ #:head-data head-data
#:siteinfo siteinfo
`(div
,(update-tree-wiki page wikiname)
@@ -52,7 +57,7 @@
,@(map
(λ (result)
(define title (jp "/title" result))
- (define page-path (regexp-replace* #rx" " title "_"))
+ (define page-path (page-title->path title))
`(li
(a (@ (href ,(format "/~a/wiki/~a" wikiname page-path)))
,title)))
@@ -61,64 +66,61 @@
(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 source-url (format "~a/wiki/~a" origin prefixed-category))
- (thread-let
- ([members-data (define dest-url
- (format "~a/api.php?~a"
- origin
- (params->query `(("action" . "query")
- ("list" . "categorymembers")
- ("cmtitle" . ,prefixed-category)
- ("cmlimit" . "max")
- ("formatversion" . "2")
- ("format" . "json")))))
- (log-outgoing dest-url)
- (define dest-res (easy:get dest-url #:timeouts timeouts))
- (easy:response-json dest-res)]
- [page-data (define dest-url
- (format "~a/api.php?~a"
- origin
- (params->query `(("action" . "parse")
- ("page" . ,prefixed-category)
- ("prop" . "text|headhtml|langlinks")
- ("formatversion" . "2")
- ("format" . "json")))))
- (log-outgoing dest-url)
- (define dest-res (easy:get dest-url #:timeouts timeouts))
- (easy:response-json dest-res)]
- [siteinfo (siteinfo-fetch wikiname)])
+ (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-html (jp "/parse/headhtml" page-data ""))
- (define body-class (match (regexp-match #rx"]*class=\"([^\"]*)" head-html)
- [(list _ classes) classes]
- [_ ""]))
- (define body (generate-results-page
- #:source-url source-url
- #:wikiname wikiname
- #:title title
- #:members-data members-data
- #:page page
- #:body-class body-class
- #:siteinfo siteinfo))
+ (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))))))
+ (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
+ #:req test-req
#:source-url ""
#:wikiname "test"
#:title "Category:Items"
diff --git a/src/page-file.rkt b/src/page-file.rkt
index 5c3f896..5151f1d 100644
--- a/src/page-file.rkt
+++ b/src/page-file.rkt
@@ -15,15 +15,18 @@
"application-globals.rkt"
"config.rkt"
"data.rkt"
+ "fandom-request.rkt"
"page-wiki.rkt"
- "syntax.rkt"
- "url-utils.rkt"
- "xexpr-utils.rkt")
+ "../lib/syntax.rkt"
+ "../lib/thread-utils.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
(provide page-file)
(module+ test
- (require rackunit)
+ (require rackunit
+ "test-utils.rkt")
(define test-media-detail
'#hasheq((fileTitle . "Example file")
(videoEmbedCode . "")
@@ -31,15 +34,13 @@
(rawImageUrl . "https://static.wikia.nocookie.net/examplefile")
(userName . "blankie")
(isPostedIn . #t)
- (smallerArticleList . (#hasheq((title . "Example_article")
- (titleText . "Example article"))))
+ (smallerArticleList . (#hasheq((titleText . "Test:Example article"))))
(articleListIsSmaller . 0)
(exists . #t)
(imageDescription . #f))))
(define (url-content-type url)
- (log-outgoing url)
- (define dest-res (easy:head url #:timeouts timeouts))
+ (define dest-res (easy:head url))
(easy:response-headers-ref dest-res 'content-type))
(define (get-media-html url content-type)
@@ -52,7 +53,8 @@
[(regexp-match? #rx"(?i:^video/)" content-type) `(video (@ (src ,maybe-proxied-url) (controls)))]
[else `""]))
-(define (generate-results-page #:source-url source-url
+(define (generate-results-page #:req req
+ #:source-url source-url
#:wikiname wikiname
#:title title
#:media-detail media-detail
@@ -69,6 +71,7 @@
(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
@@ -79,65 +82,69 @@
(p ,(if (non-empty-string? video-embed-code)
`""
`(span (a (@ (href ,maybe-proxied-raw-image-url)) "View original file") ". "))
- "Added by "
+ "Uploaded by "
(a (@ (href ,(format "/~a/wiki/User:~a" wikiname username))) ,username)
- "."
- ,(if is-posted-in
- `(span " Posted in "
- ,@(map (λ (article)
- (define page-path (jp "/title" article))
- (define title (jp "/titleText" article page-path))
- `(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) "…" "."))
- `""))
+ ".")
,(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)
- (define wikiname (path/param-path (first (url-path (request-uri req)))))
- (define prefixed-title (path/param-path (caddr (url-path (request-uri req)))))
- (define origin (format "https://~a.fandom.com" wikiname))
- (define source-url (format "~a/wiki/~a" origin prefixed-title))
+ (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))
- (thread-let ([media-detail
- (define dest-url
- (format "~a/wikia.php?~a"
- origin
- (params->query `(("format" . "json") ("controller" . "Lightbox")
- ("method" . "getMediaDetail")
- ("fileTitle" . ,prefixed-title)))))
- (log-outgoing dest-url)
- (define dest-res (easy:get dest-url #:timeouts timeouts))
- (easy:response-json dest-res)]
- [siteinfo (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 #: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)))))))
+ (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")
@@ -145,12 +152,12 @@
(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) "no"])
+ (parameterize ([(config-parameter 'strict_proxy) "false"])
(check-equal? (get-media-html "https://static.wikia.nocookie.net/c" "application/ogg")
- `(audio (@ (src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fc")
+ `(audio (@ (src "https://static.wikia.nocookie.net/c")
(controls))))
(check-equal? (get-media-html "https://static.wikia.nocookie.net/d" "video/mp4")
- `(video (@ (src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fd")
+ `(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) `""))
@@ -159,7 +166,8 @@
(check-not-false
((query-selector
(attribute-selector 'src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fexamplefile")
- (generate-results-page #:source-url ""
+ (generate-results-page #:req test-req
+ #:source-url ""
#:wikiname "test"
#:title "File:Example file"
#:media-detail test-media-detail
diff --git a/src/page-global-search.rkt b/src/page-global-search.rkt
index 4364dea..08bfd13 100644
--- a/src/page-global-search.rkt
+++ b/src/page-global-search.rkt
@@ -4,8 +4,9 @@
net/url
web-server/http
"application-globals.rkt"
- "url-utils.rkt"
- "xexpr-utils.rkt")
+ "data.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
(provide
page-global-search)
@@ -14,12 +15,18 @@
(define wikiname (dict-ref (url-query (request-uri req)) 'wikiname #f))
(define q (dict-ref (url-query (request-uri req)) 'q #f))
(response-handler
- (if (not (and wikiname q))
- (response/output
- #:code 400
- #:mime-type "text/plain"
- (λ (out)
- (displayln "Requires wikiname and q parameters." out)))
- (generate-redirect (format "/~a/search?~a"
- wikiname
- (params->query `(("q" . ,q))))))))
+ (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 b16f66a..24f7393 100644
--- a/src/page-home.rkt
+++ b/src/page-home.rkt
@@ -4,8 +4,10 @@
html-writing
web-server/http
"application-globals.rkt"
- "url-utils.rkt"
- "xexpr-utils.rkt"
+ "data.rkt"
+ "static-data.rkt"
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt"
"config.rkt")
(provide
@@ -15,24 +17,27 @@
(require rackunit))
(define examples
- '(("crosscode" "CrossCode_Wiki")
- ("pokemon" "Eevee")
- ("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 slow down your device or use up your data.")
- (p "BreezeWiki can also be called an \"alternative frontend for Fandom\".")
(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"))
@@ -40,42 +45,47 @@
(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") (required))))
+ (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 "Testimonials")
- (p (@ (class "testimonial")) ">So glad to never have to touch fandom's garbage platform directly ever again —RNL")
- (p (@ (class "testimonial")) ">you are so right that fandom still sucks even with adblock somehow. even zapping all the stupid padding it still sucks —Minimus")
+ (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))
- ,(application-footer #f)))))))
+ `(*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)))
diff --git a/src/page-it-works.rkt b/src/page-it-works.rkt
new file mode 100644
index 0000000..ce9e05f
--- /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 3c22e1e..cd94603 100644
--- a/src/page-proxy.rkt
+++ b/src/page-proxy.rkt
@@ -9,8 +9,8 @@
web-server/http
(only-in web-server/dispatchers/dispatch next-dispatcher)
"application-globals.rkt"
- "url-utils.rkt"
- "xexpr-utils.rkt")
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
(provide
page-proxy)
diff --git a/src/page-redirect-wiki-home.rkt b/src/page-redirect-wiki-home.rkt
index c8e6dde..255f625 100644
--- a/src/page-redirect-wiki-home.rkt
+++ b/src/page-redirect-wiki-home.rkt
@@ -3,8 +3,8 @@
web-server/http
"application-globals.rkt"
"data.rkt"
- "url-utils.rkt"
- "xexpr-utils.rkt")
+ "../lib/url-utils.rkt"
+ "../lib/xexpr-utils.rkt")
(provide
redirect-wiki-home)
diff --git a/src/page-search.rkt b/src/page-search.rkt
index 81a88b2..39f361a 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
@@ -13,77 +12,75 @@
"application-globals.rkt"
"config.rkt"
"data.rkt"
- "syntax.rkt"
- "url-utils.rkt"
- "xexpr-utils.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 #:siteinfo [siteinfo #f])
- (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
- #:source-url dest-url
+ ;; so I provide my helper function with the necessary context...
+ #:req req
+ #:source-url source-url
#:wikiname wikiname
- #:title "Search Results"
+ #:title query
#:siteinfo siteinfo
- `(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)))))
+ ;; 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))
- (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")))))
+ ;; 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"))))
- (thread-let
- ([dest-res (log-outgoing dest-url)
- (easy:get dest-url #:timeouts timeouts)]
- [siteinfo (siteinfo-fetch wikiname)])
+ ;; 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
+
+ ;; 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)))))
- (define body (generate-results-page dest-url wikiname query 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/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 0000000..b949142
--- /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 0000000..501bda7
--- /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 e684c74..0311229 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 0000000..199eac2
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 0000000..ccc64f7
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 0000000..dd4ac82
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 0000000..bc9c203
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 0000000..718b48e
--- /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 0000000..616d527
Binary files /dev/null and b/static/three-balloons.png differ