Compare commits
No commits in common. "main" and "main" have entirely different histories.
31 changed files with 405 additions and 1703 deletions
|
|
@ -1,7 +1,5 @@
|
||||||
#lang racket/base
|
#lang racket/base
|
||||||
(require racket/splicing
|
(require web-server/servlet-dispatch
|
||||||
web-server/servlet-dispatch
|
|
||||||
web-server/safety-limits
|
|
||||||
"src/config.rkt"
|
"src/config.rkt"
|
||||||
"src/dispatcher-tree.rkt"
|
"src/dispatcher-tree.rkt"
|
||||||
"src/reloadable.rkt")
|
"src/reloadable.rkt")
|
||||||
|
|
@ -11,9 +9,6 @@
|
||||||
(reloadable-entry-point->procedure
|
(reloadable-entry-point->procedure
|
||||||
(make-reloadable-entry-point (quote varname) filename))))
|
(make-reloadable-entry-point (quote varname) filename))))
|
||||||
|
|
||||||
(require-reloadable "src/page-captcha.rkt" page-captcha)
|
|
||||||
(require-reloadable "src/page-captcha.rkt" page-captcha-image)
|
|
||||||
(require-reloadable "src/page-captcha.rkt" page-captcha-verify)
|
|
||||||
(require-reloadable "src/page-category.rkt" page-category)
|
(require-reloadable "src/page-category.rkt" page-category)
|
||||||
(require-reloadable "src/page-global-search.rkt" page-global-search)
|
(require-reloadable "src/page-global-search.rkt" page-global-search)
|
||||||
(require-reloadable "src/page-home.rkt" page-home)
|
(require-reloadable "src/page-home.rkt" page-home)
|
||||||
|
|
@ -39,14 +34,10 @@
|
||||||
(if (config-true? 'debug) "127.0.0.1" #f)
|
(if (config-true? 'debug) "127.0.0.1" #f)
|
||||||
(config-get 'bind_host))
|
(config-get 'bind_host))
|
||||||
#:port (string->number (config-get 'port))
|
#:port (string->number (config-get 'port))
|
||||||
#:safety-limits (make-safety-limits #:max-request-body-length (* 8 1024 1024))
|
|
||||||
(λ (quit)
|
(λ (quit)
|
||||||
(channel-put ch (lambda () (semaphore-post quit)))
|
(channel-put ch (lambda () (semaphore-post quit)))
|
||||||
(dispatcher-tree
|
(dispatcher-tree
|
||||||
; order of these does not matter
|
; order of these does not matter
|
||||||
page-captcha
|
|
||||||
page-captcha-image
|
|
||||||
page-captcha-verify
|
|
||||||
page-category
|
page-category
|
||||||
page-global-search
|
page-global-search
|
||||||
page-home
|
page-home
|
||||||
|
|
|
||||||
6
dist.rkt
6
dist.rkt
|
|
@ -1,10 +1,8 @@
|
||||||
#lang racket/base
|
#lang racket/base
|
||||||
(require web-server/servlet-dispatch
|
(require web-server/servlet-dispatch
|
||||||
web-server/safety-limits
|
|
||||||
"src/config.rkt"
|
"src/config.rkt"
|
||||||
"src/dispatcher-tree.rkt")
|
"src/dispatcher-tree.rkt")
|
||||||
|
|
||||||
(require (only-in "src/page-captcha.rkt" page-captcha page-captcha-image page-captcha-verify))
|
|
||||||
(require (only-in "src/page-category.rkt" page-category))
|
(require (only-in "src/page-category.rkt" page-category))
|
||||||
(require (only-in "src/page-global-search.rkt" page-global-search))
|
(require (only-in "src/page-global-search.rkt" page-global-search))
|
||||||
(require (only-in "src/page-home.rkt" page-home))
|
(require (only-in "src/page-home.rkt" page-home))
|
||||||
|
|
@ -26,13 +24,9 @@
|
||||||
(if (config-true? 'debug) "127.0.0.1" #f)
|
(if (config-true? 'debug) "127.0.0.1" #f)
|
||||||
(config-get 'bind_host))
|
(config-get 'bind_host))
|
||||||
#:port (string->number (config-get 'port))
|
#:port (string->number (config-get 'port))
|
||||||
#:safety-limits (make-safety-limits #:max-request-body-length (* 8 1024 1024))
|
|
||||||
(λ (quit)
|
(λ (quit)
|
||||||
(dispatcher-tree
|
(dispatcher-tree
|
||||||
; order of these does not matter
|
; order of these does not matter
|
||||||
page-captcha
|
|
||||||
page-captcha-image
|
|
||||||
page-captcha-verify
|
|
||||||
page-category
|
page-category
|
||||||
page-global-search
|
page-global-search
|
||||||
page-home
|
page-home
|
||||||
|
|
|
||||||
2
info.rkt
2
info.rkt
|
|
@ -1,3 +1,3 @@
|
||||||
#lang info
|
#lang info
|
||||||
|
|
||||||
(define build-deps '("rackunit-lib" "web-server-lib" "http-easy-lib" "html-parsing" "html-writing" "json-pointer" "typed-ini-lib" "memo" "net-cookies-lib" "db" "sequence-tools-lib"))
|
(define build-deps '("rackunit-lib" "web-server-lib" "http-easy-lib" "html-parsing" "html-writing" "json-pointer" "typed-ini-lib" "memo" "net-cookies-lib" "db"))
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,4 @@
|
||||||
(uri-decode (regexp-replace* #rx"#" str "/")))
|
(uri-decode (regexp-replace* #rx"#" str "/")))
|
||||||
|
|
||||||
(define (url-segments->guess-title segments)
|
(define (url-segments->guess-title segments)
|
||||||
(string-join
|
(regexp-replace* #rx"_" (cadr segments) " "))
|
||||||
(for/list ([s (in-list (cdr segments))])
|
|
||||||
(regexp-replace* #rx"_" s " "))
|
|
||||||
"/"))
|
|
||||||
|
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
#lang racket/base
|
|
||||||
(require racket/list/grouping
|
|
||||||
racket/match
|
|
||||||
racket/syntax)
|
|
||||||
|
|
||||||
(provide make-json)
|
|
||||||
|
|
||||||
(module+ test
|
|
||||||
(require rackunit json)
|
|
||||||
(define sample
|
|
||||||
`(: continue
|
|
||||||
(: iistart "2022-01-23T03:44:17Z"
|
|
||||||
fucontinue "455"
|
|
||||||
continue "||")
|
|
||||||
query
|
|
||||||
(: pages
|
|
||||||
(: 198
|
|
||||||
(: pageid 198
|
|
||||||
ns 6
|
|
||||||
title "File:Rainbow Flag1.svg"
|
|
||||||
imageinfo
|
|
||||||
((: timestamp "2025-03-10T07:24:50Z"
|
|
||||||
user "DogeMcMeow"))
|
|
||||||
fileusage
|
|
||||||
((: pageid 191
|
|
||||||
ns 0
|
|
||||||
title "Gay")
|
|
||||||
(: pageid 215
|
|
||||||
ns 0
|
|
||||||
title "LGBTQIA+"))))))))
|
|
||||||
|
|
||||||
(define (make-json data)
|
|
||||||
(match data
|
|
||||||
[(list ': kvs ...)
|
|
||||||
(for/fold ([h (hasheq)])
|
|
||||||
([kv (windows 2 2 kvs)])
|
|
||||||
(match-define (list raw-k v) kv)
|
|
||||||
(define k (format-symbol "~a" raw-k))
|
|
||||||
(hash-set h k (make-json v)))]
|
|
||||||
[(list x ...)
|
|
||||||
(map make-json x)]
|
|
||||||
[x
|
|
||||||
x]))
|
|
||||||
|
|
||||||
(module+ test
|
|
||||||
(check-equal? (make-json sample)
|
|
||||||
(string->jsexpr #<<END
|
|
||||||
{
|
|
||||||
"continue": {
|
|
||||||
"iistart": "2022-01-23T03:44:17Z",
|
|
||||||
"fucontinue": "455",
|
|
||||||
"continue": "||"
|
|
||||||
},
|
|
||||||
"query": {
|
|
||||||
"pages": {
|
|
||||||
"198": {
|
|
||||||
"pageid": 198,
|
|
||||||
"ns": 6,
|
|
||||||
"title": "File:Rainbow Flag1.svg",
|
|
||||||
"imageinfo": [
|
|
||||||
{
|
|
||||||
"timestamp": "2025-03-10T07:24:50Z",
|
|
||||||
"user": "DogeMcMeow"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"fileusage": [
|
|
||||||
{
|
|
||||||
"pageid": 191,
|
|
||||||
"ns": 0,
|
|
||||||
"title": "Gay"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"pageid": 215,
|
|
||||||
"ns": 0,
|
|
||||||
"title": "LGBTQIA+"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
END
|
|
||||||
)))
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#lang racket/base
|
#lang racket/base
|
||||||
(require "../src/data.rkt"
|
(require (prefix-in easy: net/http-easy)
|
||||||
|
"../src/data.rkt"
|
||||||
"xexpr-utils.rkt")
|
"xexpr-utils.rkt")
|
||||||
|
|
||||||
(provide
|
(provide
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
update-tree-wiki)
|
update-tree-wiki)
|
||||||
|
|
||||||
(define (preprocess-html-wiki html)
|
(define (preprocess-html-wiki html)
|
||||||
(regexp-replace* #rx"(<(?:td|figcaption)[^>]*?>\n?)(?:[ \t]*<a href=\"[^\"]*\" class=\"info-icon\"><svg><use xlink:href=\"#wds-icons-info-small\"></use></svg></a>)?(?:<li>|[ \t]*?<p class=\"caption\">(.*?)</p>)"
|
(regexp-replace* #rx"(<(?:td|figcaption)[^>]*?>\n?)(?:<li>|[ \t]*?<p class=\"caption\">(.*?)</p>)"
|
||||||
html (λ (whole first-tag [contents #f])
|
html (λ (whole first-tag [contents #f])
|
||||||
(if (eq? (string-ref whole 1) #\f) ;; figcaption
|
(if (eq? (string-ref whole 1) #\f) ;; figcaption
|
||||||
(string-append first-tag "<span class=\"caption\">" contents "</span>")
|
(string-append first-tag "<span class=\"caption\">" contents "</span>")
|
||||||
|
|
@ -58,16 +58,6 @@
|
||||||
(data-src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
(data-src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
||||||
(class "thumbimage")))))
|
(class "thumbimage")))))
|
||||||
(figcaption "Test figure!"))
|
(figcaption "Test figure!"))
|
||||||
(div (@ (type "slideshow") (position "center") (widths "500") (mode "slideshow") (seq-no "0") (id "slideshow-0") (hash "b62d0efee427ad7dff1026e6e9dd078c") (class "wikia-slideshow wikia-gallery slideshow-center"))
|
|
||||||
(div (@ (class "wikia-slideshow-wrapper") (style "width: 500px") (data-test-outer-width))
|
|
||||||
(div (@ (class "wikia-slideshow-images-wrapper"))
|
|
||||||
(ul (@ (class "wikia-slideshow-images neutral") (style "height: 375px; width: 500px") (data-test-inner-width))
|
|
||||||
(li (@ (class "wikia-slideshow-first-image"))
|
|
||||||
(a (@ (class "image lightbox") (title "Expand slideshow") (id "slideshow-0-0") (style "width: 420px"))
|
|
||||||
(img (@ (data-src "https://static.wikia.nocookie.net/example/images/3/3d/Image.jpg/revision/latest/scale-to-width-down/500?cb=20140129105112") (class "thumbimage") (width "500") (height "281") (style "border: 0px;"))))
|
|
||||||
(div (@ (class "wikia-slideshow-overlay"))
|
|
||||||
(div (@ (class "wikia-slideshow-image-caption"))
|
|
||||||
"Example caption")))))))
|
|
||||||
(iframe (@ (src "https://example.com/iframe-src")))
|
(iframe (@ (src "https://example.com/iframe-src")))
|
||||||
(div (@ (class "reviews"))
|
(div (@ (class "reviews"))
|
||||||
(header "GameSpot Expert Reviews"))
|
(header "GameSpot Expert Reviews"))
|
||||||
|
|
@ -235,16 +225,6 @@
|
||||||
(eq? element-type 'use))
|
(eq? element-type 'use))
|
||||||
element))))
|
element))))
|
||||||
return-no-element]
|
return-no-element]
|
||||||
; svg icon for external links
|
|
||||||
[(and (eq? element-type 'a)
|
|
||||||
(dict-has-key? attributes 'href)
|
|
||||||
(not (string-contains? (car (dict-ref attributes 'href)) "wikipedia.org"))
|
|
||||||
(or (has-class? "extiw" attributes) (has-class? "external" attributes)))
|
|
||||||
`(,element-type
|
|
||||||
,attributes
|
|
||||||
(,@children
|
|
||||||
(svg (@ (fill "currentColor") (width "12") (height "12") (viewBox "0 0 12 12") (class "external"))
|
|
||||||
(path (@ (d "M6 1h5v5L8.86 3.85 4.7 8 4 7.3l4.15-4.16zM2 3h2v1H2v6h6V8h1v2a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1"))))))]
|
|
||||||
; exclude infobox items that are videos, and gallery items that are videos
|
; exclude infobox items that are videos, and gallery items that are videos
|
||||||
[(and (or (has-class? "pi-item" attributes)
|
[(and (or (has-class? "pi-item" attributes)
|
||||||
(has-class? "wikia-gallery-item" attributes))
|
(has-class? "wikia-gallery-item" attributes))
|
||||||
|
|
@ -274,9 +254,6 @@
|
||||||
; remove gamespot reviews/ads
|
; remove gamespot reviews/ads
|
||||||
[(has-class? "reviews" attributes)
|
[(has-class? "reviews" attributes)
|
||||||
return-no-element]
|
return-no-element]
|
||||||
; remove genai quick answers
|
|
||||||
[(has-class? "trfc161" attributes)
|
|
||||||
return-no-element]
|
|
||||||
; remove customcollapsible customtoggle buttons - sample: warframe/wiki/Amp_(Ability)
|
; remove customcollapsible customtoggle buttons - sample: warframe/wiki/Amp_(Ability)
|
||||||
[(and (dict-has-key? attributes 'class) (regexp-match? #rx"^mw-customtoggle-[^ ]* button-c$" (car (dict-ref attributes 'class))))
|
[(and (dict-has-key? attributes 'class) (regexp-match? #rx"^mw-customtoggle-[^ ]* button-c$" (car (dict-ref attributes 'class))))
|
||||||
return-no-element]
|
return-no-element]
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@
|
||||||
(provide
|
(provide
|
||||||
; headers to always send on all http responses
|
; headers to always send on all http responses
|
||||||
always-headers
|
always-headers
|
||||||
|
; timeout durations for http-easy requests
|
||||||
|
timeouts
|
||||||
; generates a consistent footer
|
; generates a consistent footer
|
||||||
application-footer
|
application-footer
|
||||||
; generates a consistent template for wiki page content to sit in
|
; generates a consistent template for wiki page content to sit in
|
||||||
|
|
@ -37,6 +39,7 @@
|
||||||
(define always-headers
|
(define always-headers
|
||||||
(list (header #"Referrer-Policy" #"same-origin") ; header to not send referers to fandom
|
(list (header #"Referrer-Policy" #"same-origin") ; header to not send referers to fandom
|
||||||
(header #"Link" (string->bytes/latin-1 link-header))))
|
(header #"Link" (string->bytes/latin-1 link-header))))
|
||||||
|
(define timeouts (easy:make-timeout-config #:lease 5 #:connect 5))
|
||||||
|
|
||||||
(define-runtime-path path-static "../static")
|
(define-runtime-path path-static "../static")
|
||||||
(define theme-icons
|
(define theme-icons
|
||||||
|
|
@ -68,6 +71,9 @@
|
||||||
`(p ,(format "This instance is run by the ~a developer, " (config-get 'application_name))
|
`(p ,(format "This instance is run by the ~a developer, " (config-get 'application_name))
|
||||||
(a (@ (href "https://cadence.moe/contact"))
|
(a (@ (href "https://cadence.moe/contact"))
|
||||||
"Cadence")
|
"Cadence")
|
||||||
|
". Proudly hosted by "
|
||||||
|
(a (@ (href "http://alphamethyl.barr0w.net"))
|
||||||
|
"Barrow Network Solutions" (sup "XD"))
|
||||||
".")
|
".")
|
||||||
`(p
|
`(p
|
||||||
,(format "This unofficial instance is based off the ~a source code, but is not controlled by the code developer." (config-get 'application_name)))))
|
,(format "This unofficial instance is based off the ~a source code, but is not controlled by the code developer." (config-get 'application_name)))))
|
||||||
|
|
@ -85,13 +91,11 @@
|
||||||
|
|
||||||
;; generate a notice with a link if a fandom wiki has a replacement as part of NIWA or similar
|
;; generate a notice with a link if a fandom wiki has a replacement as part of NIWA or similar
|
||||||
;; if the wiki has no replacement, display nothing
|
;; if the wiki has no replacement, display nothing
|
||||||
(define (extwiki-notice wikiname title req user-cookies)
|
(define (extwiki-notice wikiname title)
|
||||||
(define xt (findf (λ (item) (member wikiname (extwiki^-wikinames item))) extwikis))
|
(define xt (findf (λ (item) (member wikiname (extwiki^-wikinames item))) extwikis))
|
||||||
(cond/var
|
(cond/var
|
||||||
[xt
|
[xt
|
||||||
(let* ([seen? (member wikiname (user-cookies^-notices user-cookies))]
|
(let* ([group (hash-ref extwiki-groups (extwiki^-group xt))]
|
||||||
[aside-class (if seen? "niwa__notice niwa--seen" "niwa__notice")]
|
|
||||||
[group (hash-ref extwiki-groups (extwiki^-group xt))]
|
|
||||||
[search-page (format "/Special:Search?~a"
|
[search-page (format "/Special:Search?~a"
|
||||||
(params->query `(("search" . ,title)
|
(params->query `(("search" . ,title)
|
||||||
("go" . "Go"))))]
|
("go" . "Go"))))]
|
||||||
|
|
@ -102,24 +106,21 @@
|
||||||
[props (extwiki-props^ go)])
|
[props (extwiki-props^ go)])
|
||||||
(cond
|
(cond
|
||||||
[(eq? (extwiki^-banner xt) 'default)
|
[(eq? (extwiki^-banner xt) 'default)
|
||||||
`(aside (@ (class ,aside-class))
|
`(aside (@ (class "niwa__notice"))
|
||||||
(h1 (@ (class "niwa__header")) ,(extwiki^-name xt) " has its own website separate from Fandom.")
|
(h1 (@ (class "niwa__header")) ,(extwiki^-name xt) " has its own website separate from Fandom.")
|
||||||
(a (@ (class "niwa__go") (href ,go)) "Read " ,title " on " ,(extwiki^-name xt) " →")
|
(a (@ (class "niwa__go") (href ,go)) "Read " ,title " on " ,(extwiki^-name xt) " →")
|
||||||
(div (@ (class "niwa__cols"))
|
(div (@ (class "niwa__cols"))
|
||||||
(div (@ (class "niwa__left"))
|
(div (@ (class "niwa__left"))
|
||||||
(p ,((extwiki^-description xt) props))
|
|
||||||
(p ,((extwiki-group^-description group) props))
|
(p ,((extwiki-group^-description group) props))
|
||||||
(p "This wiki's core community has largely migrated away from Fandom. You should "
|
(p ,((extwiki^-description xt) props))
|
||||||
|
(p "This wiki's core community has wholly migrated away from Fandom. You should "
|
||||||
(a (@ (href ,go)) "go to " ,(extwiki^-name xt) " now!"))
|
(a (@ (href ,go)) "go to " ,(extwiki^-name xt) " now!"))
|
||||||
(p (@ (class "niwa__feedback"))
|
(p (@ (class "niwa__feedback"))
|
||||||
,@(add-between
|
,@(add-between
|
||||||
`(,@(for/list ([link (extwiki-group^-links group)])
|
`(,@(for/list ([link (extwiki-group^-links group)])
|
||||||
`(a (@ (href ,(cdr link))) ,(car link)))
|
`(a (@ (href ,(cdr link))) ,(car link)))
|
||||||
"This notice is from BreezeWiki"
|
"This notice is from BreezeWiki"
|
||||||
(a (@ (rel "nofollow")
|
(a (@ (href "https://docs.breezewiki.com/Reporting_Bugs.html")) "Feedback?"))
|
||||||
(class "niwa__got-it")
|
|
||||||
(href ,(user-cookies-setter-url/add-notice req user-cookies wikiname)))
|
|
||||||
"OK, got it"))
|
|
||||||
" / ")))
|
" / ")))
|
||||||
(div (@ (class "niwa__right"))
|
(div (@ (class "niwa__right"))
|
||||||
(img (@ (class "niwa__logo") (src ,(extwiki^-logo xt)))))))]
|
(img (@ (class "niwa__logo") (src ,(extwiki^-logo xt)))))))]
|
||||||
|
|
@ -163,21 +164,18 @@
|
||||||
#:head-data [head-data-in #f]
|
#:head-data [head-data-in #f]
|
||||||
#:siteinfo [siteinfo-in #f]
|
#:siteinfo [siteinfo-in #f]
|
||||||
#:user-cookies [user-cookies-in #f]
|
#:user-cookies [user-cookies-in #f]
|
||||||
#:online-styles [online-styles #t]
|
#:online-styles [online-styles #t])
|
||||||
#:path [path-in #f]
|
|
||||||
#:jsonp [jsonp-in #f])
|
|
||||||
(define siteinfo (or siteinfo-in siteinfo-default))
|
(define siteinfo (or siteinfo-in siteinfo-default))
|
||||||
(define head-data (or head-data-in ((head-data-getter wikiname))))
|
(define head-data (or head-data-in ((head-data-getter wikiname))))
|
||||||
(define user-cookies (or user-cookies-in (user-cookies-getter req)))
|
(define user-cookies (or user-cookies-in (user-cookies-getter req)))
|
||||||
(define origin (format "https://~a.fandom.com" wikiname))
|
(define origin (format "https://~a.fandom.com" wikiname))
|
||||||
(define path (or path-in ""))
|
|
||||||
(define required-styles
|
(define required-styles
|
||||||
(cond
|
(cond
|
||||||
[online-styles
|
[online-styles
|
||||||
(define styles
|
(define styles
|
||||||
(list
|
(list
|
||||||
(format "~a/wikia.php?controller=ThemeApi&method=themeVariables&variant=~a" origin (user-cookies^-theme user-cookies))
|
(format "~a/wikia.php?controller=ThemeApi&method=themeVariables&variant=~a" origin (user-cookies^-theme user-cookies))
|
||||||
(format "~a/load.php?lang=en&modules=site.styles%7Cskin.fandomdesktop.styles%7Cext.fandom.PortableInfoboxFandomDesktop.css%7Cext.fandom.GlobalComponents.CommunityHeaderBackground.css%7Cext.fandom.photoGallery.gallery.css%7Cext.gadget.site-styles%2Csound-styles&only=styles&skin=fandomdesktop" origin)))
|
(format "~a/load.php?lang=en&modules=site.styles%7Cskin.fandomdesktop.styles%7Cext.fandom.PortableInfoboxFandomDesktop.css%7Cext.fandom.GlobalComponents.CommunityHeaderBackground.css%7Cext.gadget.site-styles%2Csound-styles&only=styles&skin=fandomdesktop" origin)))
|
||||||
(if (config-true? 'strict_proxy)
|
(if (config-true? 'strict_proxy)
|
||||||
(map u-proxy-url styles)
|
(map u-proxy-url styles)
|
||||||
styles)]
|
styles)]
|
||||||
|
|
@ -200,19 +198,15 @@
|
||||||
(link (@ (rel "stylesheet") (type "text/css") (href ,(get-static-url "main.css"))))
|
(link (@ (rel "stylesheet") (type "text/css") (href ,(get-static-url "main.css"))))
|
||||||
(script "const BWData = "
|
(script "const BWData = "
|
||||||
,(jsexpr->string (hasheq 'wikiname wikiname
|
,(jsexpr->string (hasheq 'wikiname wikiname
|
||||||
'strict_proxy (config-true? 'strict_proxy)
|
'strict_proxy (config-true? 'strict_proxy))))
|
||||||
'path path
|
|
||||||
'jsonp jsonp-in)))
|
|
||||||
(script (@ (type "importmap")) ,importmap)
|
|
||||||
,(if (config-true? 'feature_search_suggestions)
|
,(if (config-true? 'feature_search_suggestions)
|
||||||
`(script (@ (type "module") (src ,(get-static-url "search-suggestions.js"))))
|
`(script (@ (type "module") (src ,(get-static-url "search-suggestions.js"))))
|
||||||
"")
|
"")
|
||||||
(script (@ (type "module") (src ,(get-static-url "countdown.js"))))
|
(script (@ (type "module") (src ,(get-static-url "countdown.js"))))
|
||||||
(script (@ (type "module") (src ,(get-static-url "tabs.js"))))
|
|
||||||
(link (@ (rel "icon") (href ,(u (λ (v) (config-true? 'strict_proxy))
|
(link (@ (rel "icon") (href ,(u (λ (v) (config-true? 'strict_proxy))
|
||||||
(λ (v) (u-proxy-url v))
|
(λ (v) (u-proxy-url v))
|
||||||
(head-data^-icon-url head-data))))))
|
(head-data^-icon-url head-data))))))
|
||||||
(body (@ (class ,(head-data^-body-class head-data) " bw-tabs-nojs"))
|
(body (@ (class ,(head-data^-body-class head-data)))
|
||||||
,(let ([extension-eligible?
|
,(let ([extension-eligible?
|
||||||
(cond/var
|
(cond/var
|
||||||
[(not req) #f]
|
[(not req) #f]
|
||||||
|
|
@ -233,20 +227,9 @@
|
||||||
(div (@ (class "fandom-community-header__background tileHorizontally header")))
|
(div (@ (class "fandom-community-header__background tileHorizontally header")))
|
||||||
(div (@ (class "page"))
|
(div (@ (class "page"))
|
||||||
(main (@ (class "page__main"))
|
(main (@ (class "page__main"))
|
||||||
,(extwiki-notice wikiname title req user-cookies)
|
,(extwiki-notice wikiname title)
|
||||||
(div (@ (class "custom-top"))
|
(div (@ (class "custom-top"))
|
||||||
(h1 (@ (class "page-title"))
|
(h1 (@ (class "page-title")) ,title)
|
||||||
;; adds rudimentary "up" navigation, e.g. /minecraft/wiki/Bastion_Remnant/Structure/Blueprints/Bastion_treasure_corners_edge_middle_blueprint
|
|
||||||
,@(let ([segments (string-split title "/")])
|
|
||||||
(add-between
|
|
||||||
(for/list ([segment (in-list segments)]
|
|
||||||
[depth (in-naturals 1)])
|
|
||||||
(define anti-depth (- (length segments) depth))
|
|
||||||
(define up-href (string-join (make-list anti-depth "..") "/"))
|
|
||||||
(if (non-empty-string? up-href)
|
|
||||||
`(a (@ (href ,(format "~a/~a" up-href segment))) ,segment)
|
|
||||||
segment))
|
|
||||||
"/")))
|
|
||||||
(nav (@ (class "sitesearch"))
|
(nav (@ (class "sitesearch"))
|
||||||
(form (@ (action ,(format "/~a/search" wikiname))
|
(form (@ (action ,(format "/~a/search" wikiname))
|
||||||
(class "bw-search-form")
|
(class "bw-search-form")
|
||||||
|
|
|
||||||
|
|
@ -49,12 +49,6 @@
|
||||||
(feature_offline::only . "false")
|
(feature_offline::only . "false")
|
||||||
(feature_offline::search . "fandom")
|
(feature_offline::search . "fandom")
|
||||||
|
|
||||||
(feature_jsonp::enabled . "true")
|
|
||||||
|
|
||||||
(captcha::enabled . "false")
|
|
||||||
(captcha::log . "false")
|
|
||||||
(captcha::ip_header . "")
|
|
||||||
|
|
||||||
(access_log::enabled . "false")
|
(access_log::enabled . "false")
|
||||||
|
|
||||||
(promotions::indie_wiki_buddy . "banner home")))
|
(promotions::indie_wiki_buddy . "banner home")))
|
||||||
|
|
@ -110,7 +104,7 @@
|
||||||
; all values here are optimised for maximum prettiness
|
; all values here are optimised for maximum prettiness
|
||||||
(parameterize ([pretty-print-columns 80])
|
(parameterize ([pretty-print-columns 80])
|
||||||
(display "config: ")
|
(display "config: ")
|
||||||
(pretty-write ((inst sort (Pairof Symbol String) Symbol)
|
(pretty-write ((inst sort (Pairof Symbol String))
|
||||||
(hash->list (make-immutable-hasheq combined-alist))
|
(hash->list (make-immutable-hasheq combined-alist))
|
||||||
symbol<?
|
symbol<?
|
||||||
#:key car))))
|
#:key car))))
|
||||||
|
|
|
||||||
61
src/data.rkt
61
src/data.rkt
|
|
@ -1,15 +1,14 @@
|
||||||
#lang racket/base
|
#lang racket/base
|
||||||
(require racket/list
|
(require racket/list
|
||||||
racket/match
|
racket/match
|
||||||
racket/string
|
|
||||||
web-server/http/request-structs
|
web-server/http/request-structs
|
||||||
net/url-string
|
net/url-string
|
||||||
(only-in net/cookies/server cookie-header->alist cookie->set-cookie-header make-cookie)
|
(only-in net/cookies/server cookie-header->alist cookie->set-cookie-header make-cookie)
|
||||||
(prefix-in easy: net/http-easy)
|
(prefix-in easy: net/http-easy)
|
||||||
db
|
db
|
||||||
memo
|
memo
|
||||||
"fandom-request.rkt"
|
|
||||||
"static-data.rkt"
|
"static-data.rkt"
|
||||||
|
"whole-utils.rkt"
|
||||||
"../lib/url-utils.rkt"
|
"../lib/url-utils.rkt"
|
||||||
"../lib/xexpr-utils.rkt"
|
"../lib/xexpr-utils.rkt"
|
||||||
"../archiver/archiver-database.rkt"
|
"../archiver/archiver-database.rkt"
|
||||||
|
|
@ -20,7 +19,6 @@
|
||||||
(struct-out license^)
|
(struct-out license^)
|
||||||
(struct-out head-data^)
|
(struct-out head-data^)
|
||||||
(struct-out user-cookies^)
|
(struct-out user-cookies^)
|
||||||
data->siteinfo
|
|
||||||
siteinfo-fetch
|
siteinfo-fetch
|
||||||
siteinfo-default
|
siteinfo-default
|
||||||
license-default
|
license-default
|
||||||
|
|
@ -29,8 +27,7 @@
|
||||||
user-cookies-getter
|
user-cookies-getter
|
||||||
user-cookies-default
|
user-cookies-default
|
||||||
user-cookies-setter
|
user-cookies-setter
|
||||||
user-cookies-setter-url
|
user-cookies-setter-url)
|
||||||
user-cookies-setter-url/add-notice)
|
|
||||||
|
|
||||||
(struct siteinfo^ (sitename basepage license) #:transparent)
|
(struct siteinfo^ (sitename basepage license) #:transparent)
|
||||||
(struct license^ (text url) #:transparent)
|
(struct license^ (text url) #:transparent)
|
||||||
|
|
@ -57,24 +54,21 @@
|
||||||
(vector-ref row 3)))
|
(vector-ref row 3)))
|
||||||
siteinfo-default)]
|
siteinfo-default)]
|
||||||
[else
|
[else
|
||||||
(define res
|
(define dest-url
|
||||||
(fandom-get-api
|
(format "https://~a.fandom.com/api.php?~a"
|
||||||
wikiname
|
wikiname
|
||||||
'(("action" . "query")
|
(params->query '(("action" . "query")
|
||||||
("meta" . "siteinfo")
|
("meta" . "siteinfo")
|
||||||
("siprop" . "general|rightsinfo")
|
("siprop" . "general|rightsinfo")
|
||||||
("format" . "json")
|
("format" . "json")
|
||||||
("formatversion" . "2"))))
|
("formatversion" . "2")))))
|
||||||
(cond [(= (easy:response-status-code res) 200)
|
(log-outgoing dest-url)
|
||||||
(define data (easy:response-json res))
|
(define res (easy:get dest-url))
|
||||||
(data->siteinfo data)]
|
(define data (easy:response-json res))
|
||||||
[else siteinfo-default])]))
|
(siteinfo^ (jp "/query/general/sitename" data)
|
||||||
|
(second (regexp-match #rx"/wiki/(.*)" (jp "/query/general/base" data)))
|
||||||
(define (data->siteinfo data)
|
(license^ (jp "/query/rightsinfo/text" data)
|
||||||
(siteinfo^ (jp "/query/general/sitename" data)
|
(jp "/query/rightsinfo/url" data)))]))
|
||||||
(second (regexp-match #rx"/wiki/(.*)" (jp "/query/general/base" data)))
|
|
||||||
(license^ (jp "/query/rightsinfo/text" data)
|
|
||||||
(jp "/query/rightsinfo/url" data))))
|
|
||||||
|
|
||||||
(define/memoize (head-data-getter wikiname) #:hash hash
|
(define/memoize (head-data-getter wikiname) #:hash hash
|
||||||
;; data will be stored here, can be referenced by the memoized closure
|
;; data will be stored here, can be referenced by the memoized closure
|
||||||
|
|
@ -96,8 +90,8 @@
|
||||||
;; then no matter what, return the best information we have so far
|
;; then no matter what, return the best information we have so far
|
||||||
this-data))
|
this-data))
|
||||||
|
|
||||||
(struct user-cookies^ (theme notices) #:prefab)
|
(struct user-cookies^ (theme) #:prefab)
|
||||||
(define user-cookies-default (user-cookies^ 'default '()))
|
(define user-cookies-default (user-cookies^ 'default))
|
||||||
(define (user-cookies-getter req)
|
(define (user-cookies-getter req)
|
||||||
(define cookie-header (headers-assq* #"cookie" (request-headers/raw req)))
|
(define cookie-header (headers-assq* #"cookie" (request-headers/raw req)))
|
||||||
(define cookies-alist (if cookie-header (cookie-header->alist (header-value cookie-header) bytes->string/utf-8) null))
|
(define cookies-alist (if cookie-header (cookie-header->alist (header-value cookie-header) bytes->string/utf-8) null))
|
||||||
|
|
@ -106,29 +100,16 @@
|
||||||
(match pair
|
(match pair
|
||||||
[(cons "theme" (and theme (or "light" "dark" "default")))
|
[(cons "theme" (and theme (or "light" "dark" "default")))
|
||||||
(values 'theme (string->symbol theme))]
|
(values 'theme (string->symbol theme))]
|
||||||
[(cons "notices" notices)
|
|
||||||
(values 'notices (string-split notices "|"))]
|
|
||||||
[_ (values #f #f)])))
|
[_ (values #f #f)])))
|
||||||
(user-cookies^
|
(user-cookies^
|
||||||
(hash-ref cookies-hash 'theme (user-cookies^-theme user-cookies-default))
|
(hash-ref cookies-hash 'theme (user-cookies^-theme user-cookies-default))))
|
||||||
(hash-ref cookies-hash 'notices (user-cookies^-notices user-cookies-default))))
|
|
||||||
|
|
||||||
(define (user-cookies-setter user-cookies)
|
(define (user-cookies-setter user-cookies)
|
||||||
(map (λ (c) (header #"Set-Cookie" (cookie->set-cookie-header c)))
|
(map (λ (c) (header #"Set-Cookie" (cookie->set-cookie-header c)))
|
||||||
(list (make-cookie "theme" (symbol->string (user-cookies^-theme user-cookies))
|
(list (make-cookie "theme" (symbol->string (user-cookies^-theme user-cookies))
|
||||||
#:path "/"
|
|
||||||
#:max-age (* 60 60 24 365 10))
|
|
||||||
(make-cookie "notices" (string-join (user-cookies^-notices user-cookies) "|")
|
|
||||||
|
|
||||||
#:path "/"
|
#:path "/"
|
||||||
#:max-age (* 60 60 24 365 10)))))
|
#:max-age (* 60 60 24 365 10)))))
|
||||||
|
|
||||||
(define (user-cookies-setter-url req new-settings)
|
(define (user-cookies-setter-url req new-settings)
|
||||||
(format "/set-user-settings?~a" (params->query `(("next_location" . ,(url->string (request-uri req)))
|
(format "/set-user-settings?~a" (params->query `(("next_location" . ,(url->string (request-uri req)))
|
||||||
("new_settings" . ,(format "~s" new-settings))))))
|
("new_settings" . ,(format "~a" new-settings))))))
|
||||||
|
|
||||||
(define (user-cookies-setter-url/add-notice req user-cookies notice-name)
|
|
||||||
(user-cookies-setter-url
|
|
||||||
req
|
|
||||||
(struct-copy user-cookies^ user-cookies
|
|
||||||
[notices (cons notice-name (user-cookies^-notices user-cookies))])))
|
|
||||||
|
|
|
||||||
|
|
@ -33,49 +33,26 @@
|
||||||
; don't forget that I'm returning *code* - return a call to the function
|
; don't forget that I'm returning *code* - return a call to the function
|
||||||
(datum->syntax stx `(make-dispatcher-tree ,ds)))
|
(datum->syntax stx `(make-dispatcher-tree ,ds)))
|
||||||
|
|
||||||
; guard that the page returned a response, otherwise print more detailed debugging information
|
|
||||||
(define-syntax-rule (page ds name)
|
|
||||||
(λ (req)
|
|
||||||
(define dispatcher (hash-ref ds (quote name)))
|
|
||||||
(define page-response (dispatcher req))
|
|
||||||
(if (response? page-response)
|
|
||||||
page-response
|
|
||||||
(response/output
|
|
||||||
#:code 500
|
|
||||||
#:mime-type #"text/plain"
|
|
||||||
(λ (out)
|
|
||||||
(for ([port (list (current-error-port) out)])
|
|
||||||
(parameterize ([current-output-port port])
|
|
||||||
(printf "error in ~a:~n expected page to return a response~n actually returned: ~v~n"
|
|
||||||
(quote name)
|
|
||||||
page-response))))))))
|
|
||||||
|
|
||||||
(define (make-dispatcher-tree ds)
|
(define (make-dispatcher-tree ds)
|
||||||
(define subdomain-dispatcher (hash-ref ds 'subdomain-dispatcher))
|
(define subdomain-dispatcher (hash-ref ds 'subdomain-dispatcher))
|
||||||
(define tree
|
(define tree
|
||||||
(sequencer:make
|
(sequencer:make
|
||||||
subdomain-dispatcher
|
subdomain-dispatcher
|
||||||
(pathprocedure:make "/buddyfight/wiki/It_Doesn't_Work!!" (page ds page-it-works))
|
(pathprocedure:make "/" (hash-ref ds 'page-home))
|
||||||
(pathprocedure:make "/" (page ds page-home))
|
(pathprocedure:make "/proxy" (hash-ref ds 'page-proxy))
|
||||||
(filter:make #rx"^/static/" (hash-ref ds 'static-dispatcher))
|
(pathprocedure:make "/search" (hash-ref ds 'page-global-search))
|
||||||
(filter:make (pregexp "^/captcha/img/[0-9]+/[0-9]+$") (lift:make (page ds page-captcha-image)))
|
(pathprocedure:make "/set-user-settings" (hash-ref ds 'page-set-user-settings))
|
||||||
(filter:make (pregexp "^/captcha/verify/[0-9]+/[0-9]+/[0-9]+/[0-9]+$") (lift:make (page ds page-captcha-verify)))
|
(pathprocedure:make "/buddyfight/wiki/It_Doesn't_Work!!" (hash-ref ds 'page-it-works))
|
||||||
(if (config-true? 'captcha::enabled)
|
(filter:make (pregexp (format "^/~a/wiki/Category:.+$" px-wikiname)) (lift:make (hash-ref ds 'page-category)))
|
||||||
(lift:make (page ds page-captcha))
|
(filter:make (pregexp (format "^/~a/wiki/File:.+$" px-wikiname)) (lift:make (hash-ref ds 'page-file)))
|
||||||
(λ (_conn _req) (next-dispatcher)))
|
|
||||||
(pathprocedure:make "/proxy" (page ds page-proxy))
|
|
||||||
(pathprocedure:make "/search" (page ds page-global-search))
|
|
||||||
(pathprocedure:make "/set-user-settings" (page ds page-set-user-settings))
|
|
||||||
(filter:make (pregexp (format "^/~a/wiki/Category:.+$" px-wikiname)) (lift:make (page ds page-category)))
|
|
||||||
(filter:make (pregexp (format "^/~a/wiki/File:.+$" px-wikiname)) (lift:make (page ds page-file)))
|
|
||||||
(if (config-true? 'feature_offline::enabled)
|
(if (config-true? 'feature_offline::enabled)
|
||||||
(filter:make (pregexp (format "^/~a/wiki/.+$" px-wikiname)) (lift:make (page ds page-wiki-offline)))
|
(filter:make (pregexp (format "^/~a/wiki/.+$" px-wikiname)) (lift:make (hash-ref ds 'page-wiki-offline)))
|
||||||
(λ (_conn _req) (next-dispatcher)))
|
(λ (_conn _req) (next-dispatcher)))
|
||||||
(filter:make (pregexp (format "^/~a/wiki/.+$" px-wikiname)) (lift:make (page ds page-wiki)))
|
(filter:make (pregexp (format "^/~a/wiki/.+$" px-wikiname)) (lift:make (hash-ref ds 'page-wiki)))
|
||||||
(filter:make (pregexp (format "^/~a/search$" px-wikiname)) (lift:make (page ds page-search)))
|
(filter:make (pregexp (format "^/~a/search$" px-wikiname)) (lift:make (hash-ref ds 'page-search)))
|
||||||
(filter:make (pregexp (format "^/~a(/(wiki(/)?)?)?$" px-wikiname)) (lift:make (page ds redirect-wiki-home)))
|
(filter:make (pregexp (format "^/~a(/(wiki(/)?)?)?$" px-wikiname)) (lift:make (hash-ref ds 'redirect-wiki-home)))
|
||||||
(if (config-true? 'feature_offline::enabled)
|
(if (config-true? 'feature_offline::enabled)
|
||||||
(filter:make (pregexp (format "^/archive/~a/(styles|images)/.+$" px-wikiname)) (lift:make (page ds page-static-archive)))
|
(filter:make (pregexp (format "^/archive/~a/(styles|images)/.+$" px-wikiname)) (lift:make (hash-ref ds 'page-static-archive)))
|
||||||
(λ (_conn _req) (next-dispatcher)))
|
(λ (_conn _req) (next-dispatcher)))
|
||||||
(hash-ref ds 'static-dispatcher)
|
(hash-ref ds 'static-dispatcher)
|
||||||
(lift:make (hash-ref ds 'page-not-found))))
|
(lift:make (hash-ref ds 'page-not-found))))
|
||||||
|
|
|
||||||
|
|
@ -1,248 +0,0 @@
|
||||||
#lang racket/base
|
|
||||||
(require racket/format
|
|
||||||
racket/match
|
|
||||||
racket/string
|
|
||||||
; libs
|
|
||||||
(prefix-in easy: net/http-easy)
|
|
||||||
json
|
|
||||||
; html libs
|
|
||||||
html-writing
|
|
||||||
; web server libs
|
|
||||||
web-server/http
|
|
||||||
web-server/dispatchers/dispatch
|
|
||||||
; my libs
|
|
||||||
"application-globals.rkt"
|
|
||||||
"config.rkt"
|
|
||||||
"data.rkt"
|
|
||||||
"fandom-request.rkt"
|
|
||||||
"static-data.rkt"
|
|
||||||
"../lib/archive-file-mappings.rkt"
|
|
||||||
"../lib/thread-utils.rkt"
|
|
||||||
"../lib/url-utils.rkt"
|
|
||||||
"../lib/xexpr-utils.rkt")
|
|
||||||
(require (for-syntax racket/base syntax/parse))
|
|
||||||
|
|
||||||
(provide
|
|
||||||
define-endpoint
|
|
||||||
define-standard-handler
|
|
||||||
define-post-data-handler
|
|
||||||
define-jsonp-handler
|
|
||||||
make-switch-handler
|
|
||||||
(all-from-out racket/format
|
|
||||||
racket/match
|
|
||||||
json
|
|
||||||
net/http-easy
|
|
||||||
web-server/dispatchers/dispatch
|
|
||||||
"../lib/thread-utils.rkt"
|
|
||||||
"../lib/archive-file-mappings.rkt"
|
|
||||||
"config.rkt"
|
|
||||||
"data.rkt"
|
|
||||||
"fandom-request.rkt"
|
|
||||||
"static-data.rkt"))
|
|
||||||
|
|
||||||
|
|
||||||
(define-for-syntax (fix-scopes here orig stx)
|
|
||||||
(define remover (make-syntax-delta-introducer here #f))
|
|
||||||
(define introducer (make-syntax-delta-introducer orig #f))
|
|
||||||
(introducer (remover stx 'remove)))
|
|
||||||
|
|
||||||
|
|
||||||
(define-syntax (define-endpoint stx)
|
|
||||||
(syntax-parse stx
|
|
||||||
[(_ name
|
|
||||||
(~and (~seq all ...)
|
|
||||||
(~seq
|
|
||||||
((~datum variables) variable ...)
|
|
||||||
((~datum endpoints) endpoint ...)
|
|
||||||
((~datum render) render ...))))
|
|
||||||
#'(define-syntax name #'(all ...))]))
|
|
||||||
|
|
||||||
|
|
||||||
(define ((make-switch-handler #:standard standard #:jsonp jsonp #:post post) req)
|
|
||||||
(cond
|
|
||||||
[(equal? (request-method req) #"POST")
|
|
||||||
(post req)]
|
|
||||||
[(config-true? 'feature_jsonp::enabled)
|
|
||||||
(jsonp req)]
|
|
||||||
[else
|
|
||||||
(standard req)]))
|
|
||||||
|
|
||||||
|
|
||||||
(define-syntax (define-standard-handler stx)
|
|
||||||
(syntax-parse stx
|
|
||||||
[(_ (~and name-args (name:id arg:id ...)) endpoint-ref:expr fn-body ...)
|
|
||||||
#:with endpoint-syntax (syntax-local-value (cadr (syntax->list #'endpoint-ref)))
|
|
||||||
(syntax-parse #'endpoint-syntax
|
|
||||||
#:context 'endpoint-data
|
|
||||||
[(((~datum variables) variable ...)
|
|
||||||
((~datum endpoints) (endpoint-id:id (endpoint-param:expr ...)) ...)
|
|
||||||
((~datum render) render ...))
|
|
||||||
#:with endpoint-getters (for/list ([id (syntax-e #'(endpoint-id ...))]
|
|
||||||
[params (syntax-e #'((endpoint-param ...) ...))])
|
|
||||||
(with-syntax ([params (append (syntax-e params) #;'(("callback" . "proxy.wikipage")))])
|
|
||||||
(if (eq? (syntax-e id) 'siteinfo)
|
|
||||||
#'(λ ()
|
|
||||||
(siteinfo-fetch wikiname))
|
|
||||||
#'(λ ()
|
|
||||||
(fandom-get-api
|
|
||||||
wikiname
|
|
||||||
`params
|
|
||||||
#:headers `#hasheq((cookie . ,(format "theme=~a" (user-cookies^-theme user-cookies)))))))))
|
|
||||||
#:with endpoint-intermediates->values (for/list ([id (syntax-e #'(endpoint-id ...))])
|
|
||||||
(with-syntax ([id id])
|
|
||||||
(if (eq? (syntax-e #'id) 'siteinfo)
|
|
||||||
#'id
|
|
||||||
#'(easy:response-json id))))
|
|
||||||
|
|
||||||
(fix-scopes
|
|
||||||
#'here stx
|
|
||||||
#'(define name-args
|
|
||||||
(response-handler
|
|
||||||
(let/cc k
|
|
||||||
variable ...
|
|
||||||
(define user-cookies (user-cookies-getter req))
|
|
||||||
(define-values (endpoint-id ...)
|
|
||||||
(let-values ([(endpoint-id ...) (thread-values (~@ . endpoint-getters))])
|
|
||||||
(for ([response (list endpoint-id ...)]
|
|
||||||
#:when (easy:response? response))
|
|
||||||
(match (easy:response-status-code response)
|
|
||||||
[404
|
|
||||||
(next-dispatcher)]
|
|
||||||
[(or 403 406)
|
|
||||||
(define body
|
|
||||||
(generate-wiki-page
|
|
||||||
`(div
|
|
||||||
(p "Sorry! Fandom isn't allowing BreezeWiki to show pages right now.")
|
|
||||||
(p "We'll automatically try again in 30 seconds, so please stay on this page and be patient.")
|
|
||||||
(p (small "In a hurry? " (a (@ (href ,source-url)) "Click here to read the page on Fandom."))))
|
|
||||||
#:req req
|
|
||||||
#:source-url source-url
|
|
||||||
#:wikiname wikiname
|
|
||||||
#:title title
|
|
||||||
#:siteinfo siteinfo))
|
|
||||||
(k (response/output
|
|
||||||
#:code 503
|
|
||||||
#:headers (build-headers
|
|
||||||
always-headers
|
|
||||||
(header #"Retry-After" #"30")
|
|
||||||
(header #"Cache-Control" #"max-age=30, public")
|
|
||||||
(header #"Refresh" #"35"))
|
|
||||||
(λ (out)
|
|
||||||
(write-html body out))))]
|
|
||||||
[(not 200)
|
|
||||||
(k (error 'page-wiki "Tried to load page ~a/~a~nSadly, the page didn't load because Fandom returned status code ~a with response:~n~a"
|
|
||||||
wikiname
|
|
||||||
path
|
|
||||||
(easy:response-status-code response)
|
|
||||||
(easy:response-body response)))]
|
|
||||||
[_ (void)]))
|
|
||||||
(values (~@ . endpoint-intermediates->values))))
|
|
||||||
fn-body ...
|
|
||||||
render ...))))])]))
|
|
||||||
|
|
||||||
|
|
||||||
(define-syntax (define-post-data-handler stx)
|
|
||||||
(syntax-parse stx
|
|
||||||
[(_ (~and name-args (name:id arg:id ...)) endpoint-ref:expr fn-body ...)
|
|
||||||
#:with endpoint-syntax (syntax-local-value (cadr (syntax->list #'endpoint-ref)))
|
|
||||||
(syntax-parse #'endpoint-syntax
|
|
||||||
#:context 'endpoint-data
|
|
||||||
[(((~datum variables) variable ...)
|
|
||||||
((~datum endpoints) (endpoint-id:id (endpoint-param:expr ...)) ...)
|
|
||||||
((~datum render) render ...))
|
|
||||||
#:with endpoint-getters (for/list ([id (syntax-e #'(endpoint-id ...))]
|
|
||||||
[params (syntax-e #'((endpoint-param ...) ...))])
|
|
||||||
(with-syntax ([id id])
|
|
||||||
(if (eq? (syntax-e #'id) 'siteinfo)
|
|
||||||
#'(data->siteinfo (hash-ref post-data 'id))
|
|
||||||
#'(hash-ref post-data 'id))))
|
|
||||||
|
|
||||||
(fix-scopes
|
|
||||||
#'here stx
|
|
||||||
#'(define name-args
|
|
||||||
(response-handler
|
|
||||||
(let/cc k
|
|
||||||
(define (k-validation-error message)
|
|
||||||
(k (response/jsexpr
|
|
||||||
#:code 400
|
|
||||||
#:headers always-headers
|
|
||||||
`#hasheq((error .
|
|
||||||
#hasheq((code . "breezewiki")
|
|
||||||
(info . ,message)))))))
|
|
||||||
(define post-data/bytes (request-post-data/raw req))
|
|
||||||
(when (not post-data/bytes)
|
|
||||||
(k-validation-error "POST requests only, please."))
|
|
||||||
|
|
||||||
(define origin-header
|
|
||||||
(or (headers-assq* #"origin" (request-headers/raw req))
|
|
||||||
(headers-assq* #"referer" (request-headers/raw req))))
|
|
||||||
(when (or (not origin-header) (not (string-prefix? (bytes->string/latin-1 (header-value origin-header)) (config-get 'canonical_origin))))
|
|
||||||
(k-validation-error "Origin/Referer header failed validation - cross-origin requests are not allowed here"))
|
|
||||||
|
|
||||||
(define post-data/string (bytes->string/utf-8 post-data/bytes))
|
|
||||||
(define post-data (string->jsexpr post-data/string))
|
|
||||||
(define-values (endpoint-id ...) (values (~@ . endpoint-getters)))
|
|
||||||
(define wikiname (hash-ref post-data 'wikiname))
|
|
||||||
(define path (hash-ref post-data 'path))
|
|
||||||
(define source-url (format "https://~a.fandom.com/wiki/~a" wikiname path))
|
|
||||||
fn-body ...
|
|
||||||
render ...))))])]))
|
|
||||||
|
|
||||||
|
|
||||||
(define-syntax (define-jsonp-handler stx)
|
|
||||||
(syntax-parse stx
|
|
||||||
[(_ (~and name-args (name:id arg:id ...)) endpoint-ref:expr fn-body ...)
|
|
||||||
#:with endpoint-syntax (syntax-local-value (cadr (syntax->list #'endpoint-ref)))
|
|
||||||
(syntax-parse #'endpoint-syntax
|
|
||||||
#:context 'endpoint-data
|
|
||||||
[(((~datum variables) variable ...)
|
|
||||||
((~datum endpoints) (endpoint-id:id (endpoint-param:expr ...)) ...)
|
|
||||||
((~datum render) render ...))
|
|
||||||
#:with endpoint-scripts (for/list ([id (syntax-e #'(endpoint-id ...))]
|
|
||||||
[params (syntax-e #'((endpoint-param ...) ...))])
|
|
||||||
(with-syntax ([id id]
|
|
||||||
[params (append (syntax-e params)
|
|
||||||
`(("callback" . ,(format "proxy.~a" (syntax-e id)))))])
|
|
||||||
#'(script (@ (async)
|
|
||||||
(src ,(format "https://~a.fandom.com/api.php?~a"
|
|
||||||
wikiname
|
|
||||||
(params->query `params)))
|
|
||||||
(data-jsonp-var ,(format "~a" 'id))
|
|
||||||
(onerror ,(format "proxy.~a({error: {code: 'disconnected', info: 'Fandom connection failed or was blocked by your browser. Check any browser extensions that may block third-party scripts, then reload the page.'}})" 'id))))))
|
|
||||||
|
|
||||||
(fix-scopes
|
|
||||||
#'here stx
|
|
||||||
#'(define name-args
|
|
||||||
(response-handler
|
|
||||||
(let/cc k
|
|
||||||
variable ...
|
|
||||||
fn-body ...
|
|
||||||
|
|
||||||
(define body
|
|
||||||
(generate-wiki-page
|
|
||||||
`(div
|
|
||||||
(noscript "You have to enable JavaScript to load wiki pages. Sorry!")
|
|
||||||
(div (@ (id "loading")))
|
|
||||||
(div (@ (id "progress-bar") (style "margin-bottom: 50vh"))
|
|
||||||
(progress))
|
|
||||||
(script #<<END
|
|
||||||
var jsonpData = {}
|
|
||||||
var proxy = new Proxy(jsonpData, {get(obj, prop) { return value => obj[prop] = value }})
|
|
||||||
END
|
|
||||||
)
|
|
||||||
endpoint-scripts
|
|
||||||
(script (@ (type "module") (src ,(get-static-url "jsonp.js")))))
|
|
||||||
#:req req
|
|
||||||
#:source-url source-url
|
|
||||||
#:wikiname wikiname
|
|
||||||
#:title title
|
|
||||||
#:siteinfo siteinfo-default
|
|
||||||
#:path path
|
|
||||||
#:jsonp #t))
|
|
||||||
(when (config-true? 'debug)
|
|
||||||
(xexp->html body))
|
|
||||||
(response/output
|
|
||||||
#:code 200
|
|
||||||
#:headers always-headers
|
|
||||||
(λ (out)
|
|
||||||
(write-html body out)))))))])]))
|
|
||||||
|
|
@ -27,13 +27,6 @@
|
||||||
(λ (props)
|
(λ (props)
|
||||||
`(p "The Square Enix Indpendent Wiki Alliance, or SEIWA, is a network of independent wikis established in 2011 and focused on providing high-quality coverage of Square Enix and its content. We work together, along with our affiliates and others, to co-operate and support one another while providing the best-quality content on the various Square Enix video games and media.")))
|
`(p "The Square Enix Indpendent Wiki Alliance, or SEIWA, is a network of independent wikis established in 2011 and focused on providing high-quality coverage of Square Enix and its content. We work together, along with our affiliates and others, to co-operate and support one another while providing the best-quality content on the various Square Enix video games and media.")))
|
||||||
|
|
||||||
'GWN
|
|
||||||
(extwiki-group^
|
|
||||||
"GWN"
|
|
||||||
'(("Gaming Wiki Network" . "https://gamingwikinetwork.org/"))
|
|
||||||
(λ (props)
|
|
||||||
`(p "This wiki is part of the Gaming Wiki Network, a network of independently-hosted wikis about video game franchises. The GWN was founded on October 21, 2022. It aims to support all gaming communities in building independently-hosted wikis.")))
|
|
||||||
|
|
||||||
'Terraria
|
'Terraria
|
||||||
(extwiki-group^
|
(extwiki-group^
|
||||||
"Terraria"
|
"Terraria"
|
||||||
|
|
@ -78,35 +71,11 @@
|
||||||
(λ (props)
|
(λ (props)
|
||||||
'(p "The wiki was founded by Citricsquid on July 16th, 2009 as a way to document information from Minecraft. Since November 15th, 2010, it has been hosted by Curse Media. On December 12th, 2018, it moved to Fandom as it purchased Curse Media. Since September 24, 2023, it forked from Fandom and has been hosted by Weird Gloop.")))
|
'(p "The wiki was founded by Citricsquid on July 16th, 2009 as a way to document information from Minecraft. Since November 15th, 2010, it has been hosted by Curse Media. On December 12th, 2018, it moved to Fandom as it purchased Curse Media. Since September 24, 2023, it forked from Fandom and has been hosted by Weird Gloop.")))
|
||||||
|
|
||||||
'Tardis
|
|
||||||
(extwiki-group^
|
|
||||||
"Tardis"
|
|
||||||
'(("Forking announcement" . "https://tardis.wiki/wiki/Tardis:Forking_announcement")
|
|
||||||
("Discussion on Reddit" . "https://old.reddit.com/r/doctorwho/comments/1azxmrl/tardis_wiki_has_regenerated/"))
|
|
||||||
(λ (props) '()))
|
|
||||||
|
|
||||||
'Rainverse
|
|
||||||
(extwiki-group^
|
|
||||||
"Rainverse"
|
|
||||||
'(("Forking announcement" . "https://transfem.social/notes/9qsqdkmqi78e01bh"))
|
|
||||||
(λ (props)
|
|
||||||
'()))
|
|
||||||
|
|
||||||
'Undertale
|
|
||||||
(extwiki-group^
|
|
||||||
"Undertale"
|
|
||||||
'(("Reasons and community vote" . "https://undertale.fandom.com/f/p/4400000000000078624")
|
|
||||||
("Moving announcement" . "https://undertale.fandom.com/f/p/4400000000000078715")
|
|
||||||
("About the new wiki" . "https://undertale.wiki/w/Undertale_Wiki:About"))
|
|
||||||
(λ (props)
|
|
||||||
'((p "Following a community vote on March 26, 2025, most of the wiki's editors have moved to a new host. As a result, this wiki's content may be outdated or incorrect.")
|
|
||||||
(p "The Undertale & Deltarune Wikis voted to move off their previous host, Fandom. Their reasons included bad user experience, lack of editorial freedom, and disagreement with the sporadic AI Quick Answers that were visible to logged off readers on August 19, 2023. Both wikis moved, and are now independently hosted by the wikis' staff."))))
|
|
||||||
|
|
||||||
'empty
|
'empty
|
||||||
(extwiki-group^
|
(extwiki-group^
|
||||||
"Misc"
|
"Misc"
|
||||||
'(("This wiki doesn't have a description yet. Add one?" . "https://docs.breezewiki.com/Reporting_Bugs.html"))
|
'(("This wiki doesn't have a description yet. Add one?" . "https://docs.breezewiki.com/Reporting_Bugs.html"))
|
||||||
(λ (props) '()))))
|
#f)))
|
||||||
|
|
||||||
;; wikiname, niwa-name, url, logo-url
|
;; wikiname, niwa-name, url, logo-url
|
||||||
(struct extwiki^ (wikinames banner group name home logo description) #:transparent)
|
(struct extwiki^ (wikinames banner group name home logo description) #:transparent)
|
||||||
|
|
@ -340,11 +309,11 @@
|
||||||
(extwiki^
|
(extwiki^
|
||||||
'("zelda" "zelda-archive") 'default
|
'("zelda" "zelda-archive") 'default
|
||||||
'NIWA
|
'NIWA
|
||||||
"Zelda Wiki"
|
"Zeldapedia"
|
||||||
"https://zeldawiki.wiki/wiki/Main_Page"
|
"https://zeldapedia.wiki/wiki/Main_Page"
|
||||||
"https://niwanetwork.org/images/logos/zeldapedia.png"
|
"https://niwanetwork.org/images/logos/zeldapedia.png"
|
||||||
(λ (props)
|
(λ (props)
|
||||||
`((p "Founded on April 23, 2005, Zelda Wiki is your definitive source for encyclopedic information on The Legend of Zelda series, as well as all of the latest Zelda news. Zelda Wiki went independent from Fandom in October 2022, citing Fandom's recent buyouts and staffing decisions among their reasons."))))
|
`((p "Founded on April 23, 2005 as Zelda Wiki, today's Zeldapedia is your definitive source for encyclopedic information on The Legend of Zelda series, as well as all of the latest Zelda news. Zeldapedia went independent from Fandom in October 2022, citing Fandom's recent buyouts and staffing decisions among their reasons."))))
|
||||||
|
|
||||||
(extwiki^
|
(extwiki^
|
||||||
'("chrono") 'default
|
'("chrono") 'default
|
||||||
|
|
@ -450,117 +419,6 @@
|
||||||
(λ (props)
|
(λ (props)
|
||||||
`()))
|
`()))
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("tardis") 'default
|
|
||||||
'Tardis
|
|
||||||
"TARDIS Wiki"
|
|
||||||
"https://tardis.wiki/wiki/Doctor_Who_Wiki"
|
|
||||||
"https://tardis.wiki/w/images/Tardis_Images/e/e6/Site-logo.png"
|
|
||||||
(λ (props)
|
|
||||||
`()))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("wizardry") 'default
|
|
||||||
'GWN
|
|
||||||
"Wizardry Wiki"
|
|
||||||
"https://wizardry.wiki.gg/wiki/Wizardry_Wiki"
|
|
||||||
"https://wizardry.wiki.gg/images/e/e6/Site-logo.png"
|
|
||||||
(λ (props)
|
|
||||||
`((p "On March 21, 2023, the wiki has decided to leave and abandoning from Fandom due to numerous of issues such as intrusive advertising, long-lasting bugs, restrictions on customization, etcetera. Wizardry Wiki was officially inducted into the wiki.gg wikifarm, with all contents forked over.")
|
|
||||||
(p "The wiki has partnered with " (a (@ (href "https://fallout.wiki/")) "Independent Fallout Wiki") " as of June 14, 2024."))))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("jackryan") 'default
|
|
||||||
'GWN
|
|
||||||
"Tom Clancy Wiki"
|
|
||||||
"https://tomclancy.wiki.gg/wiki/Tom_Clancy_Wiki"
|
|
||||||
"https://tomclancy.wiki.gg/images/thumb/c/c5/Jack_Ryan_Logo_Dark.png/600px-Jack_Ryan_Logo_Dark.png"
|
|
||||||
(λ (props)
|
|
||||||
`((p "The Tom Clancy Wiki is a collaborative encyclopedia dedicated to Tom Clancy’s franchises. The Tom Clancy franchise is a 40-year old expansive franchise founded by Tom Clancy, telling several unique sagas through books, video games, and films, as well as a TV show."))))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("hollowknight") 'default
|
|
||||||
'GWN
|
|
||||||
"Hollow Knight Wiki"
|
|
||||||
"https://hollowknight.wiki/wiki/Main_Page"
|
|
||||||
"https://gamingwikinetwork.org/images/logos/hollowknight.png"
|
|
||||||
(λ (props)
|
|
||||||
`((p "We are an independently hosted wiki for the games Hollow Knight and Hollow Knight: Silksong, created by fans, for fans. The wiki is a fork of the FANDOM Hollow Knight Wiki and was officially unveiled on October 31, 2023."))))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("hellokitty" "sanrio") 'default
|
|
||||||
'GWN
|
|
||||||
"Sanrio Wiki"
|
|
||||||
"https://sanriowiki.com/wiki/Sanrio_Wiki"
|
|
||||||
"https://cdn.sanriowiki.com/wiki.png"
|
|
||||||
(λ (props)
|
|
||||||
`((p "Sanrio Wiki is a project that was started on April 14, 2015 by EvieMelody. It was hosted on the wiki-farm ShoutWiki and has since become independent."))))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("sto") 'default
|
|
||||||
'GWN
|
|
||||||
"Star Trek Online Wiki"
|
|
||||||
"https://stowiki.net/wiki/Main_Page"
|
|
||||||
"https://gamingwikinetwork.org/images/logos/stowiki.png"
|
|
||||||
(λ (props)
|
|
||||||
`()))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("rayman-game" "ubisoftrayman") 'default
|
|
||||||
'GWN
|
|
||||||
"Rayman Wiki"
|
|
||||||
"https://raymanpc.com/wiki/en/Main_Page"
|
|
||||||
"https://raymanpc.com/wiki/script-en/resources/assets/logo-en.png?5c608"
|
|
||||||
(λ (props)
|
|
||||||
`()))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("granblue") 'empty
|
|
||||||
'empty
|
|
||||||
"Granblue Fantasy Wiki"
|
|
||||||
"https://gbf.wiki/"
|
|
||||||
"https://gbf.wiki/images/1/18/Vyrnball.png?0704c"
|
|
||||||
(λ (props)
|
|
||||||
`()))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("hellmet-roblox") 'empty
|
|
||||||
'empty
|
|
||||||
"HELLMET Wiki"
|
|
||||||
"https://hellmet.miraheze.org/wiki/Main_Page"
|
|
||||||
"https://static.miraheze.org/hellmetwiki/thumb/c/ce/Hellmet_Wiki_Logo.png/135px-Hellmet_Wiki_Logo.png"
|
|
||||||
(λ (props)
|
|
||||||
`()))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("rain-web-comic") 'default
|
|
||||||
'empty
|
|
||||||
"Rainverse Wiki"
|
|
||||||
"https://rainverse.wiki/wiki/Main_Page"
|
|
||||||
"https://static.miraheze.org/rainversewiki/2/2c/Rain_comic_cover.png"
|
|
||||||
(λ (props)
|
|
||||||
`((p "We have a newly-migrated Rainverse Wiki which escaped from Fandom! Rain is the comic that helped me figure out my gender, so I am really glad to have a wiki on a non-evil host.")
|
|
||||||
(p "Please stop using the abandoned copy of Rain Wiki on Fandom. Fandom is still \"training\" a generator which adds procedurally-generated bullshit to articles, with no way for users to remove or correct it, and they're demanding volunteer wiki admins waste time \"vetting\" the procedurally-generated BS for accuracy. As Jocelyn herself said, \"fuck Fandom forever.\"")
|
|
||||||
(p "If you are interested, please add more articles related to other Rainverse stories."))))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("undertale") 'default
|
|
||||||
'Undertale
|
|
||||||
"Undertale Wiki"
|
|
||||||
"https://undertale.wiki/w/Undertale_Wiki"
|
|
||||||
"https://static.wikia.nocookie.net/undertale/images/e/e6/Site-logo.png/revision/latest?cb=20220717174821"
|
|
||||||
(λ (props)
|
|
||||||
`()))
|
|
||||||
|
|
||||||
(extwiki^
|
|
||||||
'("deltarune") 'default
|
|
||||||
'Undertale
|
|
||||||
"Deltarune Wiki"
|
|
||||||
"https://deltarune.wiki/w/Deltarune_Wiki"
|
|
||||||
"https://deltarune.wiki/images/Deltarune_Wiki_logo.png?cb=cvvhwg&h=thumb.php&f=Deltarune_Wiki_logo.png"
|
|
||||||
(λ (props)
|
|
||||||
`()))
|
|
||||||
|
|
||||||
;; fandom wikinames * empty * empty * Name * Home Page
|
;; fandom wikinames * empty * empty * Name * Home Page
|
||||||
(extwiki^ '("aether") 'empty 'empty "Aether Wiki" "https://aether.wiki.gg/wiki/Aether_Wiki" #f #f)
|
(extwiki^ '("aether") 'empty 'empty "Aether Wiki" "https://aether.wiki.gg/wiki/Aether_Wiki" #f #f)
|
||||||
(extwiki^ '("before-darkness-falls") 'empty 'empty "Before Darkness Falls Wiki" "https://beforedarknessfalls.wiki.gg/wiki/Before_Darkness_Falls_Wiki" #f #f)
|
(extwiki^ '("before-darkness-falls") 'empty 'empty "Before Darkness Falls Wiki" "https://beforedarknessfalls.wiki.gg/wiki/Before_Darkness_Falls_Wiki" #f #f)
|
||||||
|
|
@ -576,7 +434,6 @@
|
||||||
(extwiki^ '("doom") 'empty 'empty "DoomWiki.org" "https://doomwiki.org/wiki/Entryway" #f #f)
|
(extwiki^ '("doom") 'empty 'empty "DoomWiki.org" "https://doomwiki.org/wiki/Entryway" #f #f)
|
||||||
(extwiki^ '("dreamscaper") 'empty 'empty "Official Dreamscaper Wiki" "https://dreamscaper.wiki.gg/wiki/Dreamscaper_Wiki" #f #f)
|
(extwiki^ '("dreamscaper") 'empty 'empty "Official Dreamscaper Wiki" "https://dreamscaper.wiki.gg/wiki/Dreamscaper_Wiki" #f #f)
|
||||||
(extwiki^ '("elderscrolls") 'empty 'empty "UESP" "https://en.uesp.net/wiki/Main_Page" #f #f)
|
(extwiki^ '("elderscrolls") 'empty 'empty "UESP" "https://en.uesp.net/wiki/Main_Page" #f #f)
|
||||||
(extwiki^ '("enterthegungeon" "exit-the-gungeon" "enter-the-gungeon-archive") 'empty 'empty "Official Enter The Gungeon Wiki" "https://enterthegungeon.wiki.gg/wiki/Enter_the_Gungeon_Wiki" "https://enterthegungeon.wiki.gg/images/e/e6/Site-logo.png" #f)
|
|
||||||
(extwiki^ '("fiend-folio") 'empty 'empty "Official Fiend Folio Wiki" "https://fiendfolio.wiki.gg/wiki/Fiend_Folio_Wiki" #f #f)
|
(extwiki^ '("fiend-folio") 'empty 'empty "Official Fiend Folio Wiki" "https://fiendfolio.wiki.gg/wiki/Fiend_Folio_Wiki" #f #f)
|
||||||
(extwiki^ '("foxhole") 'empty 'empty "Foxhole Wiki" "https://foxhole.wiki.gg/wiki/Foxhole_Wiki" #f #f)
|
(extwiki^ '("foxhole") 'empty 'empty "Foxhole Wiki" "https://foxhole.wiki.gg/wiki/Foxhole_Wiki" #f #f)
|
||||||
(extwiki^ '("have-a-nice-death") 'empty 'empty "Have a Nice Death Wiki" "https://haveanicedeath.wiki.gg/wiki/Have_a_Nice_Death_Wiki" #f #f)
|
(extwiki^ '("have-a-nice-death") 'empty 'empty "Have a Nice Death Wiki" "https://haveanicedeath.wiki.gg/wiki/Have_a_Nice_Death_Wiki" #f #f)
|
||||||
|
|
@ -597,7 +454,6 @@
|
||||||
(extwiki^ '("totherescue") 'empty 'empty "To The Rescue!" "https://totherescue.wiki.gg/wiki/To_The_Rescue%21_Wiki" #f #f)
|
(extwiki^ '("totherescue") 'empty 'empty "To The Rescue!" "https://totherescue.wiki.gg/wiki/To_The_Rescue%21_Wiki" #f #f)
|
||||||
(extwiki^ '("touhou") 'empty 'empty "Touhou Wiki" "https://en.touhouwiki.net/wiki/Touhou_Wiki" #f #f)
|
(extwiki^ '("touhou") 'empty 'empty "Touhou Wiki" "https://en.touhouwiki.net/wiki/Touhou_Wiki" #f #f)
|
||||||
(extwiki^ '("undermine") 'empty 'empty "Official UnderMine Wiki" "https://undermine.wiki.gg/wiki/UnderMine_Wiki" #f #f)
|
(extwiki^ '("undermine") 'empty 'empty "Official UnderMine Wiki" "https://undermine.wiki.gg/wiki/UnderMine_Wiki" #f #f)
|
||||||
(extwiki^ '("undertaleyellow") 'empty 'empty "Undertale Yellow Wiki" "https://undertaleyellow.wiki.gg/wiki/Undetale_Yellow_Wiki" #f #f)
|
|
||||||
(extwiki^ '("westofloathing" "loathing") 'empty 'empty "Wiki of Loathing" "https://loathing.wiki.gg/wiki/Wiki_of_Loathing" #f #f)
|
(extwiki^ '("westofloathing" "loathing") 'empty 'empty "Wiki of Loathing" "https://loathing.wiki.gg/wiki/Wiki_of_Loathing" #f #f)
|
||||||
(extwiki^ '("willyousnail") 'empty 'empty "Official Will You Snail Wiki" "https://willyousnail.wiki.gg/wiki/Will_You_Snail_Wiki" #f #f)
|
(extwiki^ '("willyousnail") 'empty 'empty "Official Will You Snail Wiki" "https://willyousnail.wiki.gg/wiki/Will_You_Snail_Wiki" #f #f)
|
||||||
(extwiki^ '("yumenikki" "yume-nikki-dream-diary") 'empty 'empty "Yume Wiki" "https://yume.wiki/Main_Page" #f #f)))
|
(extwiki^ '("yumenikki" "yume-nikki-dream-diary") 'empty 'empty "Yume Wiki" "https://yume.wiki/Main_Page" #f #f)))
|
||||||
|
|
|
||||||
|
|
@ -85,47 +85,46 @@
|
||||||
|
|
||||||
(define/memoize (get-redirect-content wikiname) #:hash hash
|
(define/memoize (get-redirect-content wikiname) #:hash hash
|
||||||
(define wiki (hash-ref wikis-hash wikiname #f))
|
(define wiki (hash-ref wikis-hash wikiname #f))
|
||||||
(with-handlers ([exn:fail:http-easy:timeout? (λ (e) #f)])
|
(cond
|
||||||
(cond
|
[wiki
|
||||||
[wiki
|
(define display-name (cadr 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 endpoint (string-append (get-api-endpoint wiki) "?action=parse&page=MediaWiki:BreezeWikiRedirect&prop=text&formatversion=2&format=json"))
|
(define res (get endpoint))
|
||||||
(define res (get endpoint))
|
(define html (jp "/parse/text" (response-json res)))
|
||||||
(define html (jp "/parse/text" (response-json res)))
|
(define content ((query-selector (λ (t a c) (has-class? "mw-parser-output" a))
|
||||||
(define content ((query-selector (λ (t a c) (has-class? "mw-parser-output" a))
|
(html->xexp html))))
|
||||||
(html->xexp html))))
|
(define body (for/list ([p (in-producer (query-selector (λ (t a c) (eq? t 'p)) content) #f)]) p))
|
||||||
(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 table (parse-table ((query-selector (λ (t a c) (eq? t 'table)) content))))
|
(define-values (links links-errors) (table->links table))
|
||||||
(define-values (links links-errors) (table->links table))
|
(define-values (logo logo-errors) (table->logo table))
|
||||||
(define-values (logo logo-errors) (table->logo table))
|
(define construct-errors (append links-errors logo-errors))
|
||||||
(define construct-errors (append links-errors logo-errors))
|
(λ (title)
|
||||||
(λ (title)
|
(define go
|
||||||
(define go
|
(string-append (get-search-page wiki)
|
||||||
(string-append (get-search-page wiki)
|
"?"
|
||||||
"?"
|
(params->query `(("search" . ,title)
|
||||||
(params->query `(("search" . ,title)
|
("go" . "Go")))))
|
||||||
("go" . "Go")))))
|
`(aside (@ (class "niwa__notice"))
|
||||||
`(aside (@ (class "niwa__notice"))
|
(h1 (@ (class "niwa__header")) ,display-name " has its own website separate from Fandom.")
|
||||||
(h1 (@ (class "niwa__header")) ,display-name " has its own website separate from Fandom.")
|
(div (@ (class "niwa__cols"))
|
||||||
(div (@ (class "niwa__cols"))
|
(div (@ (class "niwa__left"))
|
||||||
(div (@ (class "niwa__left"))
|
(a (@ (class "niwa__go") (href ,go)) "Read " ,title " on " ,display-name " →")
|
||||||
(a (@ (class "niwa__go") (href ,go)) "Read " ,title " on " ,display-name " →")
|
,@body
|
||||||
,@body
|
(p "This external wiki is a helpful alternative to Fandom. You should "
|
||||||
(p "This external wiki is a helpful alternative to Fandom. You should "
|
(a (@ (href ,go)) "check it out now!")))
|
||||||
(a (@ (href ,go)) "check it out now!")))
|
,(if logo
|
||||||
,(if logo
|
`(div (@ (class "niwa__right"))
|
||||||
`(div (@ (class "niwa__right"))
|
(img (@ (class "niwa__logo") (src ,logo))))
|
||||||
(img (@ (class "niwa__logo") (src ,logo))))
|
""))
|
||||||
""))
|
,(if (pair? links)
|
||||||
,(if (pair? links)
|
`(p (@ (class "niwa__feedback"))
|
||||||
`(p (@ (class "niwa__feedback"))
|
,@(add-between links " / "))
|
||||||
,@(add-between links " / "))
|
"")
|
||||||
"")
|
,(if (pair? construct-errors)
|
||||||
,(if (pair? construct-errors)
|
`(ul
|
||||||
`(ul
|
,@(for/list ([error construct-errors])
|
||||||
,@(for/list ([error construct-errors])
|
`(li ,error)))
|
||||||
`(li ,error)))
|
"")))]
|
||||||
"")))]
|
[#t #f]))
|
||||||
[#t #f])))
|
|
||||||
(module+ test
|
(module+ test
|
||||||
(check-not-false ((get-redirect-content "gallowmere") "MediEvil Wiki")))
|
(check-not-false ((get-redirect-content "gallowmere") "MediEvil Wiki")))
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
#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)))
|
|
||||||
|
|
@ -1,179 +0,0 @@
|
||||||
#lang racket/base
|
|
||||||
(require racket/class
|
|
||||||
racket/dict
|
|
||||||
racket/draw
|
|
||||||
pict
|
|
||||||
file/convertible
|
|
||||||
racket/format
|
|
||||||
racket/list
|
|
||||||
racket/math
|
|
||||||
racket/match
|
|
||||||
web-server/http
|
|
||||||
(only-in web-server/dispatchers/dispatch next-dispatcher)
|
|
||||||
net/url
|
|
||||||
(only-in net/cookies/server cookie->set-cookie-header cookie-header->alist)
|
|
||||||
html-writing
|
|
||||||
"application-globals.rkt"
|
|
||||||
"data.rkt"
|
|
||||||
"config.rkt"
|
|
||||||
"static-data.rkt"
|
|
||||||
"../lib/url-utils.rkt"
|
|
||||||
"../lib/xexpr-utils.rkt")
|
|
||||||
|
|
||||||
(provide
|
|
||||||
page-captcha
|
|
||||||
page-captcha-image
|
|
||||||
page-captcha-verify)
|
|
||||||
|
|
||||||
(define (get-ip req)
|
|
||||||
(define header
|
|
||||||
(if (config-true? 'captcha::ip_header)
|
|
||||||
(headers-assq* (string->bytes/utf-8 (config-get 'captcha::ip_header)) (request-headers/raw req))
|
|
||||||
#f))
|
|
||||||
(if header
|
|
||||||
(~a (header-value header))
|
|
||||||
(request-client-ip req)))
|
|
||||||
|
|
||||||
(define (get-rng req)
|
|
||||||
(parameterize ([current-pseudo-random-generator (make-pseudo-random-generator)])
|
|
||||||
(define ip-segments (regexp-match* "[0-9]+" (get-ip req)))
|
|
||||||
(define seed
|
|
||||||
(modulo
|
|
||||||
(for/sum ([i (in-naturals)]
|
|
||||||
[s ip-segments])
|
|
||||||
(* (add1 i) (add1 (string->number s))))
|
|
||||||
(expt 2 32)))
|
|
||||||
(random-seed seed)
|
|
||||||
(current-pseudo-random-generator)))
|
|
||||||
|
|
||||||
(define (get-key-solution req)
|
|
||||||
(parameterize ([current-pseudo-random-generator (get-rng req)])
|
|
||||||
(random 1 (add1 9))))
|
|
||||||
|
|
||||||
(define diameter 35)
|
|
||||||
(define font (make-object font% 12 'system))
|
|
||||||
(define msg "I'm not a robot!")
|
|
||||||
(define checkbox (filled-ellipse diameter diameter #:color "Red"))
|
|
||||||
(define assembly
|
|
||||||
(frame
|
|
||||||
(inset
|
|
||||||
(hc-append
|
|
||||||
8
|
|
||||||
checkbox
|
|
||||||
(text msg font))
|
|
||||||
8)))
|
|
||||||
(define-values (inner-x inner-y) (cc-find assembly checkbox))
|
|
||||||
(define-values (lt-x lt-y) (lt-find assembly checkbox))
|
|
||||||
(define-values (rb-x rb-y) (rb-find assembly checkbox))
|
|
||||||
|
|
||||||
(define (get-coordinate-solution req w h)
|
|
||||||
(parameterize ([current-pseudo-random-generator (get-rng req)])
|
|
||||||
(values (random (exact-truncate lt-x) (exact-truncate (- w rb-x)))
|
|
||||||
(random (exact-truncate lt-y) (exact-truncate (- h rb-y))))))
|
|
||||||
|
|
||||||
(define (page-captcha req)
|
|
||||||
(define cookie-header (headers-assq* #"cookie" (request-headers/raw req)))
|
|
||||||
(define cookies-alist (if cookie-header (cookie-header->alist (header-value cookie-header) bytes->string/utf-8) null))
|
|
||||||
(for ([pair cookies-alist])
|
|
||||||
(match pair
|
|
||||||
[(cons "captcha" method)
|
|
||||||
(when (config-true? 'captcha::log)
|
|
||||||
(printf "captcha skip - via ~a [~a] - ~v~n" method (get-ip req) (url->string (request-uri req))))
|
|
||||||
(next-dispatcher)]
|
|
||||||
[_ (void)]))
|
|
||||||
|
|
||||||
(response-handler
|
|
||||||
(define body
|
|
||||||
`(*TOP*
|
|
||||||
(*DECL* DOCTYPE html)
|
|
||||||
(html
|
|
||||||
(head
|
|
||||||
(meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))
|
|
||||||
(title "Checking you're not a bot...")
|
|
||||||
(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))))
|
|
||||||
(script (@ (defer) (src ,(get-static-url "captcha.js")))))
|
|
||||||
(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"))
|
|
||||||
"Checking you're not a bot..."))
|
|
||||||
(div (@ (id "content") #;(class "page-content"))
|
|
||||||
(div (@ (id "mw-content-text"))
|
|
||||||
(p "To confirm, please click directly in the circle, or hold down the " ,(~a (get-key-solution req)) " key on your keyboard.")
|
|
||||||
(noscript (p "JavaScript is required for the captcha. Sorry!"))
|
|
||||||
(div (@ (id "captcha-area")))))
|
|
||||||
,(application-footer #f))))))))
|
|
||||||
(when (config-true? 'debug)
|
|
||||||
(xexp->html body))
|
|
||||||
(response/output
|
|
||||||
#:code 200
|
|
||||||
#:headers always-headers
|
|
||||||
(λ (out)
|
|
||||||
(write-html body out)))))
|
|
||||||
|
|
||||||
(define (page-captcha-image req)
|
|
||||||
(response-handler
|
|
||||||
(define w (string->number (path/param-path (third (url-path (request-uri req))))))
|
|
||||||
(define h (string->number (path/param-path (fourth (url-path (request-uri req))))))
|
|
||||||
(define-values (at-x at-y) (get-coordinate-solution req w h))
|
|
||||||
(when (config-true? 'captcha::log)
|
|
||||||
(printf "captcha show - size ~a x ~a - solution ~a x ~a [~a]~n" w h at-x at-y (get-ip req)))
|
|
||||||
#;(printf "target: ~a x ~a~ncanvas: ~a x ~a~npict size: ~a-~a ~a-~a~n" at-x at-y x y lt-x rb-x lt-y rb-y)
|
|
||||||
(define dc (make-object bitmap-dc% #f))
|
|
||||||
(send dc set-font font)
|
|
||||||
(define bm (make-object bitmap% w h #f #f))
|
|
||||||
(send dc set-bitmap bm)
|
|
||||||
(draw-pict
|
|
||||||
assembly
|
|
||||||
dc
|
|
||||||
(- at-x inner-x)
|
|
||||||
(- at-y inner-y))
|
|
||||||
(define image (convert bm 'png-bytes))
|
|
||||||
(response/output
|
|
||||||
#:mime-type #"image/png"
|
|
||||||
#:headers (list (header #"Cache-Control" #"no-cache"))
|
|
||||||
(λ (out) (display image out)))))
|
|
||||||
|
|
||||||
(define (page-captcha-verify req)
|
|
||||||
(response-handler
|
|
||||||
(match-define (list w h x y) (for/list ([segment (drop (url-path (request-uri req)) 2)])
|
|
||||||
(string->number (path/param-path segment))))
|
|
||||||
#;(printf "solution: ~a x ~a~ncoordinate: ~a x ~a~ndist^2: ~a~n" solution-x solution-y x y dist)
|
|
||||||
(define headers
|
|
||||||
(build-headers
|
|
||||||
always-headers
|
|
||||||
(cond
|
|
||||||
[(and (= y 0) (= x (get-key-solution req)))
|
|
||||||
(when (config-true? 'captcha::log)
|
|
||||||
(printf "captcha pass - key ~a [~a]~n" x (get-ip req)))
|
|
||||||
|
|
||||||
(header #"Set-Cookie" (cookie->set-cookie-header (make-cookie "captcha" "key" #:path "/" #:max-age (* 60 60 24 365 10) #:http-only? #t #:secure? #t)))]
|
|
||||||
[(= y 0)
|
|
||||||
(when (config-true? 'captcha::log)
|
|
||||||
(printf "captcha fail - key ~a instead of ~a [~a]~n" x (get-key-solution req) (get-ip req)))]
|
|
||||||
[else (void)])
|
|
||||||
(when (> y 0)
|
|
||||||
(let-values ([(solution-x solution-y) (get-coordinate-solution req w h)])
|
|
||||||
(let ([dist (+ (expt (- x solution-x) 2) (expt (- y solution-y) 2))])
|
|
||||||
(cond
|
|
||||||
[(dist . < . (expt (/ diameter 2) 2))
|
|
||||||
(when (config-true? 'captcha::log)
|
|
||||||
(printf "captcha pass - coordinate, dist^2 ~a [~a]~n" dist (get-ip req)))
|
|
||||||
(header #"Set-Cookie" (cookie->set-cookie-header (make-cookie "captcha" "coordinate" #:path "/" #:max-age (* 60 60 24 365 10))))]
|
|
||||||
[else
|
|
||||||
(when (config-true? 'captcha::log)
|
|
||||||
(printf "captcha pass - coordinate, dist^2 ~a [~a]~n" dist (get-ip req)))]))))))
|
|
||||||
(match (dict-ref (url-query (request-uri req)) 'from #f)
|
|
||||||
[(? string? dest)
|
|
||||||
(response/output
|
|
||||||
#:code 302
|
|
||||||
#:mime-type #"text/plain"
|
|
||||||
#:headers (cons (header #"Location" (string->bytes/utf-8 dest)) headers)
|
|
||||||
(λ (out)
|
|
||||||
(displayln "Checking your answer..." out)))]
|
|
||||||
[#f (next-dispatcher)])))
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
#lang racket/base
|
#lang racket/base
|
||||||
(require racket/string
|
(require racket/dict
|
||||||
|
racket/list
|
||||||
|
racket/match
|
||||||
|
racket/string
|
||||||
(prefix-in easy: net/http-easy)
|
(prefix-in easy: net/http-easy)
|
||||||
; html libs
|
; html libs
|
||||||
html-parsing
|
html-parsing
|
||||||
|
|
@ -8,10 +11,15 @@
|
||||||
net/url
|
net/url
|
||||||
web-server/http
|
web-server/http
|
||||||
(only-in web-server/dispatchers/dispatch next-dispatcher)
|
(only-in web-server/dispatchers/dispatch next-dispatcher)
|
||||||
|
#;(only-in web-server/http/redirect redirect-to)
|
||||||
"application-globals.rkt"
|
"application-globals.rkt"
|
||||||
"endpoints.rkt"
|
"config.rkt"
|
||||||
"../lib/tree-updater.rkt"
|
"data.rkt"
|
||||||
|
"page-wiki.rkt"
|
||||||
|
"../lib/syntax.rkt"
|
||||||
|
"../lib/thread-utils.rkt"
|
||||||
"../lib/url-utils.rkt"
|
"../lib/url-utils.rkt"
|
||||||
|
"whole-utils.rkt"
|
||||||
"../lib/xexpr-utils.rkt")
|
"../lib/xexpr-utils.rkt")
|
||||||
|
|
||||||
(provide
|
(provide
|
||||||
|
|
@ -55,47 +63,53 @@
|
||||||
,title)))
|
,title)))
|
||||||
members))))))
|
members))))))
|
||||||
|
|
||||||
|
(define (page-category req)
|
||||||
|
(response-handler
|
||||||
|
(define wikiname (path/param-path (first (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))
|
||||||
|
|
||||||
(define-endpoint
|
(define-values (members-data page-data siteinfo)
|
||||||
category-endpoint
|
(thread-values
|
||||||
[variables
|
(λ ()
|
||||||
(define wikiname (path/param-path (car (url-path (request-uri req)))))
|
(define dest-url
|
||||||
(define prefixed-category (string-join (map path/param-path (cddr (url-path (request-uri req)))) "/"))
|
(format "~a/api.php?~a"
|
||||||
(define segments (map path/param-path (cdr (url-path (request-uri req)))))
|
origin
|
||||||
(define title (url-segments->guess-title segments))
|
(params->query `(("action" . "query")
|
||||||
(define path (string-join (cdr segments) "/"))
|
("list" . "categorymembers")
|
||||||
(define origin (format "https://~a.fandom.com" wikiname))
|
("cmtitle" . ,prefixed-category)
|
||||||
(define source-url (format "~a/wiki/~a" origin prefixed-category))]
|
("cmlimit" . "max")
|
||||||
[endpoints
|
("formatversion" . "2")
|
||||||
(membersdata
|
("format" . "json")))))
|
||||||
(("action" . "query")
|
(log-outgoing dest-url)
|
||||||
("list" . "categorymembers")
|
(define dest-res (easy:get dest-url #:timeouts timeouts))
|
||||||
("cmtitle" . ,prefixed-category)
|
(easy:response-json dest-res))
|
||||||
("cmlimit" . "max")
|
(λ ()
|
||||||
("formatversion" . "2")
|
(define dest-url
|
||||||
("format" . "json")))
|
(format "~a/api.php?~a"
|
||||||
(pagedata
|
origin
|
||||||
(("action" . "parse")
|
(params->query `(("action" . "parse")
|
||||||
("page" . ,prefixed-category)
|
("page" . ,prefixed-category)
|
||||||
("prop" . "text|headhtml|langlinks")
|
("prop" . "text|headhtml|langlinks")
|
||||||
("formatversion" . "2")
|
("formatversion" . "2")
|
||||||
("format" . "json")))
|
("format" . "json")))))
|
||||||
(siteinfo
|
(log-outgoing dest-url)
|
||||||
(("action" . "query")
|
(define dest-res (easy:get dest-url #:timeouts timeouts))
|
||||||
("meta" . "siteinfo")
|
(easy:response-json dest-res))
|
||||||
("siprop" . "general|rightsinfo")
|
(λ ()
|
||||||
("format" . "json")
|
(siteinfo-fetch wikiname))))
|
||||||
("formatversion" . "2")))]
|
|
||||||
[render
|
(define title (preprocess-html-wiki (jp "/parse/title" page-data prefixed-category)))
|
||||||
(define page-html (preprocess-html-wiki (jp "/parse/text" pagedata "")))
|
(define page-html (preprocess-html-wiki (jp "/parse/text" page-data "")))
|
||||||
(define page (html->xexp page-html))
|
(define page (html->xexp page-html))
|
||||||
(define head-data ((head-data-getter wikiname) pagedata))
|
(define head-data ((head-data-getter wikiname) page-data))
|
||||||
(define body (generate-results-page
|
(define body (generate-results-page
|
||||||
#:req req
|
#:req req
|
||||||
#:source-url source-url
|
#:source-url source-url
|
||||||
#:wikiname wikiname
|
#:wikiname wikiname
|
||||||
#:title (preprocess-html-wiki (jp "/parse/title" pagedata prefixed-category))
|
#:title title
|
||||||
#:members-data membersdata
|
#:members-data members-data
|
||||||
#:page page
|
#:page page
|
||||||
#:head-data head-data
|
#:head-data head-data
|
||||||
#:siteinfo siteinfo))
|
#:siteinfo siteinfo))
|
||||||
|
|
@ -108,22 +122,7 @@
|
||||||
#:code 200
|
#:code 200
|
||||||
#:headers (build-headers always-headers)
|
#:headers (build-headers always-headers)
|
||||||
(λ (out)
|
(λ (out)
|
||||||
(write-html body out)))])
|
(write-html body out)))))
|
||||||
|
|
||||||
(define-standard-handler (page-category-standard req)
|
|
||||||
#'category-endpoint)
|
|
||||||
|
|
||||||
(define-jsonp-handler (page-category-jsonp req)
|
|
||||||
#'category-endpoint)
|
|
||||||
|
|
||||||
(define-post-data-handler (page-category-with-data req)
|
|
||||||
#'category-endpoint
|
|
||||||
(define prefixed-category path))
|
|
||||||
|
|
||||||
(define page-category (make-switch-handler #:standard page-category-standard
|
|
||||||
#:jsonp page-category-jsonp
|
|
||||||
#:post page-category-with-data))
|
|
||||||
|
|
||||||
(module+ test
|
(module+ test
|
||||||
(check-not-false ((query-selector (attribute-selector 'href "/test/wiki/Ankle_Monitor")
|
(check-not-false ((query-selector (attribute-selector 'href "/test/wiki/Ankle_Monitor")
|
||||||
(generate-results-page
|
(generate-results-page
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
#lang racket/base
|
#lang racket/base
|
||||||
(require racket/list
|
(require racket/dict
|
||||||
|
racket/list
|
||||||
|
racket/match
|
||||||
racket/string
|
racket/string
|
||||||
|
(prefix-in easy: net/http-easy)
|
||||||
; html libs
|
; html libs
|
||||||
html-parsing
|
html-parsing
|
||||||
html-writing
|
html-writing
|
||||||
|
|
@ -8,204 +11,115 @@
|
||||||
net/url
|
net/url
|
||||||
web-server/http
|
web-server/http
|
||||||
(only-in web-server/dispatchers/dispatch next-dispatcher)
|
(only-in web-server/dispatchers/dispatch next-dispatcher)
|
||||||
|
#;(only-in web-server/http/redirect redirect-to)
|
||||||
"application-globals.rkt"
|
"application-globals.rkt"
|
||||||
"endpoints.rkt"
|
"config.rkt"
|
||||||
"../lib/tree-updater.rkt"
|
"data.rkt"
|
||||||
|
"page-wiki.rkt"
|
||||||
|
"../lib/syntax.rkt"
|
||||||
|
"../lib/thread-utils.rkt"
|
||||||
"../lib/url-utils.rkt"
|
"../lib/url-utils.rkt"
|
||||||
"../lib/xexpr-utils.rkt"
|
"whole-utils.rkt"
|
||||||
"../lib/make-json.rkt")
|
"../lib/xexpr-utils.rkt")
|
||||||
|
|
||||||
(provide page-file)
|
(provide page-file)
|
||||||
|
|
||||||
(module+ test
|
(module+ test
|
||||||
(require rackunit
|
(require rackunit
|
||||||
"test-utils.rkt"
|
"test-utils.rkt")
|
||||||
"../lib/make-json.rkt")
|
(define test-media-detail
|
||||||
(define test-wikipage
|
'#hasheq((fileTitle . "Example file")
|
||||||
(make-json
|
(videoEmbedCode . "")
|
||||||
'(: parse
|
(imageUrl . "https://static.wikia.nocookie.net/examplefile")
|
||||||
(: title "File:Sailor Cinnamoroll.jpg"
|
(rawImageUrl . "https://static.wikia.nocookie.net/examplefile")
|
||||||
pageid 4448
|
(userName . "blankie")
|
||||||
revid 13121
|
(isPostedIn . #t)
|
||||||
text
|
(smallerArticleList . (#hasheq((titleText . "Test:Example article"))))
|
||||||
(: * "<div class=\"mw-content-ltr mw-parser-output\" lang=\"en\" dir=\"ltr\">\n<!-- \nNewPP limit report\nCached time: 20251122101032\nCache expiry: 1209600\nReduced expiry: false\nComplications: []\nCPU time usage: 0.001 seconds\nReal time usage: 0.001 seconds\nPreprocessor visited node count: 0/1000000\nPost\u2010expand include size: 0/2097152 bytes\nTemplate argument size: 0/2097152 bytes\nHighest expansion depth: 0/100\nExpensive parser function count: 0/100\nUnstrip recursion depth: 0/20\nUnstrip post\u2010expand size: 0/5000000 bytes\n-->\n<!--\nTransclusion expansion time report (%,ms,calls,template)\n100.00% 0.000 1 -total\n-->\n\n<!-- Saved in parser cache with key 1.43.1_prod_squishmallowsquad:pcache:idhash:4448-0!sseVary=RegularPage!FandomDesktop!LegacyGalleries and timestamp 20251122101032 and revision id 13121. Rendering was triggered because: api-parse\n -->\n</div>")
|
(articleListIsSmaller . 0)
|
||||||
langlinks ()
|
(exists . #t)
|
||||||
categories ()
|
(imageDescription . #f))))
|
||||||
links ()
|
|
||||||
templates ()
|
|
||||||
images ()
|
|
||||||
externallinks ()
|
|
||||||
sections ()
|
|
||||||
parsewarnings ()
|
|
||||||
displaytitle "<span class=\"mw-page-title-namespace\">File</span><span class=\"mw-page-title-separator\">:</span><span class=\"mw-page-title-main\">Sailor Cinnamoroll.jpg</span>"
|
|
||||||
iwlinks ()
|
|
||||||
properties ()))))
|
|
||||||
(define test-imageinfo-outer
|
|
||||||
(make-json
|
|
||||||
`(: continue
|
|
||||||
(: iistart "2022-01-23T03:44:17Z"
|
|
||||||
fucontinue "455"
|
|
||||||
continue "||")
|
|
||||||
query
|
|
||||||
(: pages
|
|
||||||
((: pageid 198
|
|
||||||
ns 6
|
|
||||||
title "File:Rainbow Flag1.svg"
|
|
||||||
imageinfo
|
|
||||||
((: timestamp "2025-03-10T07:24:50Z"
|
|
||||||
user "DogeMcMeow"
|
|
||||||
url "https://static.wikia.nocookie.net/lgbtqia-sandbox/images/f/f8/Rainbow_Flag1.svg/revision/latest?cb=20250310072450"
|
|
||||||
descriptionurl "https://lgbtqia.fandom.com/wiki/File:Rainbow_Flag1.svg"
|
|
||||||
descriptionshorturl "https://lgbtqia.fandom.com/index.php?curid=198"
|
|
||||||
mime "image/svg+xml"
|
|
||||||
mediatype "DRAWING"))
|
|
||||||
fileusage
|
|
||||||
((: pageid 191
|
|
||||||
ns 0
|
|
||||||
title "Gay")
|
|
||||||
(: pageid 215
|
|
||||||
ns 0
|
|
||||||
title "LGBTQIA+")))))))))
|
|
||||||
|
|
||||||
;; https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/refs/heads/REL1_44/includes/libs/mime/defines.php
|
(define (url-content-type url)
|
||||||
;; (define possible-mediatypes '("UNKNOWN" "BITMAP" "DRAWING" "AUDIO" "VIDEO" "MULTIMEDIA" "OFFICE" "TEXT" "EXECUTABLE" "ARCHIVE" "3D"))
|
(log-outgoing url)
|
||||||
|
(define dest-res (easy:head url #:timeouts timeouts))
|
||||||
|
(easy:response-headers-ref dest-res 'content-type))
|
||||||
|
|
||||||
(define (get-media-html url imageinfo-inner)
|
(define (get-media-html url content-type)
|
||||||
(define maybe-proxied-url (if (config-true? 'strict_proxy) (u-proxy-url url) url))
|
(define maybe-proxied-url (if (config-true? 'strict_proxy) (u-proxy-url url) url))
|
||||||
(define mediatype (jp "/mediatype" imageinfo-inner #f))
|
(cond
|
||||||
(case mediatype
|
[(eq? content-type #f) `""]
|
||||||
[("BITMAP" "DRAWING")
|
[(regexp-match? #rx"(?i:^image/)" content-type) `(img (@ (src ,maybe-proxied-url)))]
|
||||||
(match imageinfo-inner
|
[(regexp-match? #rx"(?i:^audio/|^application/ogg(;|$))" content-type)
|
||||||
[(hash* ['width width] ['height height])
|
`(audio (@ (src ,maybe-proxied-url) (controls)))]
|
||||||
`(img (@ (src ,maybe-proxied-url) (width ,(~a width)) (height ,(~a height))))]
|
[(regexp-match? #rx"(?i:^video/)" content-type) `(video (@ (src ,maybe-proxied-url) (controls)))]
|
||||||
[else
|
[else `""]))
|
||||||
`(img (@ (src ,maybe-proxied-url)))])]
|
|
||||||
[("AUDIO") `(audio (@ (src ,maybe-proxied-url) (controls)))]
|
|
||||||
[("VIDEO") `(video (@ (src ,maybe-proxied-url) (controls)))]
|
|
||||||
[else ""]))
|
|
||||||
|
|
||||||
(define (generate-results-page #:req req
|
(define (generate-results-page #:req req
|
||||||
#:source-url source-url
|
#:source-url source-url
|
||||||
#:wikiname wikiname
|
#:wikiname wikiname
|
||||||
#:title title
|
#:title title
|
||||||
#:wikipage wikipage
|
#:media-detail media-detail
|
||||||
#:imageinfo imageinfo-outer
|
#:image-content-type image-content-type
|
||||||
#:siteinfo [siteinfo #f])
|
#:siteinfo [siteinfo #f])
|
||||||
(define imageinfo-inner (jp "/query/pages/0" imageinfo-outer))
|
(define video-embed-code (jp "/videoEmbedCode" media-detail ""))
|
||||||
(define fileusage-continues? (jp "/continue/fucontinue" imageinfo-outer #f))
|
(define raw-image-url (jp "/rawImageUrl" media-detail))
|
||||||
(define fileusage-titles (for/list ([page (jp "/fileusage" imageinfo-inner)]) (jp "/title" page)))
|
(define image-url (jp "/imageUrl" media-detail raw-image-url))
|
||||||
(define image-url (jp "/imageinfo/0/url" imageinfo-inner))
|
(define username (jp "/userName" media-detail))
|
||||||
(define username (jp "/imageinfo/0/user" imageinfo-inner))
|
(define is-posted-in (jp "/isPostedIn" media-detail #f))
|
||||||
(define maybe-proxied-image-url
|
(define smaller-article-list (jp "/smallerArticleList" media-detail))
|
||||||
(if (config-true? 'strict_proxy) (u-proxy-url image-url) image-url))
|
(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
|
(generate-wiki-page
|
||||||
#:req req
|
#:req req
|
||||||
#:source-url source-url
|
#:source-url source-url
|
||||||
#:wikiname wikiname
|
#:wikiname wikiname
|
||||||
#:title title
|
#:title title
|
||||||
#:siteinfo siteinfo
|
#:siteinfo siteinfo
|
||||||
`(div ,(get-media-html maybe-proxied-image-url (jp "/imageinfo/0" imageinfo-inner))
|
`(div ,(if (non-empty-string? video-embed-code)
|
||||||
(p (a (@ (href ,maybe-proxied-image-url)) "Download original file"))
|
(update-tree-wiki (html->xexp (preprocess-html-wiki video-embed-code)) wikiname)
|
||||||
,(if (pair? (jp "/parse/sections" wikipage))
|
(get-media-html image-url image-content-type))
|
||||||
(update-tree-wiki (html->xexp (preprocess-html-wiki (jp "/parse/text" wikipage ""))) wikiname)
|
(p ,(if (non-empty-string? video-embed-code)
|
||||||
; file license info wasn't displayed in the description (example: /lgbtqia/wiki/File:Rainbow_Flag1.svg)
|
`""
|
||||||
`(p "This file may be copyrighted. Consider licensing and fair use law before reusing it."))
|
`(span (a (@ (href ,maybe-proxied-raw-image-url)) "View original file") ". "))
|
||||||
(p "Uploaded by "
|
"Uploaded by "
|
||||||
(a (@ (href ,(format "/~a/wiki/User:~a" wikiname username))) ,username)
|
(a (@ (href ,(format "/~a/wiki/User:~a" wikiname username))) ,username)
|
||||||
".")
|
".")
|
||||||
,(if (pair? fileusage-titles)
|
,(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 "
|
`(p "This file is used in "
|
||||||
,@(add-between
|
,@(map (λ (article)
|
||||||
(for/list ([title fileusage-titles]
|
(define title (jp "/titleText" article))
|
||||||
[i (in-naturals)])
|
(define page-path (regexp-replace* #rx" " title "_"))
|
||||||
(define page-path (regexp-replace* #rx" " title "_"))
|
`(span ,(if (eq? (car smaller-article-list) article) "" ", ")
|
||||||
`(a (@ (href ,(format "/~a/wiki/~a" wikiname page-path)))
|
(a (@ (href ,(format "/~a/wiki/~a" wikiname page-path)))
|
||||||
,title))
|
,title)))
|
||||||
", ")
|
smaller-article-list)
|
||||||
,(if fileusage-continues? "…" "."))
|
,(if (eq? article-list-is-smaller 1) "…" "."))
|
||||||
`""))))
|
`""))))
|
||||||
|
|
||||||
(define-endpoint
|
(define (page-file req)
|
||||||
file-endpoint
|
|
||||||
[variables
|
|
||||||
(define wikiname (path/param-path (first (url-path (request-uri req)))))
|
|
||||||
(define prefixed-file (path/param-path (caddr (url-path (request-uri req)))))
|
|
||||||
(define segments (map path/param-path (cdr (url-path (request-uri req)))))
|
|
||||||
(define title (url-segments->guess-title segments))
|
|
||||||
(define path (string-join (cdr segments) "/"))
|
|
||||||
(define source-url (format "https://~a.fandom.com/wiki/~a" wikiname prefixed-file))]
|
|
||||||
[endpoints
|
|
||||||
(wikipage
|
|
||||||
(("action" . "parse")
|
|
||||||
("page" . ,prefixed-file)
|
|
||||||
("prop" . "text|headhtml|langlinks|sections")
|
|
||||||
("formatversion" . "2")
|
|
||||||
("format" . "json")))
|
|
||||||
(siteinfo
|
|
||||||
(("action" . "query")
|
|
||||||
("meta" . "siteinfo")
|
|
||||||
("siprop" . "general|rightsinfo")
|
|
||||||
("format" . "json")
|
|
||||||
("formatversion" . "2")))
|
|
||||||
(imageinfo
|
|
||||||
(("action" . "query")
|
|
||||||
("titles" . ,prefixed-file)
|
|
||||||
("prop" . "imageinfo|fileusage")
|
|
||||||
("iiprop" . "timestamp|user|canonicaltitle|url|size|mime|mediatype")
|
|
||||||
("iilocalonly" . "true")
|
|
||||||
("format" . "json")
|
|
||||||
("formatversion" . "2")))]
|
|
||||||
[render
|
|
||||||
(response-handler
|
|
||||||
(define title (jp "/parse/title" wikipage prefixed-file))
|
|
||||||
(define body
|
|
||||||
(generate-results-page #:req req
|
|
||||||
#:source-url source-url
|
|
||||||
#:wikiname wikiname
|
|
||||||
#:title title
|
|
||||||
#:wikipage wikipage
|
|
||||||
#:imageinfo imageinfo
|
|
||||||
#: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-standard-handler (page-file-standard req)
|
|
||||||
#'file-endpoint
|
|
||||||
(when (equal? "missingtitle" (jp "/error/code" wikipage #f))
|
|
||||||
(next-dispatcher)))
|
|
||||||
|
|
||||||
(define-jsonp-handler (page-file-jsonp req)
|
|
||||||
#'file-endpoint)
|
|
||||||
|
|
||||||
(define-post-data-handler (page-file-with-data req)
|
|
||||||
#'file-endpoint
|
|
||||||
(define prefixed-file path))
|
|
||||||
|
|
||||||
(define page-file (make-switch-handler #:standard page-file-standard
|
|
||||||
#:jsonp page-file-jsonp
|
|
||||||
#:post page-file-with-data))
|
|
||||||
|
|
||||||
#;(define (page-file req)
|
|
||||||
(response-handler
|
(response-handler
|
||||||
(define wikiname (path/param-path (first (url-path (request-uri 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 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 origin (format "https://~a.fandom.com" wikiname))
|
||||||
|
(define source-url (format "~a/wiki/~a" origin prefixed-title))
|
||||||
|
|
||||||
(define-values (media-detail siteinfo)
|
(define-values (media-detail siteinfo)
|
||||||
(thread-values
|
(thread-values
|
||||||
(λ ()
|
(λ ()
|
||||||
(define dest-res
|
(define dest-url
|
||||||
(fandom-get
|
(format "~a/wikia.php?~a"
|
||||||
wikiname
|
origin
|
||||||
(format "/wikia.php?~a"
|
(params->query `(("format" . "json") ("controller" . "Lightbox")
|
||||||
(params->query `(("format" . "json") ("controller" . "Lightbox")
|
("method" . "getMediaDetail")
|
||||||
("method" . "getMediaDetail")
|
("fileTitle" . ,prefixed-title)))))
|
||||||
("fileTitle" . ,prefixed-title))))))
|
(log-outgoing dest-url)
|
||||||
|
(define dest-res (easy:get dest-url #:timeouts timeouts))
|
||||||
(easy:response-json dest-res))
|
(easy:response-json dest-res))
|
||||||
(λ ()
|
(λ ()
|
||||||
(siteinfo-fetch wikiname))))
|
(siteinfo-fetch wikiname))))
|
||||||
|
|
@ -236,28 +150,28 @@
|
||||||
(λ (out) (write-html body out)))))))
|
(λ (out) (write-html body out)))))))
|
||||||
(module+ test
|
(module+ test
|
||||||
(parameterize ([(config-parameter 'strict_proxy) "true"])
|
(parameterize ([(config-parameter 'strict_proxy) "true"])
|
||||||
(check-equal? (get-media-html "https://static.wikia.nocookie.net/a" (make-json '(: mediatype "BITMAP")))
|
(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"))))
|
`(img (@ (src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fa"))))
|
||||||
(check-equal? (get-media-html "https://static.wikia.nocookie.net/b" (make-json '(: mediatype "AUDIO")))
|
(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")
|
`(audio (@ (src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fb")
|
||||||
(controls)))))
|
(controls)))))
|
||||||
(parameterize ([(config-parameter 'strict_proxy) "false"])
|
(parameterize ([(config-parameter 'strict_proxy) "false"])
|
||||||
(check-equal? (get-media-html "https://static.wikia.nocookie.net/c" (make-json '(: mediatype "AUDIO")))
|
(check-equal? (get-media-html "https://static.wikia.nocookie.net/c" "application/ogg")
|
||||||
`(audio (@ (src "https://static.wikia.nocookie.net/c")
|
`(audio (@ (src "https://static.wikia.nocookie.net/c")
|
||||||
(controls))))
|
(controls))))
|
||||||
(check-equal? (get-media-html "https://static.wikia.nocookie.net/d" (make-json '(: mediatype "VIDEO")))
|
(check-equal? (get-media-html "https://static.wikia.nocookie.net/d" "video/mp4")
|
||||||
`(video (@ (src "https://static.wikia.nocookie.net/d")
|
`(video (@ (src "https://static.wikia.nocookie.net/d")
|
||||||
(controls)))))
|
(controls)))))
|
||||||
(check-equal? (get-media-html "https://example.com" "who knows") "")
|
(check-equal? (get-media-html "https://example.com" "who knows") `"")
|
||||||
(check-equal? (get-media-html "https://example.com" #f) ""))
|
(check-equal? (get-media-html "https://example.com" #f) `""))
|
||||||
(module+ test
|
(module+ test
|
||||||
(parameterize ([(config-parameter 'strict_proxy) "true"])
|
(parameterize ([(config-parameter 'strict_proxy) "true"])
|
||||||
(check-not-false
|
(check-not-false
|
||||||
((query-selector
|
((query-selector
|
||||||
(attribute-selector 'src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Flgbtqia-sandbox%2Fimages%2Ff%2Ff8%2FRainbow_Flag1.svg%2Frevision%2Flatest%3Fcb%3D20250310072450")
|
(attribute-selector 'src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fexamplefile")
|
||||||
(generate-results-page #:req test-req
|
(generate-results-page #:req test-req
|
||||||
#:source-url ""
|
#:source-url ""
|
||||||
#:wikiname "test"
|
#:wikiname "test"
|
||||||
#:title "File:Example file"
|
#:title "File:Example file"
|
||||||
#:wikipage test-wikipage
|
#:media-detail test-media-detail
|
||||||
#:imageinfo test-imageinfo-outer))))))
|
#:image-content-type "image/jpeg"))))))
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
(require racket/dict
|
(require racket/dict
|
||||||
racket/list
|
racket/list
|
||||||
racket/string
|
racket/string
|
||||||
|
(prefix-in easy: net/http-easy)
|
||||||
; html libs
|
; html libs
|
||||||
html-writing
|
html-writing
|
||||||
; web server libs
|
; web server libs
|
||||||
|
|
@ -17,13 +18,15 @@
|
||||||
"../lib/syntax.rkt"
|
"../lib/syntax.rkt"
|
||||||
"../lib/thread-utils.rkt"
|
"../lib/thread-utils.rkt"
|
||||||
"../lib/url-utils.rkt"
|
"../lib/url-utils.rkt"
|
||||||
|
"whole-utils.rkt"
|
||||||
"../lib/xexpr-utils.rkt")
|
"../lib/xexpr-utils.rkt")
|
||||||
|
|
||||||
(provide
|
(provide
|
||||||
page-search)
|
page-search)
|
||||||
|
|
||||||
(define search-providers
|
(define search-providers
|
||||||
(hash "solr" search-solr))
|
(hash "fandom" search-fandom
|
||||||
|
"solr" search-solr))
|
||||||
|
|
||||||
;; this takes the info we gathered from fandom and makes the big fat x-expression page
|
;; 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])
|
(define (generate-results-page req source-url wikiname query results-content #:siteinfo [siteinfo #f])
|
||||||
|
|
@ -39,7 +42,7 @@
|
||||||
results-content))
|
results-content))
|
||||||
|
|
||||||
;; will be called when the web browser asks to load the page
|
;; will be called when the web browser asks to load the page
|
||||||
(define (page-search-solr req)
|
(define (page-search req)
|
||||||
;; this just means, catch any errors and display them in the browser. it's a function somewhere else
|
;; this just means, catch any errors and display them in the browser. it's a function somewhere else
|
||||||
(response-handler
|
(response-handler
|
||||||
;; the URL will look like "/minecraft/wiki/Special:Search?q=Spawner"
|
;; the URL will look like "/minecraft/wiki/Special:Search?q=Spawner"
|
||||||
|
|
@ -83,9 +86,3 @@
|
||||||
(λ (out)
|
(λ (out)
|
||||||
(write-html body out)))))
|
(write-html body out)))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(define (page-search req)
|
|
||||||
(if (equal? (config-get 'feature_offline::search) "fandom")
|
|
||||||
(page-search-fandom req)
|
|
||||||
(page-search-solr req)))
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
#lang racket/base
|
#lang racket/base
|
||||||
(require racket/dict
|
(require racket/dict
|
||||||
|
racket/function
|
||||||
|
racket/list
|
||||||
|
racket/match
|
||||||
racket/string
|
racket/string
|
||||||
|
; libs
|
||||||
|
(prefix-in easy: net/http-easy)
|
||||||
; html libs
|
; html libs
|
||||||
"../lib/html-parsing/main.rkt"
|
"../lib/html-parsing/main.rkt"
|
||||||
html-writing
|
html-writing
|
||||||
|
|
@ -10,176 +15,92 @@
|
||||||
web-server/dispatchers/dispatch
|
web-server/dispatchers/dispatch
|
||||||
; my libs
|
; my libs
|
||||||
"application-globals.rkt"
|
"application-globals.rkt"
|
||||||
"endpoints.rkt"
|
"config.rkt"
|
||||||
|
"data.rkt"
|
||||||
|
"../lib/pure-utils.rkt"
|
||||||
|
"../lib/syntax.rkt"
|
||||||
|
"../lib/thread-utils.rkt"
|
||||||
"../lib/tree-updater.rkt"
|
"../lib/tree-updater.rkt"
|
||||||
"../lib/url-utils.rkt"
|
"../lib/url-utils.rkt"
|
||||||
|
"whole-utils.rkt"
|
||||||
"../lib/xexpr-utils.rkt")
|
"../lib/xexpr-utils.rkt")
|
||||||
(require (for-syntax racket/base syntax/parse))
|
|
||||||
|
|
||||||
(provide
|
(provide
|
||||||
page-wiki)
|
; used by the web server
|
||||||
|
page-wiki
|
||||||
|
; used by page-category, and similar pages that are partially wiki pages
|
||||||
|
update-tree-wiki
|
||||||
|
preprocess-html-wiki)
|
||||||
|
|
||||||
(define-endpoint
|
(module+ test
|
||||||
wiki-endpoint
|
(require rackunit))
|
||||||
[variables
|
|
||||||
(define wikiname (path/param-path (car (url-path (request-uri req)))))
|
|
||||||
(define segments (map path/param-path (cdr (url-path (request-uri req)))))
|
|
||||||
(define title (url-segments->guess-title segments))
|
|
||||||
(define path (string-join (cdr segments) "/"))
|
|
||||||
(define source-url (format "https://~a.fandom.com/wiki/~a" wikiname path))]
|
|
||||||
[endpoints
|
|
||||||
(wikipage (("action" . "parse")
|
|
||||||
("page" . ,path)
|
|
||||||
("prop" . "text|headhtml|langlinks")
|
|
||||||
("formatversion" . "2")
|
|
||||||
("format" . "json")))
|
|
||||||
(siteinfo (("action" . "query")
|
|
||||||
("meta" . "siteinfo")
|
|
||||||
("siprop" . "general|rightsinfo")
|
|
||||||
("format" . "json")
|
|
||||||
("formatversion" . "2")))]
|
|
||||||
[render
|
|
||||||
(define page-html (preprocess-html-wiki (jp "/parse/text" wikipage "")))
|
|
||||||
(define page (html->xexp page-html))
|
|
||||||
(define head-data ((head-data-getter wikiname) wikipage))
|
|
||||||
(define body
|
|
||||||
(generate-wiki-page
|
|
||||||
(update-tree-wiki page wikiname)
|
|
||||||
#:req req
|
|
||||||
#:source-url source-url
|
|
||||||
#:wikiname wikiname
|
|
||||||
#:title (jp "/parse/title" wikipage "")
|
|
||||||
#:head-data head-data
|
|
||||||
#:siteinfo siteinfo))
|
|
||||||
(define redirect-query-parameter (dict-ref (url-query (request-uri req)) 'redirect "yes"))
|
|
||||||
(define redirect-msg ((query-selector (attribute-selector 'class "redirectMsg") body)))
|
|
||||||
(define redirect-msg-a (if redirect-msg
|
|
||||||
((query-selector (λ (t a c) (eq? t 'a)) redirect-msg))
|
|
||||||
#f))
|
|
||||||
(define html (xexp->html-bytes body))
|
|
||||||
(define headers
|
|
||||||
(build-headers
|
|
||||||
always-headers
|
|
||||||
; redirect-query-parameter: only the string "no" is significant:
|
|
||||||
; https://github.com/Wikia/app/blob/fe60579a53f16816d65dad1644363160a63206a6/includes/Wiki.php#L367
|
|
||||||
(when (and redirect-msg-a
|
|
||||||
(not (equal? redirect-query-parameter "no")))
|
|
||||||
(let* ([dest (get-attribute 'href (bits->attributes redirect-msg-a))]
|
|
||||||
[value (bytes-append #"0;url=" (string->bytes/utf-8 dest))])
|
|
||||||
(header #"Refresh" value)))))
|
|
||||||
(response/full
|
|
||||||
200
|
|
||||||
#"OK"
|
|
||||||
(current-seconds)
|
|
||||||
#"text/html; charset=utf-8"
|
|
||||||
headers
|
|
||||||
(list html))])
|
|
||||||
|
|
||||||
(define-standard-handler (page-wiki-standard req)
|
(define (page-wiki req)
|
||||||
#'wiki-endpoint
|
|
||||||
(when (equal? "missingtitle" (jp "/error/code" wikipage #f))
|
|
||||||
(next-dispatcher)))
|
|
||||||
|
|
||||||
(define-jsonp-handler (page-wiki-jsonp req)
|
|
||||||
#'wiki-endpoint)
|
|
||||||
|
|
||||||
(define-post-data-handler (page-wiki-with-data req)
|
|
||||||
#'wiki-endpoint)
|
|
||||||
|
|
||||||
(define page-wiki (make-switch-handler #:standard page-wiki-standard
|
|
||||||
#:jsonp page-wiki-jsonp
|
|
||||||
#:post page-wiki-with-data))
|
|
||||||
|
|
||||||
#;(define (page-wiki-with-data req)
|
|
||||||
(response-handler
|
|
||||||
(let/cc return
|
|
||||||
(define post-data/bytes (request-post-data/raw req))
|
|
||||||
(when (not post-data/bytes)
|
|
||||||
(k (response/jsexpr
|
|
||||||
#:code 400
|
|
||||||
#:headers always-headers
|
|
||||||
'#hasheq((error .
|
|
||||||
#hasheq((code . "breezewiki")
|
|
||||||
(info . "POST requests only, please.")))))))
|
|
||||||
|
|
||||||
(define origin-header
|
|
||||||
(or (headers-assq* #"origin" (request-headers/raw req))
|
|
||||||
(headers-assq* #"referer" (request-headers/raw req))))
|
|
||||||
(when (or (not origin-header) (not (string-prefix? (bytes->string/latin-1 (header-value origin-header)) (config-get 'canonical_origin))))
|
|
||||||
(k (response/jsexpr
|
|
||||||
#:code 400
|
|
||||||
#:headers always-headers
|
|
||||||
'#hasheq((error .
|
|
||||||
#hasheq((code . "breezewiki")
|
|
||||||
(info . "Origin/Referer header failed validation - cross-origin requests are not allowed here")))))))
|
|
||||||
|
|
||||||
(define post-data/string (bytes->string/utf-8 post-data/bytes))
|
|
||||||
(define post-data (string->jsexpr post-data/string))
|
|
||||||
(define wikiname (jp "/wikiname" post-data))
|
|
||||||
(define path (jp "/path" post-data))
|
|
||||||
(take-json-rewrite-and-return-page
|
|
||||||
#:req req
|
|
||||||
#:wikiname wikiname
|
|
||||||
#:source-url (format "https://~a.fandom.com/wiki/~a" wikiname path)
|
|
||||||
#:data (jp "/data" post-data)
|
|
||||||
#:siteinfo (data->siteinfo (jp "/siteinfo" post-data))))))
|
|
||||||
|
|
||||||
#;(define (page-wiki req)
|
|
||||||
(define wikiname (path/param-path (first (url-path (request-uri req)))))
|
(define wikiname (path/param-path (first (url-path (request-uri req)))))
|
||||||
(define segments (map path/param-path (cdr (url-path (request-uri req)))))
|
|
||||||
(define user-cookies (user-cookies-getter req))
|
(define user-cookies (user-cookies-getter req))
|
||||||
(define path (string-join (cdr segments) "/"))
|
(define origin (format "https://~a.fandom.com" wikiname))
|
||||||
|
(define path (string-join (map path/param-path (cddr (url-path (request-uri req)))) "/"))
|
||||||
(define source-url (format "https://~a.fandom.com/wiki/~a" wikiname path))
|
(define source-url (format "https://~a.fandom.com/wiki/~a" wikiname path))
|
||||||
|
|
||||||
(define-values (dest-res siteinfo)
|
(define-values (dest-res siteinfo)
|
||||||
(thread-values
|
(thread-values
|
||||||
(λ ()
|
(λ ()
|
||||||
(fandom-get-api
|
(define dest-url
|
||||||
wikiname
|
(format "~a/api.php?~a"
|
||||||
`(("action" . "parse")
|
origin
|
||||||
("page" . ,path)
|
(params->query `(("action" . "parse")
|
||||||
("prop" . "text|headhtml|langlinks")
|
("page" . ,path)
|
||||||
("formatversion" . "2")
|
("prop" . "text|headhtml|langlinks")
|
||||||
("format" . "json"))
|
("formatversion" . "2")
|
||||||
#:headers `#hasheq((cookie . ,(format "theme=~a" (user-cookies^-theme user-cookies))))))
|
("format" . "json")))))
|
||||||
|
(log-outgoing dest-url)
|
||||||
|
(easy:get dest-url
|
||||||
|
#:timeouts timeouts
|
||||||
|
#:headers `#hasheq((cookie . ,(format "theme=~a" (user-cookies^-theme user-cookies))))))
|
||||||
(λ ()
|
(λ ()
|
||||||
(siteinfo-fetch wikiname))))
|
(siteinfo-fetch wikiname))))
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
[(eq? 200 (easy:response-status-code dest-res))
|
[(eq? 200 (easy:response-status-code dest-res))
|
||||||
(let ([data (easy:response-json dest-res)])
|
(let* ([data (easy:response-json dest-res)]
|
||||||
|
[title (jp "/parse/title" data "")]
|
||||||
|
[page-html (jp "/parse/text" data "")]
|
||||||
|
[page-html (preprocess-html-wiki page-html)]
|
||||||
|
[page (html->xexp page-html)]
|
||||||
|
[head-data ((head-data-getter wikiname) data)])
|
||||||
(if (equal? "missingtitle" (jp "/error/code" data #f))
|
(if (equal? "missingtitle" (jp "/error/code" data #f))
|
||||||
(next-dispatcher)
|
(next-dispatcher)
|
||||||
(take-json-rewrite-and-return-page data)))]
|
(response-handler
|
||||||
[(eq? 404 (easy:response-status-code dest-res))
|
(define body
|
||||||
(next-dispatcher)]
|
(generate-wiki-page
|
||||||
[(memq (easy:response-status-code dest-res) '(403 406))
|
(update-tree-wiki page wikiname)
|
||||||
(response-handler
|
#:req req
|
||||||
(define body
|
#:source-url source-url
|
||||||
(generate-wiki-page
|
#:wikiname wikiname
|
||||||
`(div
|
#:title title
|
||||||
(p "Sorry! Fandom isn't allowing BreezeWiki to show pages right now.")
|
#:head-data head-data
|
||||||
(p "We'll automatically try again in 30 seconds, so please stay on this page and be patient.")
|
#:siteinfo siteinfo))
|
||||||
(p (small "In a hurry? " (a (@ (href ,source-url)) "Click here to read the page on Fandom."))))
|
(define redirect-query-parameter (dict-ref (url-query (request-uri req)) 'redirect "yes"))
|
||||||
#:req req
|
(define redirect-msg ((query-selector (attribute-selector 'class "redirectMsg") body)))
|
||||||
#:source-url source-url
|
(define redirect-msg-a (if redirect-msg
|
||||||
#:wikiname wikiname
|
((query-selector (λ (t a c) (eq? t 'a)) redirect-msg))
|
||||||
#:title (url-segments->guess-title segments)
|
#f))
|
||||||
#:siteinfo siteinfo))
|
(define headers
|
||||||
(response/output
|
(build-headers
|
||||||
#:code 503
|
always-headers
|
||||||
#:headers (build-headers
|
; redirect-query-parameter: only the string "no" is significant:
|
||||||
always-headers
|
; https://github.com/Wikia/app/blob/fe60579a53f16816d65dad1644363160a63206a6/includes/Wiki.php#L367
|
||||||
(header #"Retry-After" #"30")
|
(when (and redirect-msg-a
|
||||||
(header #"Cache-Control" #"max-age=30, public")
|
(not (equal? redirect-query-parameter "no")))
|
||||||
(header #"Refresh" #"35"))
|
(let* ([dest (get-attribute 'href (bits->attributes redirect-msg-a))]
|
||||||
(λ (out)
|
[value (bytes-append #"0;url=" (string->bytes/utf-8 dest))])
|
||||||
(write-html body out))))]
|
(header #"Refresh" value)))))
|
||||||
[else
|
(when (config-true? 'debug)
|
||||||
(response-handler
|
; used for its side effects
|
||||||
(error 'page-wiki "Tried to load page ~a/~a~nSadly, the page didn't load because Fandom returned status code ~a with response:~n~a"
|
; convert to string with error checking, error will be raised if xexp is invalid
|
||||||
wikiname
|
(xexp->html body))
|
||||||
path
|
(response/output
|
||||||
(easy:response-status-code dest-res)
|
#:code 200
|
||||||
(easy:response-body dest-res)))]))
|
#:headers headers
|
||||||
|
(λ (out)
|
||||||
|
(write-html body out))))))]))
|
||||||
|
|
|
||||||
|
|
@ -1,95 +1,65 @@
|
||||||
#lang racket/base
|
#lang racket/base
|
||||||
(require racket/dict
|
(require racket/string
|
||||||
racket/string
|
|
||||||
(prefix-in easy: net/http-easy)
|
(prefix-in easy: net/http-easy)
|
||||||
net/url
|
|
||||||
web-server/http
|
|
||||||
html-writing
|
|
||||||
"application-globals.rkt"
|
"application-globals.rkt"
|
||||||
"config.rkt"
|
"config.rkt"
|
||||||
"endpoints.rkt"
|
|
||||||
"fandom-request.rkt"
|
|
||||||
"../lib/url-utils.rkt"
|
"../lib/url-utils.rkt"
|
||||||
|
"whole-utils.rkt"
|
||||||
"../lib/xexpr-utils.rkt")
|
"../lib/xexpr-utils.rkt")
|
||||||
|
|
||||||
(provide
|
(provide
|
||||||
page-search-fandom)
|
search-fandom)
|
||||||
|
|
||||||
(define-endpoint
|
(module+ test
|
||||||
search-endpoint
|
(require rackunit
|
||||||
[variables
|
"test-utils.rkt")
|
||||||
(define wikiname (path/param-path (car (url-path (request-uri req)))))
|
(define search-results-data
|
||||||
(define params (url-query (request-uri req)))
|
'(#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 query (dict-ref params 'q #f))
|
|
||||||
(define title "Search")
|
|
||||||
(define path (format "Special:Search?~a" (params->query `(("query" . ,query)
|
|
||||||
("search" . "internal")))))
|
|
||||||
(define source-url (format "https://~a.fandom.com/wiki/~a" wikiname path))]
|
|
||||||
[endpoints
|
|
||||||
(search
|
|
||||||
(("action" . "query")
|
|
||||||
("list" . "search")
|
|
||||||
("srsearch" . ,query)
|
|
||||||
("formatversion" . "2")
|
|
||||||
("format" . "json")))
|
|
||||||
(siteinfo
|
|
||||||
(("action" . "query")
|
|
||||||
("meta" . "siteinfo")
|
|
||||||
("siprop" . "general|rightsinfo")
|
|
||||||
("format" . "json")
|
|
||||||
("formatversion" . "2")))]
|
|
||||||
[render
|
|
||||||
(define search-results (jp "/query/search" search))
|
|
||||||
(define body
|
|
||||||
(generate-wiki-page
|
|
||||||
#:req req
|
|
||||||
#:source-url source-url
|
|
||||||
#:wikiname wikiname
|
|
||||||
#:title query
|
|
||||||
#:siteinfo siteinfo
|
|
||||||
`(div (@ (class "mw-parser-output"))
|
|
||||||
;; header before the search results showing how many we found
|
|
||||||
(p ,(format "~a results found for " (length search-results))
|
|
||||||
(strong ,query))
|
|
||||||
;; *u*nordered *l*ist of matching search results
|
|
||||||
(ul ,@(for/list ([result search-results])
|
|
||||||
(let* ([title (jp "/title" result)]
|
|
||||||
[page-path (page-title->path title)]
|
|
||||||
[timestamp (jp "/timestamp" result)]
|
|
||||||
[wordcount (jp "/wordcount" result)]
|
|
||||||
[size (jp "/size" result)])
|
|
||||||
;; and make this x-expression...
|
|
||||||
`(li (@ (class "my-result"))
|
|
||||||
(a (@ (class "my-result__link") (href ,(format "/~a/wiki/~a" wikiname page-path))) ; using unquote to insert the result page URL
|
|
||||||
,title) ; using unquote to insert the result page title
|
|
||||||
(div (@ (class "my-result__info")) ; constructing the line under the search result
|
|
||||||
"last edited "
|
|
||||||
(time (@ (datetime ,timestamp)) ,(list-ref (string-split timestamp "T") 0))
|
|
||||||
,(format ", ~a words, ~a kb"
|
|
||||||
wordcount
|
|
||||||
(exact->inexact (/ (round (/ size 100)) 10)))))))))))
|
|
||||||
(when (config-true? 'debug)
|
|
||||||
(xexp->html body))
|
|
||||||
(response/output
|
|
||||||
#:code 200
|
|
||||||
#:headers (build-headers always-headers)
|
|
||||||
(λ (out)
|
|
||||||
(write-html body out)))])
|
|
||||||
|
|
||||||
|
(define (search-fandom wikiname query params)
|
||||||
|
;; constructing the URL where I want to get fandom data from...
|
||||||
|
(define origin (format "https://~a.fandom.com" wikiname))
|
||||||
|
;; the dest-URL will look something like https://minecraft.fandom.com/api.php?action=query&list=search&srsearch=Spawner&formatversion=2&format=json
|
||||||
|
(define dest-url
|
||||||
|
(format "~a/api.php?~a"
|
||||||
|
origin
|
||||||
|
(params->query `(("action" . "query")
|
||||||
|
("list" . "search")
|
||||||
|
("srsearch" . ,query)
|
||||||
|
("formatversion" . "2")
|
||||||
|
("format" . "json")))))
|
||||||
|
;; HTTP request to dest-url for search results
|
||||||
|
(log-outgoing dest-url)
|
||||||
|
(define res (easy:get dest-url #:timeouts timeouts))
|
||||||
|
(define json (easy:response-json res))
|
||||||
|
(define search-results (jp "/query/search" json))
|
||||||
|
(generate-results-content-fandom wikiname query search-results))
|
||||||
|
|
||||||
(define-standard-handler (page-search-standard req)
|
;;; generate content for display in the wiki page layout
|
||||||
#'search-endpoint)
|
(define (generate-results-content-fandom wikiname query search-results)
|
||||||
|
`(div (@ (class "mw-parser-output"))
|
||||||
|
;; header before the search results showing how many we found
|
||||||
|
(p ,(format "~a results found for " (length search-results))
|
||||||
|
(strong ,query))
|
||||||
|
;; *u*nordered *l*ist of matching search results
|
||||||
|
(ul ,@(for/list ([result search-results])
|
||||||
|
(let* ([title (jp "/title" result)]
|
||||||
|
[page-path (page-title->path title)]
|
||||||
|
[timestamp (jp "/timestamp" result)]
|
||||||
|
[wordcount (jp "/wordcount" result)]
|
||||||
|
[size (jp "/size" result)])
|
||||||
|
;; and make this x-expression...
|
||||||
|
`(li (@ (class "my-result"))
|
||||||
|
(a (@ (class "my-result__link") (href ,(format "/~a/wiki/~a" wikiname page-path))) ; using unquote to insert the result page URL
|
||||||
|
,title) ; using unquote to insert the result page title
|
||||||
|
(div (@ (class "my-result__info")) ; constructing the line under the search result
|
||||||
|
"last edited "
|
||||||
|
(time (@ (datetime ,timestamp)) ,(list-ref (string-split timestamp "T") 0))
|
||||||
|
,(format ", ~a words, ~a kb"
|
||||||
|
wordcount
|
||||||
|
(exact->inexact (/ (round (/ size 100)) 10))))))))))
|
||||||
|
|
||||||
(define-jsonp-handler (page-search-jsonp req)
|
(module+ test
|
||||||
#'search-endpoint)
|
(parameterize ([(config-parameter 'feature_offline::only) "false"])
|
||||||
|
(check-not-false ((query-selector (attribute-selector 'href "/test/wiki/Gacha_Capsule")
|
||||||
(define-post-data-handler (page-search-with-data req)
|
(generate-results-content-fandom "test" "Gacha" search-results-data))))))
|
||||||
#'search-endpoint
|
|
||||||
(define params (url-query (request-uri req)))
|
|
||||||
(define query (dict-ref params 'q #f))
|
|
||||||
(define title "Search"))
|
|
||||||
|
|
||||||
(define page-search-fandom
|
|
||||||
(make-switch-handler #:standard page-search-standard
|
|
||||||
#:jsonp page-search-jsonp
|
|
||||||
#:post page-search-with-data))
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"application-globals.rkt"
|
"application-globals.rkt"
|
||||||
"../lib/html-parsing/main.rkt"
|
"../lib/html-parsing/main.rkt"
|
||||||
"../lib/url-utils.rkt"
|
"../lib/url-utils.rkt"
|
||||||
|
"whole-utils.rkt"
|
||||||
"../lib/xexpr-utils.rkt")
|
"../lib/xexpr-utils.rkt")
|
||||||
|
|
||||||
(provide
|
(provide
|
||||||
|
|
@ -35,7 +36,8 @@
|
||||||
("hl.tag.post" . "</mark>")
|
("hl.tag.post" . "</mark>")
|
||||||
("sort" . ,(cdr sort))))))
|
("sort" . ,(cdr sort))))))
|
||||||
;; HTTP request to dest-url for search results
|
;; HTTP request to dest-url for search results
|
||||||
(define res (easy:get dest-url #:timeouts (easy:make-timeout-config #:lease 5 #:connect 5)))
|
(log-outgoing dest-url)
|
||||||
|
(define res (easy:get dest-url #:timeouts timeouts))
|
||||||
(define json (easy:response-json res))
|
(define json (easy:response-json res))
|
||||||
|
|
||||||
;; build result objects
|
;; build result objects
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
#lang typed/racket/base
|
#lang typed/racket/base
|
||||||
(require racket/path
|
(require racket/path
|
||||||
racket/runtime-path
|
racket/runtime-path
|
||||||
racket/string
|
racket/string)
|
||||||
typed/json)
|
|
||||||
|
|
||||||
(provide
|
(provide
|
||||||
get-static-url
|
get-static-url
|
||||||
importmap
|
|
||||||
link-header)
|
link-header)
|
||||||
|
|
||||||
(define-runtime-path path-static "../static")
|
(define-runtime-path path-static "../static")
|
||||||
|
|
||||||
(define static-data
|
(define static-data
|
||||||
(for/hash : (Immutable-HashTable Path Nonnegative-Integer)
|
(for/hash : (Immutable-HashTable Path Nonnegative-Integer)([f (directory-list path-static)])
|
||||||
([f (directory-list path-static)]
|
|
||||||
#:when (not (regexp-match? #rx"^.#|^#|~$" (path->string f))))
|
|
||||||
(define built (simple-form-path (build-path path-static f)))
|
(define built (simple-form-path (build-path path-static f)))
|
||||||
(values built (file-or-directory-modify-seconds built))))
|
(values built (file-or-directory-modify-seconds built))))
|
||||||
|
|
||||||
|
|
@ -25,21 +21,12 @@
|
||||||
(build-path path-static path-or-filename))))
|
(build-path path-static path-or-filename))))
|
||||||
(format "/static/~a?t=~a" (file-name-from-path the-path) (hash-ref static-data the-path)))
|
(format "/static/~a?t=~a" (file-name-from-path the-path) (hash-ref static-data the-path)))
|
||||||
|
|
||||||
(: importmap String)
|
|
||||||
(define importmap
|
|
||||||
(jsexpr->string
|
|
||||||
`#hasheq((imports . ,(for/hasheq : (Immutable-HashTable Symbol String)
|
|
||||||
([(k v) (in-hash static-data)]
|
|
||||||
#:when (equal? (path-get-extension k) #".js"))
|
|
||||||
(values (string->symbol (path->string (path-replace-extension (assert (file-name-from-path k) path?) #"")))
|
|
||||||
(get-static-url k)))))))
|
|
||||||
|
|
||||||
; https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload
|
; https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload
|
||||||
(: link-header String)
|
(: link-header String)
|
||||||
(define link-header
|
(define link-header
|
||||||
(let* ([with-t '(("main.css" "as=style")
|
(let* ([with-t '(("main.css" "as=style"))]
|
||||||
("preact.js" "as=script"))]
|
[without-t '(("preact.js" "as=script")
|
||||||
[without-t '(("source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2" "as=font" "crossorigin" "type=font/woff2"))]
|
("source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2" "as=font" "crossorigin" "type=font/woff2"))]
|
||||||
[with-t-full (map (λ ([path : (Listof String)]) (cons (get-static-url (car path)) (cdr path))) with-t)]
|
[with-t-full (map (λ ([path : (Listof String)]) (cons (get-static-url (car path)) (cdr path))) with-t)]
|
||||||
[without-t-full (map (λ ([path : (Listof String)]) (cons (format "/static/~a" (car path)) (cdr path))) without-t)]
|
[without-t-full (map (λ ([path : (Listof String)]) (cons (format "/static/~a" (car path)) (cdr path))) without-t)]
|
||||||
[all (append with-t-full without-t-full)]
|
[all (append with-t-full without-t-full)]
|
||||||
|
|
|
||||||
11
src/whole-utils.rkt
Normal file
11
src/whole-utils.rkt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#lang typed/racket/base
|
||||||
|
(require "config.rkt")
|
||||||
|
|
||||||
|
(provide
|
||||||
|
; prints "out: <url>"
|
||||||
|
log-outgoing)
|
||||||
|
|
||||||
|
(: log-outgoing (String -> Void))
|
||||||
|
(define (log-outgoing url-string)
|
||||||
|
(when (config-true? 'log_outgoing)
|
||||||
|
(printf "out: ~a~n" url-string)))
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
const u = new URL(location)
|
|
||||||
const from = u.searchParams.get("from") || location.href
|
|
||||||
let answered = false
|
|
||||||
|
|
||||||
const area = document.getElementById("captcha-area")
|
|
||||||
const areaBox = area.getBoundingClientRect()
|
|
||||||
const width = Math.floor(areaBox.width)
|
|
||||||
const height = Math.floor(window.innerHeight - areaBox.bottom - areaBox.left)
|
|
||||||
const img = document.createElement("img")
|
|
||||||
img.src = `/captcha/img/${width}/${height}`
|
|
||||||
img.addEventListener("click", event => {
|
|
||||||
if (answered) return
|
|
||||||
answered = true
|
|
||||||
location = `/captcha/verify/${width}/${height}/${event.offsetX}/${event.offsetY}?` + new URLSearchParams({from})
|
|
||||||
})
|
|
||||||
area.appendChild(img)
|
|
||||||
|
|
||||||
document.addEventListener("keydown", event => {
|
|
||||||
if (event.repeat) {
|
|
||||||
if (answered) return
|
|
||||||
answered = true
|
|
||||||
location = `/captcha/verify/0/0/${event.key}/0?` + new URLSearchParams({from})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
// sample: bandori/wiki/BanG_Dream!_Wikia
|
// sample: bandori/wiki/BanG_Dream!_Wikia
|
||||||
// sample: ensemble-stars/wiki/The_English_Ensemble_Stars_Wiki
|
// sample: ensemble-stars/wiki/The_English_Ensemble_Stars_Wiki
|
||||||
|
|
||||||
import {h, htm, render, signal, computed, effect} from "preact"
|
import {h, htm, render, signal, computed, effect} from "./preact.js"
|
||||||
const html = htm.bind(h)
|
const html = htm.bind(h)
|
||||||
|
|
||||||
const now = signal(Date.now())
|
const now = signal(Date.now())
|
||||||
|
|
|
||||||
140
static/jsonp.js
140
static/jsonp.js
|
|
@ -1,140 +0,0 @@
|
||||||
import {h, htm, render, signal, computed, effect} from "preact"
|
|
||||||
const html = htm.bind(h)
|
|
||||||
|
|
||||||
// *** Status
|
|
||||||
|
|
||||||
const loaded = signal(false)
|
|
||||||
export {loaded}
|
|
||||||
|
|
||||||
// *** Loading indicator
|
|
||||||
|
|
||||||
render(html`Loading, please wait...`, document.getElementById("loading"))
|
|
||||||
|
|
||||||
// *** Progress bar
|
|
||||||
|
|
||||||
const progress = signal(null)
|
|
||||||
|
|
||||||
const progressBar = document.getElementById("progress-bar")
|
|
||||||
while (progressBar.childNodes[0] !== undefined) progressBar.childNodes[0].remove() // clear out loading indicators
|
|
||||||
|
|
||||||
render(html`<progress value="${progress}" max="1"></progress>`, progressBar)
|
|
||||||
|
|
||||||
// *** Incoming data processing
|
|
||||||
|
|
||||||
// Handle case where data is immediately available
|
|
||||||
cont()
|
|
||||||
|
|
||||||
// Handle case where data may become available in the future
|
|
||||||
window.proxy = new Proxy(jsonpData, {
|
|
||||||
get(obj, prop) {
|
|
||||||
return value => {
|
|
||||||
obj[prop] = value
|
|
||||||
cont()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// *** Data upload and download
|
|
||||||
|
|
||||||
async function cont() {
|
|
||||||
// Check for errors
|
|
||||||
for (const v of Object.values(jsonpData)) {
|
|
||||||
if (v.error) return error(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for completion
|
|
||||||
const dependencies = [...document.querySelectorAll("[data-jsonp-var]")].map(e => e.getAttribute("data-jsonp-var"))
|
|
||||||
for (const d of dependencies) {
|
|
||||||
if (!(d in jsonpData)) return
|
|
||||||
}
|
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
|
|
||||||
const uploadFraction = 0.7
|
|
||||||
|
|
||||||
const pkg = {
|
|
||||||
url: location.href,
|
|
||||||
init: {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
wikiname: BWData.wikiname,
|
|
||||||
path: BWData.path,
|
|
||||||
...jsonpData
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload progress
|
|
||||||
xhr.upload.addEventListener("progress", event => {
|
|
||||||
if (event.lengthComputable) {
|
|
||||||
progress.value = (event.loaded / event.total) * uploadFraction
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Download progress
|
|
||||||
xhr.addEventListener("progress", event => {
|
|
||||||
if (event.lengthComputable) {
|
|
||||||
progress.value = (event.loaded / event.total) * (1 - uploadFraction) + uploadFraction
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
xhr.addEventListener("load", () => {
|
|
||||||
console.log(xhr)
|
|
||||||
// check for errors
|
|
||||||
if (xhr.status === 500) return fetch(pkg.url, pkg.init).then(res => res.text()).then(error)
|
|
||||||
// check for captcha screen
|
|
||||||
if (xhr.responseXML.head.querySelector('script[src*="captcha.js"]')) return location.reload()
|
|
||||||
// page -> #content
|
|
||||||
const imported = document.importNode(xhr.responseXML.getElementById("content"), true)
|
|
||||||
document.getElementById("content").replaceWith(imported)
|
|
||||||
// page theme
|
|
||||||
// Try to make it a bit more accurate. Normally this is done server-side by sending a `theme=x` cookie with the parse request. But that's not possible in jsonp mode, and there's no equivalent URL query string to set the theme.
|
|
||||||
// Helps on Minecraft wiki. Might not be complete on some wikis.
|
|
||||||
cookieStore.get("theme").then(cookie => {
|
|
||||||
if (cookie && cookie.value !== "default") {
|
|
||||||
document.body.classList.remove("theme-fandomdesktop-light")
|
|
||||||
document.body.classList.remove("theme-fandomdesktop-dark")
|
|
||||||
document.body.removeAttribute("data-theme")
|
|
||||||
document.body.classList.add(`theme-fandomdesktop-${cookie.value}`)
|
|
||||||
document.body.setAttribute("data-theme", cookie.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// <head>
|
|
||||||
document.title = xhr.responseXML.title
|
|
||||||
document.body.className = xhr.responseXML.body.className
|
|
||||||
for (const e of xhr.responseXML.head.children) {
|
|
||||||
const alreadyImported = [...document.querySelectorAll("link[href]")].map(e => e.href)
|
|
||||||
if (e.tagName === "LINK" && !alreadyImported.includes(e.href)) {
|
|
||||||
const imported = document.importNode(e, true)
|
|
||||||
document.head.appendChild(imported)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// scroll
|
|
||||||
if (location.hash) {
|
|
||||||
document.getElementById(location.hash.slice(1)).scrollIntoView({behavior: "instant"})
|
|
||||||
}
|
|
||||||
// redirects
|
|
||||||
const redirectTo = document.querySelector("#content .redirectMsg a")
|
|
||||||
if (redirectTo) {
|
|
||||||
redirectTo.click()
|
|
||||||
}
|
|
||||||
loaded.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
xhr.open(pkg.init.method, pkg.url)
|
|
||||||
xhr.responseType = "document"
|
|
||||||
xhr.send(pkg.init.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
function error(data) {
|
|
||||||
const eContent = document.getElementById("content")
|
|
||||||
while (eContent.childNodes[0] !== undefined) eContent.childNodes[0].remove() // clear out loading indicators
|
|
||||||
document.title = `Error | BreezeWiki`
|
|
||||||
if (typeof data === "string") {
|
|
||||||
render(html`<p><strong>BreezeWiki ran into an error on this page.</strong></p><p>Try reloading the page.</p><p>If this keeps happening, you could <a href="mailto:~cadence/breezewiki-discuss@lists.sr.ht">send a public bug report</a>. Please include the following information:</p><pre>URL: ${window.location.href}${"\n"}${data}</pre>`, eContent)
|
|
||||||
} else if (data.error.code === "missingtitle") {
|
|
||||||
render(html`<p><strong>This page doesn't exist on Fandom.</strong></p><p><small><a href="/${BWData.wikiname}/wiki/Main_Page">Return to the wiki's main page</a></small></p>`, eContent)
|
|
||||||
} else {
|
|
||||||
render(html`<p>BreezeWiki wasn't able to load this page.</p><p><strong>${data.error.code}: ${data.error.info}</strong></p>`, eContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -120,9 +120,6 @@ p {
|
||||||
background-color: var(--theme-page-background-color);
|
background-color: var(--theme-page-background-color);
|
||||||
padding: 3vw;
|
padding: 3vw;
|
||||||
}
|
}
|
||||||
.fandom-community-header__background {
|
|
||||||
transform: none; /* fandom offsets this 46px by default due to their position: fixed top bar */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* table of contents */
|
/* table of contents */
|
||||||
.toc {
|
.toc {
|
||||||
|
|
@ -177,13 +174,13 @@ img {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* indicate wikipedia links */
|
/* indicate wikipedia links */
|
||||||
svg.external {
|
.extiw::after, .external::after {
|
||||||
vertical-align: super;
|
|
||||||
}
|
|
||||||
.external[href*="wikipedia.org"]::after, .extiw[href*="wikipedia.org"]::after {
|
|
||||||
vertical-align: super;
|
vertical-align: super;
|
||||||
|
content: "[🡕]";
|
||||||
font-family: serif;
|
font-family: serif;
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
.extiw[href*="wikipedia.org"]::after {
|
||||||
content: "[W]";
|
content: "[W]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,11 +202,11 @@ figcaption, .lightbox-caption, .thumbcaption {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* show tabs if tabs.js isn't loaded */
|
/* show tabs always */
|
||||||
.bw-tabs-nojs .wds-tabs__wrapper {
|
.wds-tabs__wrapper {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.bw-tabs-nojs .wds-tab__content {
|
.wds-tab__content {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,6 +288,7 @@ a.ext-audiobutton { /* see hearthstone/wiki/Diablo_(Duels_hero) */
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background: white;
|
background: white;
|
||||||
|
color: black;
|
||||||
border: solid #808080;
|
border: solid #808080;
|
||||||
border-width: 0px 1px 1px;
|
border-width: 0px 1px 1px;
|
||||||
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
|
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
|
@ -327,7 +325,6 @@ a.ext-audiobutton { /* see hearthstone/wiki/Diablo_(Duels_hero) */
|
||||||
border: none;
|
border: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
color: black;
|
|
||||||
background: none;
|
background: none;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -434,25 +431,6 @@ a.ext-audiobutton { /* see hearthstone/wiki/Diablo_(Duels_hero) */
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
/* more compact notice after it's been seen the first time */
|
|
||||||
.niwa--seen {
|
|
||||||
padding: 1.5vw 2vw 2vw;
|
|
||||||
overflow-y: auto;
|
|
||||||
max-height: min(280px, 33vh);
|
|
||||||
font-size: 17px;
|
|
||||||
margin-top: -2vw;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.niwa--seen .niwa__header {
|
|
||||||
font-size: 26px;
|
|
||||||
}
|
|
||||||
.niwa--seen .niwa__go {
|
|
||||||
padding: 10px 18px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
.niwa--seen .niwa__got-it {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* media queries */
|
/* media queries */
|
||||||
|
|
||||||
|
|
@ -503,10 +481,6 @@ a.ext-audiobutton { /* see hearthstone/wiki/Diablo_(Duels_hero) */
|
||||||
.page-title {
|
.page-title {
|
||||||
color: var(--theme-body-text-color);
|
color: var(--theme-body-text-color);
|
||||||
}
|
}
|
||||||
/* don't squeeze text alongside infoboxes */
|
|
||||||
.infobox, .portable-infobox {
|
|
||||||
float: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* *****
|
/* *****
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,3 @@ User-Agent: *
|
||||||
Disallow: /*/wiki/*
|
Disallow: /*/wiki/*
|
||||||
Disallow: /proxy
|
Disallow: /proxy
|
||||||
Disallow: /set-user-settings
|
Disallow: /set-user-settings
|
||||||
Disallow: /captcha
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {h, htm, render, signal, computed, effect, useSignalEffect} from "preact"
|
import {h, htm, render, signal, computed, effect} from "./preact.js"
|
||||||
const html = htm.bind(h)
|
const html = htm.bind(h)
|
||||||
const classNames = classArr => classArr.filter(el => el).join(" ")
|
const classNames = classArr => classArr.filter(el => el).join(" ")
|
||||||
|
|
||||||
|
|
@ -13,7 +13,6 @@ const query = signal("")
|
||||||
const focus = signal(false)
|
const focus = signal(false)
|
||||||
const st = signal("ready")
|
const st = signal("ready")
|
||||||
const suggestions = signal([])
|
const suggestions = signal([])
|
||||||
const enterWasLastKey = signal(false)
|
|
||||||
|
|
||||||
// processing functions
|
// processing functions
|
||||||
|
|
||||||
|
|
@ -50,25 +49,13 @@ function acceptSuggestion(hit) {
|
||||||
// suggestion list view
|
// suggestion list view
|
||||||
|
|
||||||
function Suggestion(hit) {
|
function Suggestion(hit) {
|
||||||
useSignalEffect(() => {
|
|
||||||
if (enterWasLastKey.value && st.value === "ready") {
|
|
||||||
enterWasLastKey.value.preventDefault()
|
|
||||||
acceptSuggestion(hit)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return html`<li class="bw-ss__item"><button type="button" class="bw-ss__button" onClick=${() => acceptSuggestion(hit)}>${hit.title}</button></li>`
|
return html`<li class="bw-ss__item"><button type="button" class="bw-ss__button" onClick=${() => acceptSuggestion(hit)}>${hit.title}</button></li>`
|
||||||
}
|
}
|
||||||
|
|
||||||
function DefaultSearch({q}) {
|
|
||||||
if (!q) return ""
|
|
||||||
return html`<li class="bw-ss__item"><button type="submit" class="bw-ss__button">Results for "${q}"</button></li>`
|
|
||||||
}
|
|
||||||
|
|
||||||
function SuggestionList() {
|
function SuggestionList() {
|
||||||
return html`
|
return html`
|
||||||
<ul class=${classNames(["bw-ss__list", focus.value && "bw-ss__list--focus", `bw-ss__list--${st.value}`])}>
|
<ul class=${classNames(["bw-ss__list", focus.value && "bw-ss__list--focus", `bw-ss__list--${st.value}`])}>
|
||||||
${suggestions.value.map(hit => html`<${Suggestion} ...${hit} />`)}
|
${suggestions.value.map(hit => html`<${Suggestion} ...${hit} />`)}
|
||||||
<${DefaultSearch} q=${query.value} />
|
|
||||||
</ul>`
|
</ul>`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,15 +79,9 @@ window.addEventListener("pageshow", () => {
|
||||||
st.value = "ready" // unlock results from changing after returning to page
|
st.value = "ready" // unlock results from changing after returning to page
|
||||||
})
|
})
|
||||||
|
|
||||||
effect(() => {
|
|
||||||
if (enterWasLastKey.value && (st.value === "loading" || st.value === "accepted")) {
|
|
||||||
enterWasLastKey.value.preventDefault() // wait for results before going
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function SuggestionInput() {
|
function SuggestionInput() {
|
||||||
return html`
|
return html`
|
||||||
<input type="text" name="q" id="bw-search-input" autocomplete="off" onInput=${e => query.value = e.target.value} onKeyDown=${e => enterWasLastKey.value = e.key === "Enter" && e} value=${query.value} class=${classNames(["bw-ss__input", `bw-ss__input--${st.value}`])} />`
|
<input type="text" name="q" id="bw-search-input" autocomplete="off" onInput=${e => query.value = e.target.value} value=${query.value} class=${classNames(["bw-ss__input", `bw-ss__input--${st.value}`])} />`
|
||||||
}
|
}
|
||||||
|
|
||||||
render(html`<${SuggestionInput} />`, eInput)
|
render(html`<${SuggestionInput} />`, eInput)
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import {effect} from "preact"
|
|
||||||
import {loaded} from "jsonp"
|
|
||||||
|
|
||||||
const tabFromHash = location.hash.length > 1 ? location.hash.substring(1) : null
|
|
||||||
|
|
||||||
function setUpAllTabs() {
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!BWData.jsonp) {
|
|
||||||
setUpAllTabs()
|
|
||||||
} else if (loaded.value) {
|
|
||||||
setUpAllTabs()
|
|
||||||
} else {
|
|
||||||
effect(() => {
|
|
||||||
if (loaded.value) {
|
|
||||||
setUpAllTabs()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.classList.remove("bw-tabs-nojs")
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue