forked from cadence/breezewiki
Compare commits
30 commits
76515dc2fb
...
95418613e5
Author | SHA1 | Date | |
---|---|---|---|
95418613e5 | |||
f216a1996a | |||
99b5d8d8f6 | |||
9fd2b4699d | |||
ba3b39242c | |||
a661ddb313 | |||
50a3bc819a | |||
9afccbb9cd | |||
92591a5eab | |||
1c83c0b4d3 | |||
324e34eb72 | |||
bf6efde979 | |||
ef12faf72d | |||
8c7a045830 | |||
565d0a439a | |||
cea9d2082b | |||
ac17f40329 | |||
b0e90f5cf9 | |||
02819a7459 | |||
aab52bd92b | |||
1219334d06 | |||
e812f2082c | |||
645fe1beee | |||
15b41c24f7 | |||
63d37d5e4f | |||
d3c5498d47 | |||
8b200d621a | |||
3c7a2f8453 | |||
d8d4e4375e | |||
bf055836cc |
35 changed files with 1196 additions and 438 deletions
|
@ -17,6 +17,7 @@
|
|||
(require-reloadable "src/page-proxy.rkt" page-proxy)
|
||||
(require-reloadable "src/page-redirect-wiki-home.rkt" redirect-wiki-home)
|
||||
(require-reloadable "src/page-search.rkt" page-search)
|
||||
(require-reloadable "src/page-set-user-settings.rkt" page-set-user-settings)
|
||||
(require-reloadable "src/page-static.rkt" static-dispatcher)
|
||||
(require-reloadable "src/page-subdomain.rkt" subdomain-dispatcher)
|
||||
(require-reloadable "src/page-wiki.rkt" page-wiki)
|
||||
|
@ -40,6 +41,7 @@
|
|||
page-not-found
|
||||
page-proxy
|
||||
page-search
|
||||
page-set-user-settings
|
||||
page-wiki
|
||||
page-file
|
||||
redirect-wiki-home
|
||||
|
|
2
dist.rkt
2
dist.rkt
|
@ -11,6 +11,7 @@
|
|||
(require (only-in "src/page-proxy.rkt" page-proxy))
|
||||
(require (only-in "src/page-redirect-wiki-home.rkt" redirect-wiki-home))
|
||||
(require (only-in "src/page-search.rkt" page-search))
|
||||
(require (only-in "src/page-set-user-settings.rkt" page-set-user-settings))
|
||||
(require (only-in "src/page-static.rkt" static-dispatcher))
|
||||
(require (only-in "src/page-subdomain.rkt" subdomain-dispatcher))
|
||||
(require (only-in "src/page-wiki.rkt" page-wiki))
|
||||
|
@ -29,6 +30,7 @@
|
|||
page-not-found
|
||||
page-proxy
|
||||
page-search
|
||||
page-set-user-settings
|
||||
page-wiki
|
||||
page-file
|
||||
redirect-wiki-home
|
||||
|
|
2
info.rkt
2
info.rkt
|
@ -1,3 +1,3 @@
|
|||
#lang info
|
||||
|
||||
(define build-deps '("rackunit-lib" "web-server-lib" "http-easy-lib" "html-parsing" "html-writing" "json-pointer" "ini-lib" "memo"))
|
||||
(define build-deps '("rackunit-lib" "web-server-lib" "http-easy-lib" "html-parsing" "html-writing" "json-pointer" "ini-lib" "memo" "net-cookies-lib"))
|
||||
|
|
45
misc/download-wiki-names.rkt
Normal file
45
misc/download-wiki-names.rkt
Normal file
|
@ -0,0 +1,45 @@
|
|||
#lang racket/base
|
||||
(require racket/generator
|
||||
racket/list
|
||||
racket/string
|
||||
json
|
||||
net/http-easy
|
||||
html-parsing
|
||||
"../src/xexpr-utils.rkt"
|
||||
"../src/url-utils.rkt")
|
||||
|
||||
(define output-file "wiki-names.json")
|
||||
(define limit "5000")
|
||||
|
||||
(define (get-page offset)
|
||||
(define res (get (format "https://community.fandom.com/wiki/Special:NewWikis?~a"
|
||||
(params->query `(("offset" . ,offset)
|
||||
("limit" . ,limit))))))
|
||||
(html->xexp (bytes->string/utf-8 (response-body res))))
|
||||
|
||||
(define (convert-list-items gen)
|
||||
(for/list ([item (in-producer gen #f)])
|
||||
; '(li "\n" "\t" (a (@ (href "http://terra-hexalis.fandom.com/")) "Terra Hexalis Wiki") "\n" "\t\t\ten\t")
|
||||
(hasheq 'title (third (fourth item))
|
||||
'link (second (second (second (fourth item))))
|
||||
'lang (string-trim (sixth item)))))
|
||||
|
||||
(define (get-items-recursive [offset ""] [items null])
|
||||
(define page (get-page offset))
|
||||
(define page-content ((query-selector (attribute-selector 'class "mw-spcontent") page)))
|
||||
(define next ((query-selector (attribute-selector 'class "mw-nextlink") page-content)))
|
||||
(define next-offset
|
||||
(if next
|
||||
(second (regexp-match #rx"offset=([0-9]*)" (get-attribute 'href (bits->attributes next))))
|
||||
#f))
|
||||
(define list-item-generator (query-selector (λ (e a c) (eq? e 'li)) page-content))
|
||||
(define these-items (convert-list-items list-item-generator))
|
||||
(define all-items (append items these-items))
|
||||
(printf "page offset \"~a\" has ~a items (~a so far)~n" offset (length these-items) (length all-items))
|
||||
(if next
|
||||
(get-items-recursive next-offset all-items)
|
||||
all-items))
|
||||
|
||||
(call-with-output-file output-file #:exists 'truncate/replace
|
||||
(λ (out)
|
||||
(write-json (get-items-recursive) out)))
|
|
@ -1,11 +1,18 @@
|
|||
#lang racket/base
|
||||
(require racket/string
|
||||
(require racket/file
|
||||
racket/list
|
||||
racket/runtime-path
|
||||
racket/string
|
||||
json
|
||||
(prefix-in easy: net/http-easy)
|
||||
html-parsing
|
||||
html-writing
|
||||
web-server/http
|
||||
"config.rkt"
|
||||
"data.rkt"
|
||||
"niwa-data.rkt"
|
||||
"static-data.rkt"
|
||||
"pure-utils.rkt"
|
||||
"xexpr-utils.rkt"
|
||||
"url-utils.rkt")
|
||||
|
||||
|
@ -23,18 +30,26 @@
|
|||
|
||||
(module+ test
|
||||
(require rackunit
|
||||
html-writing))
|
||||
html-writing
|
||||
"test-utils.rkt"))
|
||||
|
||||
(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))))
|
||||
(define timeouts (easy:make-timeout-config #:lease 5 #:connect 5))
|
||||
|
||||
(define-runtime-path path-static "../static")
|
||||
(define theme-icons
|
||||
(for/hasheq ([theme '(default light dark)])
|
||||
(values theme
|
||||
(html->xexp (file->string (build-path path-static (format "icon-theme-~a.svg" theme)) #:mode 'binary)))))
|
||||
|
||||
(define (application-footer source-url #:license [license-in #f])
|
||||
(define license (or license-in license-default))
|
||||
`(footer (@ (class "custom-footer"))
|
||||
(div (@ (class ,(if source-url "custom-footer__cols" "internal-footer")))
|
||||
(div (p
|
||||
(img (@ (class "my-logo") (src "/static/breezewiki.svg"))))
|
||||
(img (@ (class "my-logo") (src ,(get-static-url "breezewiki.svg")))))
|
||||
(p
|
||||
(a (@ (href "https://gitdab.com/cadence/breezewiki"))
|
||||
,(format "~a source code" (config-get 'application_name))))
|
||||
|
@ -62,69 +77,123 @@
|
|||
" Media files and official Fandom documents have different copying restrictions.")
|
||||
(p ,(format "Fandom is a trademark of Fandom, Inc. ~a is not affiliated with Fandom." (config-get 'application_name))))))))
|
||||
|
||||
;; 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
|
||||
(define (niwa-notice wikiname title)
|
||||
(define ind (findf (λ (item) (member wikiname (first item))) niwa-data))
|
||||
(if ind
|
||||
(let* ([search-page (format "/Special:Search?~a"
|
||||
(params->query `(("search" . ,title)
|
||||
("go" . "Go"))))]
|
||||
[go (if (string-suffix? (third ind) "/")
|
||||
(regexp-replace #rx"/$" (third ind) (λ (_) search-page))
|
||||
(let* ([joiner (second (regexp-match #rx"/(w[^./]*)/" (third ind)))])
|
||||
(regexp-replace #rx"/w[^./]*/.*$" (third ind) (λ (_) (format "/~a~a" joiner search-page)))))])
|
||||
`(aside (@ (class "niwa__notice"))
|
||||
(h1 (@ (class "niwa__header")) ,(second ind) " has its own website separate from Fandom.")
|
||||
(a (@ (class "niwa__go") (href ,go)) "Read " ,title " on " ,(second ind) " →")
|
||||
(div (@ (class "niwa__cols"))
|
||||
(div (@ (class "niwa__left"))
|
||||
(p "Most major Nintendo wikis are part of the "
|
||||
(a (@ (href "https://www.niwanetwork.org/about/")) "Nintendo Independent Wiki Alliance")
|
||||
" and have their own wikis off Fandom. You can help this wiki by "
|
||||
(a (@ (href ,go)) "visiting it directly."))
|
||||
(p ,(fifth ind))
|
||||
(div (@ (class "niwa__divider")))
|
||||
(p "Why are you seeing this message? Fandom refuses to delete or archive their copy of this wiki, so that means their pages will appear high up in search results. Fandom hopes to get clicks from readers who don't know any better.")
|
||||
(p (@ (class "niwa__feedback")) "This notice brought to you by BreezeWiki / " (a (@ (href "https://www.kotaku.com.au/2022/10/massive-zelda-wiki-reclaims-independence-six-months-before-tears-of-the-kingdom/")) "Info & Context") " / " (a (@ (href "https://docs.breezewiki.com/Reporting_Bugs.html")) "Feedback?")))
|
||||
(div (@ (class "niwa__right"))
|
||||
(img (@ (class "niwa__logo") (src ,(format "https://www.niwanetwork.org~a" (fourth ind)))))))))
|
||||
""))
|
||||
|
||||
(define (generate-wiki-page
|
||||
content
|
||||
#:req req
|
||||
#:source-url source-url
|
||||
#:wikiname wikiname
|
||||
#:title title
|
||||
#:body-class [body-class-in #f]
|
||||
#:siteinfo [siteinfo-in #f])
|
||||
#:head-data [head-data-in #f]
|
||||
#:siteinfo [siteinfo-in #f]
|
||||
#:user-cookies [user-cookies-in #f])
|
||||
(define siteinfo (or siteinfo-in siteinfo-default))
|
||||
(define body-class (if (not body-class-in)
|
||||
"skin-fandomdesktop"
|
||||
body-class-in))
|
||||
(define head-data (or head-data-in ((head-data-getter wikiname))))
|
||||
(define user-cookies (or user-cookies-in (user-cookies-getter req)))
|
||||
(define (required-styles origin)
|
||||
(map (λ (dest-path)
|
||||
(define url (format dest-path origin))
|
||||
(if (config-true? 'strict_proxy)
|
||||
(u-proxy-url url)
|
||||
url))
|
||||
'(#;"~a/load.php?lang=en&modules=skin.fandomdesktop.styles&only=styles&skin=fandomdesktop"
|
||||
`(#;"~a/load.php?lang=en&modules=skin.fandomdesktop.styles&only=styles&skin=fandomdesktop"
|
||||
#;"~a/load.php?lang=en&modules=ext.gadget.dungeonsWiki%2CearthWiki%2Csite-styles%2Csound-styles&only=styles&skin=fandomdesktop"
|
||||
#;"~a/load.php?lang=en&modules=site.styles&only=styles&skin=fandomdesktop"
|
||||
; combine the above entries into a single request for potentially extra speed - fandom.com doesn't even do this!
|
||||
"~a/wikia.php?controller=ThemeApi&method=themeVariables"
|
||||
,(format "~~a/wikia.php?controller=ThemeApi&method=themeVariables&variant=~a" (user-cookies^-theme user-cookies))
|
||||
"~a/load.php?lang=en&modules=skin.fandomdesktop.styles%7Cext.fandom.PortableInfoboxFandomDesktop.css%7Cext.fandom.GlobalComponents.CommunityHeaderBackground.css%7Cext.gadget.site-styles%2Csound-styles%7Csite.styles&only=styles&skin=fandomdesktop")))
|
||||
`(html
|
||||
(head
|
||||
(meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))
|
||||
(title ,(format "~a | ~a+~a"
|
||||
title
|
||||
(regexp-replace #rx" ?Wiki$" (siteinfo^-sitename siteinfo) "")
|
||||
(config-get 'application_name)))
|
||||
,@(map (λ (url)
|
||||
`(link (@ (rel "stylesheet") (type "text/css") (href ,url))))
|
||||
(required-styles (format "https://~a.fandom.com" wikiname)))
|
||||
(link (@ (rel "stylesheet") (type "text/css") (href "/static/main.css")))
|
||||
(script "const BWData = "
|
||||
,(jsexpr->string (hasheq 'wikiname wikiname
|
||||
'strict_proxy (config-true? 'strict_proxy))))
|
||||
,(if (config-true? 'feature_search_suggestions)
|
||||
'(script (@ (type "module") (src "/static/search-suggestions.js")))
|
||||
""))
|
||||
(body (@ (class ,body-class))
|
||||
(div (@ (class "main-container"))
|
||||
(div (@ (class "fandom-community-header__background tileHorizontally header")))
|
||||
(div (@ (class "page"))
|
||||
(main (@ (class "page__main"))
|
||||
(div (@ (class "custom-top"))
|
||||
(h1 (@ (class "page-title")) ,title)
|
||||
(nav (@ (class "sitesearch"))
|
||||
(form (@ (action ,(format "/~a/search" wikiname))
|
||||
(class "bw-search-form")
|
||||
(id "bw-pr-search"))
|
||||
(label (@ (for "bw-search-input")) "Search ")
|
||||
(input (@ (type "text") (name "q") (id "bw-search-input") (autocomplete "off")))
|
||||
(div (@ (class "bw-ss__container"))))))
|
||||
(div (@ (id "content") #;(class "page-content"))
|
||||
(div (@ (id "mw-content-text"))
|
||||
,content))
|
||||
,(application-footer source-url #:license (siteinfo^-license siteinfo))))))))
|
||||
`(*TOP*
|
||||
(*DECL* DOCTYPE html)
|
||||
(html
|
||||
(head
|
||||
(meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))
|
||||
(title ,(format "~a | ~a+~a"
|
||||
title
|
||||
(regexp-replace #rx" ?Wiki$" (siteinfo^-sitename siteinfo) "")
|
||||
(config-get 'application_name)))
|
||||
,@(map (λ (url)
|
||||
`(link (@ (rel "stylesheet") (type "text/css") (href ,url))))
|
||||
(required-styles (format "https://~a.fandom.com" wikiname)))
|
||||
(link (@ (rel "stylesheet") (type "text/css") (href ,(get-static-url "main.css"))))
|
||||
(script "const BWData = "
|
||||
,(jsexpr->string (hasheq 'wikiname wikiname
|
||||
'strict_proxy (config-true? 'strict_proxy))))
|
||||
,(if (config-true? 'feature_search_suggestions)
|
||||
`(script (@ (type "module") (src ,(get-static-url "search-suggestions.js"))))
|
||||
"")
|
||||
(script (@ (type "module") (src ,(get-static-url "countdown.js"))))
|
||||
(link (@ (rel "icon") (href ,(u (λ (v) (config-true? 'strict_proxy))
|
||||
(λ (v) (u-proxy-url v))
|
||||
(head-data^-icon-url head-data))))))
|
||||
(body (@ (class ,(head-data^-body-class head-data)))
|
||||
(div (@ (class "main-container"))
|
||||
(div (@ (class "fandom-community-header__background tileHorizontally header")))
|
||||
(div (@ (class "page"))
|
||||
(main (@ (class "page__main"))
|
||||
,(niwa-notice wikiname title)
|
||||
(div (@ (class "custom-top"))
|
||||
(h1 (@ (class "page-title")) ,title)
|
||||
(nav (@ (class "sitesearch"))
|
||||
(form (@ (action ,(format "/~a/search" wikiname))
|
||||
(class "bw-search-form")
|
||||
(id "bw-pr-search-form"))
|
||||
(label (@ (for "bw-search-input")) "Search ")
|
||||
(div (@ (id "bw-pr-search-input"))
|
||||
(input (@ (type "text") (name "q") (id "bw-search-input") (autocomplete "off"))))
|
||||
(div (@ (class "bw-ss__container") (id "bw-pr-search-suggestions"))))
|
||||
(div (@ (class "bw-theme__select"))
|
||||
(span (@ (class "bw-theme__main-label")) "Page theme")
|
||||
(div (@ (class "bw-theme__items"))
|
||||
,@(for/list ([theme '(default light dark)])
|
||||
(define class
|
||||
(if (equal? theme (user-cookies^-theme user-cookies))
|
||||
"bw-theme__item bw-theme__item--selected"
|
||||
"bw-theme__item"))
|
||||
`(a (@ (href ,(user-cookies-setter-url
|
||||
req
|
||||
(struct-copy user-cookies^ user-cookies
|
||||
[theme theme]))) (class ,class))
|
||||
(div (@ (class "bw-theme__icon-container"))
|
||||
,(hash-ref theme-icons theme))
|
||||
,(format "~a" theme)))))))
|
||||
(div (@ (id "content") #;(class "page-content"))
|
||||
(div (@ (id "mw-content-text"))
|
||||
,content))
|
||||
,(application-footer source-url #:license (siteinfo^-license siteinfo)))))))))
|
||||
(module+ test
|
||||
(define page
|
||||
(parameterize ([(config-parameter 'strict_proxy) "true"])
|
||||
(generate-wiki-page
|
||||
'(template)
|
||||
#:req test-req
|
||||
#:source-url ""
|
||||
#:title "test"
|
||||
#:wikiname "test")))
|
||||
|
@ -139,11 +208,11 @@
|
|||
page))))
|
||||
"/proxy?dest=https%3A%2F%2Ftest.fandom.com")))
|
||||
|
||||
(define (generate-redirect dest)
|
||||
(define (generate-redirect dest #:headers [headers-in '()])
|
||||
(define dest-bytes (string->bytes/utf-8 dest))
|
||||
(response/output
|
||||
#:code 302
|
||||
#:headers (list (header #"Location" dest-bytes))
|
||||
#:headers (append (list (header #"Location" dest-bytes)) headers-in)
|
||||
(λ (out)
|
||||
(write-html
|
||||
`(html
|
||||
|
|
61
src/data.rkt
61
src/data.rkt
|
@ -1,22 +1,37 @@
|
|||
#lang racket/base
|
||||
(require racket/list
|
||||
racket/match
|
||||
web-server/http/request-structs
|
||||
net/url-string
|
||||
(only-in net/cookies/server cookie-header->alist cookie->set-cookie-header make-cookie)
|
||||
(prefix-in easy: net/http-easy)
|
||||
memo
|
||||
"static-data.rkt"
|
||||
"url-utils.rkt"
|
||||
"xexpr-utils.rkt")
|
||||
|
||||
(provide
|
||||
(struct-out siteinfo^)
|
||||
(struct-out license^)
|
||||
(struct-out head-data^)
|
||||
(struct-out user-cookies^)
|
||||
siteinfo-fetch
|
||||
siteinfo-default
|
||||
license-default)
|
||||
license-default
|
||||
head-data-getter
|
||||
head-data-default
|
||||
user-cookies-getter
|
||||
user-cookies-default
|
||||
user-cookies-setter
|
||||
user-cookies-setter-url)
|
||||
|
||||
(struct siteinfo^ (sitename basepage license) #:transparent)
|
||||
(struct license^ (text url) #:transparent)
|
||||
(struct head-data^ (body-class icon-url) #:transparent)
|
||||
|
||||
(define license-default (license^ "CC-BY-SA" "https://www.fandom.com/licensing"))
|
||||
(define siteinfo-default (siteinfo^ "Test Wiki" "Main_Page" license-default))
|
||||
(define head-data-default (head-data^ "skin-fandomdesktop" (get-static-url "breezewiki-favicon.svg")))
|
||||
|
||||
(define/memoize (siteinfo-fetch wikiname) #:hash hash
|
||||
(define dest-url
|
||||
|
@ -34,3 +49,47 @@
|
|||
(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
|
||||
;; data will be stored here, can be referenced by the memoized closure
|
||||
(define this-data head-data-default)
|
||||
;; returns the getter
|
||||
(λ ([res-in #f])
|
||||
(when res-in
|
||||
;; when actual information is provided, parse it into the struct and store it for the future
|
||||
(define head-html (jp "/parse/headhtml" res-in ""))
|
||||
(define data
|
||||
(head-data^
|
||||
(match (regexp-match #rx"<body [^>]*class=\"([^\"]*)" head-html)
|
||||
[(list _ classes) classes]
|
||||
[_ (head-data^-body-class head-data-default)])
|
||||
(match (regexp-match #rx"<link rel=\"(?:shortcut )?icon\" href=\"([^\"]*)" head-html)
|
||||
[(list _ icon-url) icon-url]
|
||||
[_ (head-data^-icon-url head-data-default)])))
|
||||
(set! this-data data))
|
||||
;; then no matter what, return the best information we have so far
|
||||
this-data))
|
||||
|
||||
(struct user-cookies^ (theme) #:prefab)
|
||||
(define user-cookies-default (user-cookies^ 'default))
|
||||
(define (user-cookies-getter 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-hash
|
||||
(for/hasheq ([pair cookies-alist])
|
||||
(match pair
|
||||
[(cons "theme" (and theme (or "light" "dark" "default")))
|
||||
(values 'theme (string->symbol theme))]
|
||||
[_ (values #f #f)])))
|
||||
(user-cookies^
|
||||
(hash-ref cookies-hash 'theme (user-cookies^-theme user-cookies-default))))
|
||||
|
||||
(define (user-cookies-setter user-cookies)
|
||||
(map (λ (c) (header #"Set-Cookie" (cookie->set-cookie-header c)))
|
||||
(list (make-cookie "theme" (symbol->string (user-cookies^-theme user-cookies))
|
||||
#:path "/"
|
||||
#:max-age (* 60 60 24 365 10)))))
|
||||
|
||||
(define (user-cookies-setter-url req new-settings)
|
||||
(format "/set-user-settings?~a" (params->query `(("next_location" . ,(url->string (request-uri req)))
|
||||
("new_settings" . ,(format "~a" new-settings))))))
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
(for-syntax racket/base)
|
||||
racket/string
|
||||
net/url
|
||||
web-server/http
|
||||
(prefix-in host: web-server/dispatchers/dispatch-host)
|
||||
(prefix-in pathprocedure: web-server/dispatchers/dispatch-pathprocedure)
|
||||
(prefix-in sequencer: web-server/dispatchers/dispatch-sequencer)
|
||||
|
@ -32,23 +33,18 @@
|
|||
(datum->syntax stx `(make-dispatcher-tree ,ds)))
|
||||
|
||||
(define (make-dispatcher-tree ds)
|
||||
(host:make
|
||||
(λ (host-sym)
|
||||
(if/out (config-true? 'canonical_origin)
|
||||
(let* ([host-header (symbol->string host-sym)]
|
||||
[splitter (string-append "." (url-host (string->url (config-get 'canonical_origin))))]
|
||||
[s (string-split host-header splitter #:trim? #f)])
|
||||
(if/in (and (eq? 2 (length s)) (equal? "" (cadr s)))
|
||||
((hash-ref ds 'subdomain-dispatcher) (car s))))
|
||||
(sequencer:make
|
||||
(pathprocedure:make "/" (hash-ref ds 'page-home))
|
||||
(pathprocedure:make "/proxy" (hash-ref ds 'page-proxy))
|
||||
(pathprocedure:make "/search" (hash-ref ds 'page-global-search))
|
||||
(pathprocedure:make "/buddyfight/wiki/It_Doesn't_Work!!" (hash-ref ds 'page-it-works))
|
||||
(filter:make (pregexp (format "^/~a/wiki/Category:.+$" px-wikiname)) (lift:make (hash-ref ds 'page-category)))
|
||||
(filter:make (pregexp (format "^/~a/wiki/File:.+$" px-wikiname)) (lift:make (hash-ref ds 'page-file)))
|
||||
(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 (hash-ref ds 'page-search)))
|
||||
(filter:make (pregexp (format "^/~a(/(wiki(/)?)?)?$" px-wikiname)) (lift:make (hash-ref ds 'redirect-wiki-home)))
|
||||
(hash-ref ds 'static-dispatcher)
|
||||
(lift:make (hash-ref ds 'page-not-found)))))))
|
||||
(define subdomain-dispatcher (hash-ref ds 'subdomain-dispatcher))
|
||||
(sequencer:make
|
||||
subdomain-dispatcher
|
||||
(pathprocedure:make "/" (hash-ref ds 'page-home))
|
||||
(pathprocedure:make "/proxy" (hash-ref ds 'page-proxy))
|
||||
(pathprocedure:make "/search" (hash-ref ds 'page-global-search))
|
||||
(pathprocedure:make "/set-user-settings" (hash-ref ds 'page-set-user-settings))
|
||||
(pathprocedure:make "/buddyfight/wiki/It_Doesn't_Work!!" (hash-ref ds 'page-it-works))
|
||||
(filter:make (pregexp (format "^/~a/wiki/Category:.+$" px-wikiname)) (lift:make (hash-ref ds 'page-category)))
|
||||
(filter:make (pregexp (format "^/~a/wiki/File:.+$" px-wikiname)) (lift:make (hash-ref ds 'page-file)))
|
||||
(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 (hash-ref ds 'page-search)))
|
||||
(filter:make (pregexp (format "^/~a(/(wiki(/)?)?)?$" px-wikiname)) (lift:make (hash-ref ds 'redirect-wiki-home)))
|
||||
(hash-ref ds 'static-dispatcher)
|
||||
(lift:make (hash-ref ds 'page-not-found))))
|
||||
|
|
156
src/niwa-data.rkt
Normal file
156
src/niwa-data.rkt
Normal file
|
@ -0,0 +1,156 @@
|
|||
#lang racket/base
|
||||
|
||||
(provide
|
||||
niwa-data)
|
||||
|
||||
;; wikiname, niwa-name, url, logo-url
|
||||
(define niwa-data
|
||||
'((("arms" "armsgame")
|
||||
"ARMS Institute"
|
||||
"https://armswiki.org/wiki/Home"
|
||||
"/images/logos/armswiki.png"
|
||||
"ARMS Institute is a comprehensive resource for information about the Nintendo Switch game, ARMS. Founded on May 1, 2017 and growing rapidly, the wiki strives to offer in-depth coverage of ARMS from both a competitive and casual perspective. Join us and ARM yourself with knowledge!")
|
||||
(("pokemon" "monster")
|
||||
"Bulbapedia"
|
||||
"https://bulbapedia.bulbagarden.net/wiki/Main_Page"
|
||||
"/images/logos/bulbapedia.png"
|
||||
"A part of the Bulbagarden community, Bulbapedia was founded on December 21, 2004 by Liam Pomfret. Everything you need to know about Pokémon can be found at Bulbapedia, whether about the games, the anime, the manga, or something else entirely. With its Bulbanews section and the Bulbagarden forums, it's your one-stop online place for Pokémon.")
|
||||
(("dragalialost")
|
||||
"Dragalia Lost Wiki"
|
||||
"https://dragalialost.wiki/w/Dragalia_Lost_Wiki"
|
||||
"/images/logos/dragalialost.png"
|
||||
"The Dragalia Lost Wiki was originally founded in September 2018 on the Gamepedia platform but went independent in January 2021. The Wiki aims to document anything and everything Dragalia Lost, from in-game data to mechanics, story, guides, and more!")
|
||||
(("dragonquest")
|
||||
"Dragon Quest Wiki"
|
||||
"https://dragon-quest.org/wiki/Main_Page"
|
||||
"/images/logos/dragonquestwiki.png"
|
||||
"Originally founded on Wikia, the Dragon Quest Wiki was largely inactive until FlyingRagnar became an admin in late 2009. The wiki went independent about a year later when it merged with the Dragon Quest Dictionary/Encyclopedia which was run by Zenithian and supported by the Dragon's Den. The Dragon Quest Wiki aims to be the most complete resource for Dragon Quest information on the web. It continues to grow in the hope that one day the series will be as popular in the rest of the world as it is in Japan.")
|
||||
(("fireemblem")
|
||||
"Fire Emblem Wiki"
|
||||
"https://fireemblemwiki.org/wiki/Main_Page"
|
||||
"/images/logos/fireemblemwiki.png"
|
||||
"Growing since August 26, 2010, Fire Emblem Wiki is a project whose goal is to cover all information pertaining to the Fire Emblem series. It aspires to become the most complete and accurate independent source of information on this series.")
|
||||
(("fzero" "f-zero")
|
||||
"F-Zero Wiki"
|
||||
"https://mutecity.org/wiki/F-Zero_Wiki"
|
||||
"/images/logos/fzerowiki.png"
|
||||
"Founded on Wikia in November 2007, F-Zero Wiki became independent with NIWA's help in 2011. F-Zero Wiki is quickly growing into the Internet's definitive source for the world of 2200 km/h+, from pilots to machines, and is the founding part of MuteCity.org, the web's first major F-Zero community.")
|
||||
(("goldensun")
|
||||
"Golden Sun Universe"
|
||||
"https://www.goldensunwiki.net/wiki/Main_Page"
|
||||
"/images/logos/goldensununiverse.png"
|
||||
"Originally founded on Wikia in late 2006, Golden Sun Universe has always worked hard to meet one particular goal: to be the single most comprehensive yet accessible resource on the Internet for Nintendo's RPG series Golden Sun. It became an independent wiki four years later. Covering characters and plot, documenting all aspects of the gameplay, featuring walkthroughs both thorough and bare-bones, and packed with all manner of odd and fascinating minutiae, Golden Sun Universe leaves no stone unturned!")
|
||||
(("tetris")
|
||||
"Hard Drop - Tetris Wiki"
|
||||
"https://harddrop.com/wiki/Main_Page"
|
||||
"/images/logos/harddrop.png"
|
||||
"The Tetris Wiki was founded by Tetris fans for Tetris fans on tetrisconcept.com in March 2006. The Tetris Wiki torch was passed to harddrop.com in July 2009. Hard Drop is a Tetris community for all Tetris players, regardless of skill or what version of Tetris you play.")
|
||||
(("kidicarus")
|
||||
"Icaruspedia"
|
||||
"https://www.kidicaruswiki.org/wiki/Main_Page"
|
||||
"/images/logos/icaruspedia.png"
|
||||
"Icaruspedia is the Kid Icarus wiki that keeps flying to new heights. After going independent on January 8, 2012, Icaruspedia has worked to become the largest and most trusted independent source of Kid Icarus information. Just like Pit, they'll keep on fighting until the job is done.")
|
||||
(("splatoon" "uk-splatoon" "splatoon3" "splatoon2")
|
||||
"Inkipedia"
|
||||
"https://splatoonwiki.org/wiki/Main_Page"
|
||||
"/images/logos/inkipedia.png"
|
||||
"Inkipedia is your ever-growing go-to source for all things Splatoon related. Though founded on Wikia on June 10, 2014, Inkipedia went independent on May 18, 2015, just days before Splatoon's release. Our aim is to cover all aspects of the series, both high and low. Come splat with us now!")
|
||||
(("starfox")
|
||||
"Lylat Wiki"
|
||||
"https://starfoxwiki.info/wiki/Lylat_Wiki"
|
||||
"/images/logos/lylatwiki.png"
|
||||
"Out of seemingly nowhere, Lylat Wiki sprung up one day in early 2010. Led by creator, Justin Folvarcik, and project head, Tacopill, the wiki has reached stability since the move to its own domain. The staff of Lylat Wiki are glad to help out the NIWA wikis and are even prouder to join NIWA's ranks as the source for information on the Star Fox series.")
|
||||
(("metroid" "themetroid")
|
||||
"Metroid Wiki"
|
||||
"https://www.metroidwiki.org/wiki/Main_Page"
|
||||
"/images/logos/metroidwiki.png"
|
||||
"Metroid Wiki, founded on January 27, 2010 by Nathanial Rumphol-Janc and Zelda Informer, is a rapidly expanding wiki that covers everything Metroid, from the games, to every suit, vehicle and weapon.")
|
||||
(("nintendo" "nintendoseries" "nintendogames")
|
||||
"Nintendo Wiki"
|
||||
"http://niwanetwork.org/wiki/Main_Page"
|
||||
"/images/logos/nintendowiki.png"
|
||||
"Created on May 12, 2010, NintendoWiki (N-Wiki) is a collaborative project by the NIWA team to create an encyclopedia dedicated to Nintendo, being the company around which all other NIWA content is focused. It ranges from mainstream information such as the games and people who work for the company, to the most obscure info like patents and interesting trivia.")
|
||||
(("animalcrossing" "animalcrossingcf" "acnh")
|
||||
"Nookipedia"
|
||||
"https://nookipedia.com/wiki/Main_Page"
|
||||
"/images/logos/nookipedia.png"
|
||||
"Founded in August 2005 on Wikia, Nookipedia was originally known as Animal Crossing City. Shortly after its five-year anniversary, Animal Crossing City decided to merge with the independent Animal Crossing Wiki, which in January 2011 was renamed to Nookipedia. Covering everything from the series including characters, items, critters, and much more, Nookipedia is your number one resource for everything Animal Crossing!")
|
||||
(("pikmin")
|
||||
"Pikipedia"
|
||||
"https://www.pikminwiki.com/"
|
||||
"/images/logos/pikipedia.png"
|
||||
"Pikipedia, also known as Pikmin Wiki, was founded by Dark Lord Revan on Wikia in December 2005. In September 2010, with NIWA's help, Pikipedia moved away from Wikia to become independent. Pikipedia is working towards their goal of being the foremost source for everything Pikmin.")
|
||||
(("pikmin-fan" "pikpikpedia")
|
||||
"Pimkin Fanon"
|
||||
"https://www.pikminfanon.com/wiki/Main_Page"
|
||||
"/images/logos/pikifanon.png"
|
||||
"Pikmin Fanon is a Pikmin wiki for fan stories (fanon). Founded back on November 1, 2008 by Rocky0718 as a part of Wikia, Pikmin Fanon has been independent since September 14, 2010. Check them out for fan created stories based around the Pikmin series.")
|
||||
(("supersmashbros")
|
||||
"SmashWiki"
|
||||
"https://www.ssbwiki.com/"
|
||||
"/images/logos/smashwiki.png"
|
||||
"Originally two separate wikis (one on SmashBoards, the other on Wikia), SmashWiki as we know it was formed out of a merge on February 29th, 2008, becoming independent on September 28th, 2010. SmashWiki is the premier source of Smash Bros. information, from simple tidbits to detailed mechanics, and also touches on the origins of its wealth of content from its sibling franchises.")
|
||||
(("starfy")
|
||||
"Starfy Wiki"
|
||||
"https://www.starfywiki.org/wiki/Main_Page"
|
||||
"/images/logos/starfywiki.png"
|
||||
"Founded on May 30, 2009, Starfy Wiki's one goal is to become the best source on Nintendo's elusive game series The Legendary Starfy. After gaining independence in 2011 with the help of Tappy and the wiki's original administrative team, the wiki still hopes to achieve its goal and be the best source of Starfy info for all present and future fans.")
|
||||
(()
|
||||
"StrategyWiki"
|
||||
"https://www.strategywiki.org/wiki/Main_Page"
|
||||
"/images/logos/strategywiki.png"
|
||||
"StrategyWiki was founded in December 2005 by former member Brandon Suit with the idea that the existing strategy guides on the Internet could be improved. Three years later, in December 2008, Scott Jacobi officially established Abxy LLC for the purpose of owning and operating StrategyWiki as a community. Their vision is to bring free, collaborative video game strategy guides to the masses, including Nintendo franchise strategy guides.")
|
||||
(("mario" "themario" "imario" "supermarionintendo" "mariokart" "luigi-kart" "mario3")
|
||||
"Super Mario Wiki"
|
||||
"https://www.mariowiki.com/"
|
||||
"/images/logos/mariowiki.png"
|
||||
"Online since August 12, 2005, when it was founded by Steve Shinn, Super Mario Wiki has you covered for anything Mario, Donkey Kong, Wario, Luigi, Yoshi—the whole gang, in fact. With its own large community in its accompanying forum, Super Mario Wiki is not only a great encyclopedia, but a fansite for you to talk anything Mario.")
|
||||
(("mario64")
|
||||
"Ukikipedia"
|
||||
"https://ukikipedia.net/wiki/Main_Page"
|
||||
"/images/logos/ukikipedia.png"
|
||||
"Founded in 2018, Ukikipedia is a wiki focused on expert level knowledge of Super Mario 64, including detailed coverage of game mechanics, glitches, speedrunning, and challenges.")
|
||||
(("advancewars")
|
||||
"Wars Wiki"
|
||||
"https://www.warswiki.org/wiki/Main_Page"
|
||||
"/images/logos/warswiki.png"
|
||||
"Created in February 2009, Wars Wiki is a small wiki community with a large heart. Founded by JoJo and Wars Central, Wars Wiki is going strong on one of Nintendo's lesser known franchises. Wars Wiki is keen to contribute to NIWA, and we're proud to be able to support them. With the Wars Central community, including forums, it's definitely worth checking out.")
|
||||
(("earthbound")
|
||||
"WikiBound"
|
||||
"https://www.wikibound.info/wiki/WikiBound"
|
||||
"/images/logos/wikibound.png"
|
||||
"Founded in early 2010 by Tacopill, WikiBound strives to create a detailed database on the Mother/EarthBound games, a quaint series only having two games officially released outside of Japan. Help spread the PK Love by editing WikiBound!")
|
||||
(("kirby")
|
||||
"WiKirby"
|
||||
"https://wikirby.com/wiki/Kirby_Wiki"
|
||||
"/images/logos/wikirby.png"
|
||||
"WiKirby. It's a wiki. About Kirby! Amidst the excitement of NIWA being founded, Josh LeJeune decided to create a Kirby Wiki, due to lack of a strong independent one online. Coming online on January 24, 2010, WiKirby continues its strong launch with a dedicated community and a daily growing source of Kirby based knowledge.")
|
||||
(("xenoblade" "xenoseries" "xenogears" "xenosaga")
|
||||
"Xeno Series Wiki"
|
||||
"https://www.xenoserieswiki.org/wiki/Main_Page"
|
||||
"/images/logos/xenoserieswiki.png"
|
||||
"Xeno Series Wiki was created February 4, 2020 by Sir Teatei Moonlight. While founded by the desire to have an independent wiki for Xenoblade, there was an interest in including the Xenogears and Xenosaga games within its focus as well. This wide range of coverage means it's always in need of new editors to help bolster its many subjects.")
|
||||
(("zelda" "zelda-archive")
|
||||
"Zeldapedia"
|
||||
"https://zeldapedia.wiki/wiki/Main_Page"
|
||||
"/images/logos/zeldapedia.png"
|
||||
"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.")))
|
||||
|
||||
;; get the current dataset so it can be stored above
|
||||
(module+ fetch
|
||||
(require racket/generator
|
||||
racket/list
|
||||
net/http-easy
|
||||
html-parsing
|
||||
"xexpr-utils.rkt")
|
||||
(define r (get "https://www.niwanetwork.org/members/"))
|
||||
(define x (html->xexp (bytes->string/utf-8 (response-body r))))
|
||||
(define english ((query-selector (λ (e a c) (equal? (get-attribute 'id a) "content1")) x)))
|
||||
(define gen (query-selector (λ (e a c) (has-class? "member" a)) english))
|
||||
(for/list ([item (in-producer gen #f)])
|
||||
(define links (query-selector (λ (e a c) (eq? e 'a)) item))
|
||||
(define url (get-attribute 'href (bits->attributes (links))))
|
||||
(define title (third (links)))
|
||||
(define icon (get-attribute 'src (bits->attributes ((query-selector (λ (e a c) (eq? e 'img)) item)))))
|
||||
(define description (second ((query-selector (λ (e a c) (eq? e 'p)) item))))
|
||||
(list '() title url icon description)))
|
|
@ -24,24 +24,27 @@
|
|||
page-category)
|
||||
|
||||
(module+ test
|
||||
(require rackunit)
|
||||
(require rackunit
|
||||
"test-utils.rkt")
|
||||
(define category-json-data
|
||||
'#hasheq((batchcomplete . #t) (continue . #hasheq((cmcontinue . "page|4150504c45|41473") (continue . "-||"))) (query . #hasheq((categorymembers . (#hasheq((ns . 0) (pageid . 25049) (title . "Item (entity)")) #hasheq((ns . 0) (pageid . 128911) (title . "3D")) #hasheq((ns . 0) (pageid . 124018) (title . "A Very Fine Item")) #hasheq((ns . 0) (pageid . 142208) (title . "Amethyst Shard")) #hasheq((ns . 0) (pageid . 121612) (title . "Ankle Monitor")))))))))
|
||||
|
||||
(define (generate-results-page
|
||||
#:req req
|
||||
#:source-url source-url
|
||||
#:wikiname wikiname
|
||||
#:title title
|
||||
#:members-data members-data
|
||||
#:page page
|
||||
#:body-class [body-class #f]
|
||||
#:head-data [head-data #f]
|
||||
#:siteinfo [siteinfo #f])
|
||||
(define members (jp "/query/categorymembers" members-data))
|
||||
(generate-wiki-page
|
||||
#:req req
|
||||
#:source-url source-url
|
||||
#:wikiname wikiname
|
||||
#:title title
|
||||
#:body-class body-class
|
||||
#:head-data head-data
|
||||
#:siteinfo siteinfo
|
||||
`(div
|
||||
,(update-tree-wiki page wikiname)
|
||||
|
@ -52,7 +55,7 @@
|
|||
,@(map
|
||||
(λ (result)
|
||||
(define title (jp "/title" result))
|
||||
(define page-path (regexp-replace* #rx" " title "_"))
|
||||
(define page-path (page-title->path title))
|
||||
`(li
|
||||
(a (@ (href ,(format "/~a/wiki/~a" wikiname page-path)))
|
||||
,title)))
|
||||
|
@ -94,17 +97,15 @@
|
|||
(define title (preprocess-html-wiki (jp "/parse/title" page-data prefixed-category)))
|
||||
(define page-html (preprocess-html-wiki (jp "/parse/text" page-data "")))
|
||||
(define page (html->xexp page-html))
|
||||
(define head-html (jp "/parse/headhtml" page-data ""))
|
||||
(define body-class (match (regexp-match #rx"<body [^>]*class=\"([^\"]*)" head-html)
|
||||
[(list _ classes) classes]
|
||||
[_ ""]))
|
||||
(define head-data ((head-data-getter wikiname) page-data))
|
||||
(define body (generate-results-page
|
||||
#:req req
|
||||
#:source-url source-url
|
||||
#:wikiname wikiname
|
||||
#:title title
|
||||
#:members-data members-data
|
||||
#:page page
|
||||
#:body-class body-class
|
||||
#:head-data head-data
|
||||
#:siteinfo siteinfo))
|
||||
|
||||
(when (config-true? 'debug)
|
||||
|
@ -119,6 +120,7 @@
|
|||
(module+ test
|
||||
(check-not-false ((query-selector (attribute-selector 'href "/test/wiki/Ankle_Monitor")
|
||||
(generate-results-page
|
||||
#:req test-req
|
||||
#:source-url ""
|
||||
#:wikiname "test"
|
||||
#:title "Category:Items"
|
||||
|
|
|
@ -23,7 +23,8 @@
|
|||
(provide page-file)
|
||||
|
||||
(module+ test
|
||||
(require rackunit)
|
||||
(require rackunit
|
||||
"test-utils.rkt")
|
||||
(define test-media-detail
|
||||
'#hasheq((fileTitle . "Example file")
|
||||
(videoEmbedCode . "")
|
||||
|
@ -51,7 +52,8 @@
|
|||
[(regexp-match? #rx"(?i:^video/)" content-type) `(video (@ (src ,maybe-proxied-url) (controls)))]
|
||||
[else `""]))
|
||||
|
||||
(define (generate-results-page #:source-url source-url
|
||||
(define (generate-results-page #:req req
|
||||
#:source-url source-url
|
||||
#:wikiname wikiname
|
||||
#:title title
|
||||
#:media-detail media-detail
|
||||
|
@ -68,6 +70,7 @@
|
|||
(define maybe-proxied-raw-image-url
|
||||
(if (config-true? 'strict_proxy) (u-proxy-url raw-image-url) raw-image-url))
|
||||
(generate-wiki-page
|
||||
#:req req
|
||||
#:source-url source-url
|
||||
#:wikiname wikiname
|
||||
#:title title
|
||||
|
@ -125,7 +128,8 @@
|
|||
#f
|
||||
(url-content-type (jp "/imageUrl" media-detail))))
|
||||
(define body
|
||||
(generate-results-page #:source-url source-url
|
||||
(generate-results-page #:req req
|
||||
#:source-url source-url
|
||||
#:wikiname wikiname
|
||||
#:title title
|
||||
#:media-detail media-detail
|
||||
|
@ -159,7 +163,8 @@
|
|||
(check-not-false
|
||||
((query-selector
|
||||
(attribute-selector 'src "/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fexamplefile")
|
||||
(generate-results-page #:source-url ""
|
||||
(generate-results-page #:req test-req
|
||||
#:source-url ""
|
||||
#:wikiname "test"
|
||||
#:title "File:Example file"
|
||||
#:media-detail test-media-detail
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
net/url
|
||||
web-server/http
|
||||
"application-globals.rkt"
|
||||
"data.rkt"
|
||||
"url-utils.rkt"
|
||||
"xexpr-utils.rkt")
|
||||
|
||||
|
@ -14,12 +15,18 @@
|
|||
(define wikiname (dict-ref (url-query (request-uri req)) 'wikiname #f))
|
||||
(define q (dict-ref (url-query (request-uri req)) 'q #f))
|
||||
(response-handler
|
||||
(if (not (and wikiname q))
|
||||
(response/output
|
||||
#:code 400
|
||||
#:mime-type "text/plain"
|
||||
(λ (out)
|
||||
(displayln "Requires wikiname and q parameters." out)))
|
||||
(generate-redirect (format "/~a/search?~a"
|
||||
wikiname
|
||||
(params->query `(("q" . ,q))))))))
|
||||
(cond
|
||||
[(not wikiname)
|
||||
(response/output
|
||||
#:code 400
|
||||
#:mime-type "text/plain"
|
||||
(λ (out)
|
||||
(displayln "Requires wikiname and q parameters." out)))]
|
||||
[(or (not q) (equal? q ""))
|
||||
(define siteinfo (siteinfo-fetch wikiname))
|
||||
(define dest (format "/~a/wiki/~a" wikiname (or (siteinfo^-basepage siteinfo) "Main_Page")))
|
||||
(generate-redirect dest)]
|
||||
[#t
|
||||
(generate-redirect (format "/~a/search?~a"
|
||||
wikiname
|
||||
(params->query `(("q" . ,q)))))])))
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
html-writing
|
||||
web-server/http
|
||||
"application-globals.rkt"
|
||||
"data.rkt"
|
||||
"static-data.rkt"
|
||||
"url-utils.rkt"
|
||||
"xexpr-utils.rkt"
|
||||
"config.rkt")
|
||||
|
@ -15,13 +17,11 @@
|
|||
(require rackunit))
|
||||
|
||||
(define examples
|
||||
'(("crosscode" "CrossCode_Wiki")
|
||||
("pokemon" "Eevee")
|
||||
("minecraft" "Bricks")
|
||||
("undertale" "Hot_Dog...%3F")
|
||||
("tardis" "Eleanor_Blake")
|
||||
("fireemblem" "God-Shattering_Star")
|
||||
("fallout" "Pip-Boy_3000")))
|
||||
'(("minecraft" "Bricks")
|
||||
("crosscode" "CrossCode Wiki")
|
||||
("undertale" "Hot Dog...?")
|
||||
("tardis" "Eleanor Blake")
|
||||
("zelda" "Boomerang")))
|
||||
|
||||
(define content
|
||||
`((h2 "BreezeWiki makes wiki pages on Fandom readable")
|
||||
|
@ -40,12 +40,12 @@
|
|||
(input (@ (name "wikiname") (class "paired__input") (type "text") (placeholder "pokemon") (required))))
|
||||
(label (@ (class "paired__label"))
|
||||
"Search query"
|
||||
(input (@ (name "q") (class "paired__input") (type "text") (placeholder "Eevee") (required))))
|
||||
(input (@ (name "q") (class "paired__input") (type "text") (placeholder "Eevee"))))
|
||||
(button "Search"))
|
||||
(h2 "Example pages")
|
||||
(ul
|
||||
,@(map (λ (x)
|
||||
`(li (a (@ (href ,(apply format "/~a/wiki/~a" x)))
|
||||
`(li (a (@ (href ,(format "/~a/wiki/~a" (car x) (page-title->path (cadr x)))))
|
||||
,(apply format "~a: ~a" x))))
|
||||
examples))
|
||||
(h2 "Testimonials")
|
||||
|
@ -53,29 +53,34 @@
|
|||
(p (@ (class "testimonial")) ">you are so right that fandom still sucks even with adblock somehow. even zapping all the stupid padding it still sucks —Minimus")
|
||||
(p (@ (class "testimonial")) ">attempting to go to a wiki's forum page with breezewiki doesn't work, which is based honestly —Tom Skeleton")
|
||||
(p (@ (class "testimonial")) ">Fandom pages crashing and closing, taking forever to load and locking up as they load the ads on the site... they are causing the site to crash because they are trying to load video ads both at the top and bottom of the site as well as two or three banner ads, then a massive top of site ad and eventually my anti-virus shuts the whole site down because it's literally pulling more resources than WoW in ultra settings... —Anonymous")
|
||||
(p (@ (class "testimonial")) ">reblogs EXTREMELY appreciated I want that twink* (*fandom wiki) obliterated —footlong")
|
||||
|
||||
(h2 "What BreezeWiki isn't")
|
||||
(p "BreezeWiki isn't an \"alternative\" to Fandom, and it doesn't let you edit or write new pages.")
|
||||
(p "If you want to create your own wiki, try Miraheze!")))
|
||||
|
||||
(define body
|
||||
`(html
|
||||
(head
|
||||
(meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))
|
||||
(title "About | BreezeWiki")
|
||||
(link (@ (rel "stylesheet") (type "text/css") (href "/static/internal.css")))
|
||||
(link (@ (rel "stylesheet") (type "text/css") (href "/static/main.css"))))
|
||||
(body (@ (class "skin-fandomdesktop theme-fandomdesktop-light internal"))
|
||||
(div (@ (class "main-container"))
|
||||
(div (@ (class "fandom-community-header__background tileBoth header")))
|
||||
(div (@ (class "page"))
|
||||
(main (@ (class "page__main"))
|
||||
(div (@ (class "custom-top"))
|
||||
(h1 (@ (class "page-title"))
|
||||
"About BreezeWiki"))
|
||||
(div (@ (id "content") #;(class "page-content"))
|
||||
(div (@ (id "mw-content-text"))
|
||||
,@content))
|
||||
,(application-footer #f)))))))
|
||||
`(*TOP*
|
||||
(*DECL* DOCTYPE html)
|
||||
(html
|
||||
(head
|
||||
(meta (@ (name "viewport") (content "width=device-width, initial-scale=1")))
|
||||
(title "About | BreezeWiki")
|
||||
(link (@ (rel "stylesheet") (type "text/css") (href ,(get-static-url "internal.css"))))
|
||||
(link (@ (rel "stylesheet") (type "text/css") (href ,(get-static-url "main.css"))))
|
||||
(link (@ (rel "icon") (href ,(head-data^-icon-url head-data-default)))))
|
||||
(body (@ (class "skin-fandomdesktop theme-fandomdesktop-light internal"))
|
||||
(div (@ (class "main-container"))
|
||||
(div (@ (class "fandom-community-header__background tileBoth header")))
|
||||
(div (@ (class "page"))
|
||||
(main (@ (class "page__main"))
|
||||
(div (@ (class "custom-top"))
|
||||
(h1 (@ (class "page-title"))
|
||||
"About BreezeWiki"))
|
||||
(div (@ (id "content") #;(class "page-content"))
|
||||
(div (@ (id "mw-content-text"))
|
||||
,@content))
|
||||
,(application-footer #f))))))))
|
||||
(module+ test
|
||||
(check-not-false (xexp->html body)))
|
||||
|
||||
|
|
|
@ -21,16 +21,18 @@
|
|||
page-search)
|
||||
|
||||
(module+ test
|
||||
(require rackunit)
|
||||
(require rackunit
|
||||
"test-utils.rkt")
|
||||
(define search-json-data
|
||||
'#hasheq((batchcomplete . #t) (query . #hasheq((search . (#hasheq((ns . 0) (pageid . 219) (size . 1482) (snippet . "") (timestamp . "2022-08-21T08:54:23Z") (title . "Gacha Capsule") (wordcount . 214)) #hasheq((ns . 0) (pageid . 201) (size . 1198) (snippet . "") (timestamp . "2022-07-11T17:52:47Z") (title . "Badges") (wordcount . 181)))))))))
|
||||
|
||||
(define (generate-results-page dest-url wikiname query data #:siteinfo [siteinfo #f])
|
||||
(define (generate-results-page req dest-url wikiname query data #:siteinfo [siteinfo #f])
|
||||
(define search-results (jp "/query/search" data))
|
||||
(generate-wiki-page
|
||||
#:req req
|
||||
#:source-url dest-url
|
||||
#:wikiname wikiname
|
||||
#:title "Search Results"
|
||||
#:title query
|
||||
#:siteinfo siteinfo
|
||||
`(div (@ (class "mw-parser-output"))
|
||||
(p ,(format "~a results found for " (length search-results))
|
||||
|
@ -38,7 +40,7 @@
|
|||
(ul ,@(map
|
||||
(λ (result)
|
||||
(let* ([title (jp "/title" result)]
|
||||
[page-path (regexp-replace* #rx" " title "_")]
|
||||
[page-path (page-title->path title)]
|
||||
[timestamp (jp "/timestamp" result)]
|
||||
[wordcount (jp "/wordcount" result)]
|
||||
[size (jp "/size" result)])
|
||||
|
@ -74,7 +76,7 @@
|
|||
|
||||
(define data (easy:response-json dest-res))
|
||||
|
||||
(define body (generate-results-page dest-url wikiname query data #:siteinfo siteinfo))
|
||||
(define body (generate-results-page req dest-url wikiname query data #:siteinfo siteinfo))
|
||||
(when (config-true? 'debug)
|
||||
; used for its side effects
|
||||
; convert to string with error checking, error will be raised if xexp is invalid
|
||||
|
@ -86,4 +88,4 @@
|
|||
(write-html body out))))))
|
||||
(module+ test
|
||||
(check-not-false ((query-selector (attribute-selector 'href "/test/wiki/Gacha_Capsule")
|
||||
(generate-results-page "" "test" "Gacha" search-json-data)))))
|
||||
(generate-results-page test-req "" "test" "Gacha" search-json-data)))))
|
||||
|
|
18
src/page-set-user-settings.rkt
Normal file
18
src/page-set-user-settings.rkt
Normal file
|
@ -0,0 +1,18 @@
|
|||
#lang racket/base
|
||||
(require racket/dict
|
||||
net/url
|
||||
web-server/http
|
||||
"application-globals.rkt"
|
||||
"data.rkt"
|
||||
"url-utils.rkt"
|
||||
"xexpr-utils.rkt")
|
||||
|
||||
(provide
|
||||
page-set-user-settings)
|
||||
|
||||
(define (page-set-user-settings req)
|
||||
(response-handler
|
||||
(define next-location (dict-ref (url-query (request-uri req)) 'next_location))
|
||||
(define new-settings (read (open-input-string (dict-ref (url-query (request-uri req)) 'new_settings))))
|
||||
(define headers (user-cookies-setter new-settings))
|
||||
(generate-redirect next-location #:headers headers)))
|
|
@ -22,6 +22,7 @@
|
|||
#".js" #"text/javascript"
|
||||
#".png" #"image/png"
|
||||
#".svg" #"image/svg+xml"
|
||||
#".woff2" #"font/woff2"
|
||||
#".txt" #"text/plain"))
|
||||
|
||||
(define (ext->mime-type ext)
|
||||
|
@ -64,5 +65,7 @@
|
|||
((files:make
|
||||
#:url->path (lambda (u) ((make-url->path path-static) u))
|
||||
#:path->mime-type (lambda (u) (ext->mime-type (path-get-extension u)))
|
||||
#:cache-no-cache (config-true? 'debug) #;"browser applies heuristics if unset")
|
||||
#:cache-no-cache (config-true? 'debug)
|
||||
#:cache-immutable (not (config-true? 'debug))
|
||||
#:cache-max-age (if (config-true? 'debug) #f 604800))
|
||||
conn new-req))
|
||||
|
|
|
@ -1,22 +1,65 @@
|
|||
#lang racket/base
|
||||
(require racket/path
|
||||
(require racket/match
|
||||
racket/path
|
||||
racket/string
|
||||
net/url
|
||||
web-server/http
|
||||
web-server/dispatchers/dispatch
|
||||
(only-in racket/promise delay)
|
||||
(prefix-in lift: web-server/dispatchers/dispatch-lift)
|
||||
"application-globals.rkt"
|
||||
"config.rkt"
|
||||
"syntax.rkt"
|
||||
"xexpr-utils.rkt")
|
||||
|
||||
(provide
|
||||
subdomain-dispatcher)
|
||||
|
||||
(define (subdomain-dispatcher subdomain)
|
||||
(module+ test
|
||||
(require rackunit))
|
||||
|
||||
(define (do-redirect:make subdomain canonical-origin)
|
||||
(lift:make
|
||||
(λ (req)
|
||||
(response-handler
|
||||
(define uri (request-uri req))
|
||||
(define path (url-path uri))
|
||||
(define path-string (string-join (map (λ (p) (path/param-path p)) path) "/"))
|
||||
(define dest (format "~a/~a/~a" (config-get 'canonical_origin) subdomain path-string))
|
||||
(define dest (format "~a/~a/~a" canonical-origin subdomain path-string))
|
||||
(generate-redirect dest)))))
|
||||
|
||||
(define (router req)
|
||||
(define host (bytes->string/utf-8 (header-value (headers-assq* #"host" (request-headers/raw req)))))
|
||||
(define x-canonical-origin (headers-assq* #"x-canonical-origin" (request-headers/raw req)))
|
||||
(define canonical-origin
|
||||
(cond
|
||||
[x-canonical-origin (bytes->string/utf-8 (header-value x-canonical-origin))]
|
||||
[(config-true? 'canonical_origin) (config-get 'canonical_origin)]
|
||||
[#t #f]))
|
||||
(if/out canonical-origin
|
||||
(let* ([canonical-origin-host (url-host (string->url canonical-origin))])
|
||||
(if/in canonical-origin-host
|
||||
(let* ([splitter (string-append "." (url-host (string->url canonical-origin)))]
|
||||
[s (string-split host splitter #:trim? #f)])
|
||||
(if/in (and (eq? 2 (length s)) (equal? "" (cadr s)))
|
||||
(list 'redirect (car s) canonical-origin)))))
|
||||
'next-dispatcher))
|
||||
(module+ test
|
||||
(define (qr url headers)
|
||||
(request #"GET" (string->url url) (map (λ (h) (header (car h) (cadr h))) headers) (delay '()) #f "127.0.0.1" 10416 "127.0.0.1"))
|
||||
(parameterize ([(config-parameter 'canonical_origin) "https://breezewiki.com"])
|
||||
(check-equal? (router (qr "/" '((#"Host" #"breezewiki.com"))))
|
||||
'next-dispatcher)
|
||||
(check-equal? (router (qr "/wiki/Spell" '((#"Host" #"magic.breezewiki.com"))))
|
||||
'(redirect "magic" "https://breezewiki.com"))
|
||||
(check-equal? (router (qr "/" '((#"Host" #"magic.bw.breezewiki.com")
|
||||
(#"X-Canonical-Origin" #"https://bw.breezewiki.com"))))
|
||||
'(redirect "magic" "https://bw.breezewiki.com"))
|
||||
(check-equal? (router (qr "/" '((#"Host" #"magic.bwxxxxx.onion")
|
||||
(#"X-Canonical-Origin" #"http://bwxxxxx.onion"))))
|
||||
'(redirect "magic" "http://bwxxxxx.onion"))))
|
||||
|
||||
(define (subdomain-dispatcher conn req)
|
||||
(match (router req)
|
||||
[(list 'redirect subdomain canonical-origin) ((do-redirect:make subdomain canonical-origin) conn req)]
|
||||
[_ (next-dispatcher)]))
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"data.rkt"
|
||||
"pure-utils.rkt"
|
||||
"syntax.rkt"
|
||||
"tree-updater.rkt"
|
||||
"xexpr-utils.rkt"
|
||||
"url-utils.rkt")
|
||||
|
||||
|
@ -30,51 +31,19 @@
|
|||
preprocess-html-wiki)
|
||||
|
||||
(module+ test
|
||||
(require rackunit)
|
||||
(define wiki-document
|
||||
'(*TOP*
|
||||
(div (@ (class "mw-parser-output"))
|
||||
(aside (@ (role "region") (class "portable-infobox pi-theme-wikia pi-layout-default"))
|
||||
(h2 (@ (class "pi-item pi-title") (data-source "title"))
|
||||
"Infobox Title")
|
||||
(figure (@ (class "pi-item pi-image") (data-source "image"))
|
||||
(a (@ (href "https://static.wikia.nocookie.net/nice-image.png") (class "image image-thumbnail") (title ""))
|
||||
(img (@ (src "https://static.wikia.nocookie.net/nice-image-thumbnail.png") (class "pi-image-thumbnail")))))
|
||||
(div (@ (class "pi-item pi-data") (data-source "description"))
|
||||
(h3 (@ (class "pi-data-label"))
|
||||
"Description")
|
||||
(div (@ (class "pi-data-value"))
|
||||
"Mystery infobox!")))
|
||||
(div (@ (data-test-collapsesection) (class "collapsible collapsetoggle-inline collapsed"))
|
||||
(i (b "This section is hidden for dramatic effect."))
|
||||
(div (@ (class "collapsible-content"))
|
||||
(p "Another page link: "
|
||||
(a (@ (data-test-wikilink) (href "https://test.fandom.com/wiki/Another_Page") (title "Another Page"))
|
||||
"Another Page"))))
|
||||
(figure (@ (class "thumb tnone"))
|
||||
(a (@ (href "https://static.wikia.nocookie.net/nice-image.png") (class "image") (data-test-figure-a))
|
||||
(img (@ (src "data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D")
|
||||
(data-src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
||||
(class "thumbimage lazyload"))))
|
||||
(noscript
|
||||
(a (@ (href "https://static.wikia.nocookie.net/nice-image.png") (class "image"))
|
||||
(img (@ (src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
||||
(data-src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
||||
(class "thumbimage")))))
|
||||
(figcaption "Test figure!"))
|
||||
(iframe (@ (src "https://example.com/iframe-src")))))))
|
||||
(require rackunit))
|
||||
|
||||
(define (preprocess-html-wiki html)
|
||||
(define (rr* find replace contents)
|
||||
(define ((rr* find replace) contents)
|
||||
(regexp-replace* find contents replace))
|
||||
((compose1
|
||||
; fix navbox list nesting
|
||||
; navbox on right of page has incorrect html "<td ...><li>" and the xexpr parser puts the <li> much further up the tree
|
||||
; add a <ul> to make the parser happy
|
||||
; usage: /fallout/wiki/Fallout:_New_Vegas_achievements_and_trophies
|
||||
(curry rr* #rx"(<td[^>]*>\n?)(<li>)" "\\1<ul>\\2")
|
||||
(rr* #rx"(<td[^>]*>\n?)(<li>)" "\\1<ul>\\2")
|
||||
; change <figcaption><p> to <figcaption><span> to make the parser happy
|
||||
(curry rr* #rx"(<figcaption[^>]*>)[ \t]*<p class=\"caption\">([^<]*)</p>" "\\1<span class=\"caption\">\\2</span>"))
|
||||
(rr* #rx"(<figcaption[^>]*>)[ \t]*<p class=\"caption\">([^<]*)</p>" "\\1<span class=\"caption\">\\2</span>"))
|
||||
html))
|
||||
(module+ test
|
||||
(check-equal? (preprocess-html-wiki "<td class=\"va-navbox-column\" style=\"width: 33%\">\n<li>Hey</li>")
|
||||
|
@ -82,189 +51,9 @@
|
|||
(check-equal? (preprocess-html-wiki "<figure class=\"thumb tright\" style=\"width: 150px\"><a class=\"image\"><img></a><noscript><a><img></a></noscript><figcaption class=\"thumbcaption\"> <p class=\"caption\">Caption text.</p></figcaption></figure>")
|
||||
"<figure class=\"thumb tright\" style=\"width: 150px\"><a class=\"image\"><img></a><noscript><a><img></a></noscript><figcaption class=\"thumbcaption\"><span class=\"caption\">Caption text.</span></figcaption></figure>"))
|
||||
|
||||
(define (update-tree-wiki tree wikiname)
|
||||
(update-tree
|
||||
(λ (element element-type attributes children)
|
||||
;; replace whole element?
|
||||
(cond
|
||||
; wrap tables in a div.table-scroller
|
||||
[(and (eq? element-type 'table)
|
||||
(has-class? "wikitable" attributes)
|
||||
(not (dict-has-key? attributes 'data-scrolling)))
|
||||
`(div
|
||||
((class "table-scroller"))
|
||||
((,element-type (@ (data-scrolling) ,@attributes)
|
||||
,@children)))]
|
||||
; exclude empty figcaptions
|
||||
[(and (eq? element-type 'figcaption)
|
||||
(or (eq? (length (filter element-is-element? children)) 0)
|
||||
((query-selector (λ (element-type attributes children)
|
||||
(eq? element-type 'use))
|
||||
element))))
|
||||
return-no-element]
|
||||
; exclude infobox items that are videos, and gallery items that are videos
|
||||
[(and (or (has-class? "pi-item" attributes)
|
||||
(has-class? "wikia-gallery-item" attributes))
|
||||
((query-selector (λ (element-type attributes children)
|
||||
(has-class? "video-thumbnail" attributes))
|
||||
element)))
|
||||
return-no-element]
|
||||
; exclude the invisible brackets after headings
|
||||
[(and (eq? element-type 'span)
|
||||
(has-class? "mw-editsection" attributes))
|
||||
return-no-element]
|
||||
; display a link instead of an iframe
|
||||
[(eq? element-type 'iframe)
|
||||
(define src (car (dict-ref attributes 'src null)))
|
||||
`(a
|
||||
((class "iframe-alternative") (href ,src))
|
||||
(,(format "Embedded media: ~a" src)))]
|
||||
; remove noscript versions of images because they are likely lower quality than the script versions
|
||||
[(and (eq? element-type 'noscript)
|
||||
(match children
|
||||
; either the noscript has a.image as a first child...
|
||||
[(list (list 'a (list '@ a-att ...) _)) (has-class? "image" a-att)]
|
||||
; or the noscript has img as a first child
|
||||
[(list (list 'img _)) #t]
|
||||
[_ #f]))
|
||||
return-no-element]
|
||||
[#t
|
||||
(list element-type
|
||||
;; attributes
|
||||
((compose1
|
||||
; uncollapsing
|
||||
(curry attribute-maybe-update 'class
|
||||
(λ (class)
|
||||
(string-join
|
||||
((compose1
|
||||
; uncollapse all navbox items (bottom of page mass navigation)
|
||||
(curry u
|
||||
(λ (classlist) (and (eq? element-type 'table)
|
||||
(member "navbox" classlist)
|
||||
(member "collapsed" classlist)))
|
||||
(λ (classlist) (filter (curry (negate equal?) "collapsed") classlist)))
|
||||
; uncollapse portable-infobox sections
|
||||
(curry u
|
||||
(λ (classlist) (and (eq? element-type 'section)
|
||||
(member "pi-collapse" classlist)))
|
||||
(λ (classlist) (filter (λ (v)
|
||||
(and (not (equal? v "pi-collapse-closed"))
|
||||
(not (equal? v "pi-collapse"))))
|
||||
classlist)))
|
||||
; generic: includes article sections and tables, probably more
|
||||
(curry u
|
||||
(λ (classlist) (and (member "collapsible" classlist)
|
||||
(member "collapsed" classlist)))
|
||||
(λ (classlist) (filter (curry (negate equal?) "collapsed") classlist))))
|
||||
(string-split class " "))
|
||||
" ")))
|
||||
; change links to stay on the same wiki
|
||||
(curry attribute-maybe-update 'href
|
||||
(λ (href)
|
||||
((compose1
|
||||
(λ (href) (regexp-replace #rx"^(/wiki/.*)" href (format "/~a\\1" wikiname)))
|
||||
(λ (href) (regexp-replace (pregexp (format "^https://(~a)\\.fandom\\.com(/wiki/.*)" px-wikiname)) href "/\\1\\2")))
|
||||
href)))
|
||||
; add noreferrer to a.image
|
||||
(curry u
|
||||
(λ (v) (and (eq? element-type 'a)
|
||||
(has-class? "image" v)))
|
||||
(λ (v) (dict-update v 'rel (λ (s)
|
||||
(list (string-append (car s) " noreferrer")))
|
||||
'(""))))
|
||||
; proxy images from inline styles, if strict_proxy is set
|
||||
(curry u
|
||||
(λ (v) (config-true? 'strict_proxy))
|
||||
(λ (v) (attribute-maybe-update 'style
|
||||
(λ (style)
|
||||
(regexp-replace #rx"url\\(['\"]?(.*?)['\"]?\\)" style
|
||||
(λ (whole url)
|
||||
(string-append
|
||||
"url("
|
||||
(u-proxy-url url)
|
||||
")")))) v)))
|
||||
; and also their links, if strict_proxy is set
|
||||
(curry u
|
||||
(λ (v)
|
||||
(and (config-true? 'strict_proxy)
|
||||
(eq? element-type 'a)
|
||||
(or (has-class? "image-thumbnail" v)
|
||||
(has-class? "image" v))))
|
||||
(λ (v) (attribute-maybe-update 'href u-proxy-url v)))
|
||||
; proxy images from src attributes, if strict_proxy is set
|
||||
(curry u
|
||||
(λ (v) (config-true? 'strict_proxy))
|
||||
(λ (v) (attribute-maybe-update 'src u-proxy-url v)))
|
||||
; don't lazyload images
|
||||
(curry u
|
||||
(λ (v) (dict-has-key? v 'data-src))
|
||||
(λ (v) (attribute-maybe-update 'src (λ (_) (car (dict-ref v 'data-src))) v)))
|
||||
; don't use srcset - TODO: use srcset?
|
||||
(λ (v) (dict-remove v 'srcset)))
|
||||
attributes)
|
||||
;; children
|
||||
((compose1
|
||||
; wrap blinking animated images in a slot so they can be animated with CSS
|
||||
(curry u
|
||||
(λ (v) (and (has-class? "animated" attributes)
|
||||
((length v) . > . 1)))
|
||||
(λ (v)
|
||||
`((span (@ (class "animated-slot__outer") (style ,(format "--steps: ~a" (length v))))
|
||||
(span (@ (class "animated-slot__inner"))
|
||||
,@v))))))
|
||||
children))]))
|
||||
tree))
|
||||
(module+ test
|
||||
(define transformed
|
||||
(parameterize ([(config-parameter 'strict_proxy) "true"])
|
||||
(update-tree-wiki wiki-document "test")))
|
||||
; check that wikilinks are changed to be local
|
||||
(check-equal? (get-attribute 'href (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (dict-has-key? a 'data-test-wikilink))
|
||||
transformed))))
|
||||
"/test/wiki/Another_Page")
|
||||
; check that a.image has noreferrer
|
||||
(check-equal? (get-attribute 'rel (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (and (eq? t 'a)
|
||||
(has-class? "image" a)))
|
||||
transformed))))
|
||||
" noreferrer")
|
||||
; check that article collapse sections become uncollapsed
|
||||
(check-equal? (get-attribute 'class (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (dict-has-key? a 'data-test-collapsesection))
|
||||
transformed))))
|
||||
"collapsible collapsetoggle-inline")
|
||||
; check that iframes are gone
|
||||
(check-false ((query-selector (λ (t a c) (eq? t 'iframe)) transformed)))
|
||||
(check-equal? (let* ([alternative ((query-selector (λ (t a c) (has-class? "iframe-alternative" a)) transformed))]
|
||||
[link ((query-selector (λ (t a c) (eq? t 'a)) alternative))])
|
||||
(get-attribute 'href (bits->attributes link)))
|
||||
"https://example.com/iframe-src")
|
||||
; check that images are proxied
|
||||
(check-equal? (get-attribute 'src (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (eq? t 'img))
|
||||
transformed))))
|
||||
"/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fnice-image-thumbnail.png")
|
||||
; check that links to images are proxied
|
||||
(check-equal? (get-attribute 'href (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (and (eq? t 'a) (has-class? "image-thumbnail" a)))
|
||||
transformed))))
|
||||
"/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fnice-image.png")
|
||||
(check-equal? (get-attribute 'href (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (member '(data-test-figure-a) a))
|
||||
transformed))))
|
||||
"/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fnice-image.png")
|
||||
; check that noscript images are removed
|
||||
(check-equal? ((query-selector (λ (t a c) (eq? t 'noscript)) transformed)) #f))
|
||||
|
||||
(define (page-wiki req)
|
||||
(define wikiname (path/param-path (first (url-path (request-uri req)))))
|
||||
(define user-cookies (user-cookies-getter req))
|
||||
(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))
|
||||
|
@ -279,7 +68,9 @@
|
|||
("formatversion" . "2")
|
||||
("format" . "json")))))
|
||||
(log-outgoing dest-url)
|
||||
(easy:get dest-url #:timeouts timeouts)]
|
||||
(easy:get dest-url
|
||||
#:timeouts timeouts
|
||||
#:headers `#hasheq((cookie . ,(format "theme=~a" (user-cookies^-theme user-cookies)))))]
|
||||
[siteinfo (siteinfo-fetch wikiname)])
|
||||
|
||||
(cond
|
||||
|
@ -289,29 +80,31 @@
|
|||
[page-html (jp "/parse/text" data "")]
|
||||
[page-html (preprocess-html-wiki page-html)]
|
||||
[page (html->xexp page-html)]
|
||||
[head-html (jp "/parse/headhtml" data "")]
|
||||
[body-class (match (regexp-match #rx"<body [^>]*class=\"([^\"]*)" head-html)
|
||||
[(list _ classes) classes]
|
||||
[_ ""])])
|
||||
[head-data ((head-data-getter wikiname) data)])
|
||||
(if (equal? "missingtitle" (jp "/error/code" data #f))
|
||||
(next-dispatcher)
|
||||
(response-handler
|
||||
(define body
|
||||
(generate-wiki-page
|
||||
(update-tree-wiki page wikiname)
|
||||
#:req req
|
||||
#:source-url source-url
|
||||
#:wikiname wikiname
|
||||
#:title title
|
||||
#:body-class body-class
|
||||
#:head-data head-data
|
||||
#:siteinfo siteinfo))
|
||||
(define redirect-msg ((query-selector (attribute-selector 'class "redirectMsg") body)))
|
||||
(define redirect-query-parameter (dict-ref (url-query (request-uri req)) 'redirect "yes"))
|
||||
(define headers
|
||||
(build-headers
|
||||
always-headers
|
||||
(when redirect-msg
|
||||
(let* ([dest (get-attribute 'href (bits->attributes ((query-selector (λ (t a c) (eq? t 'a)) redirect-msg))))]
|
||||
[value (bytes-append #"0;url=" (string->bytes/utf-8 dest))])
|
||||
(header #"Refresh" value)))))
|
||||
; redirect-query-parameter: only the string "no" is significant:
|
||||
; https://github.com/Wikia/app/blob/fe60579a53f16816d65dad1644363160a63206a6/includes/Wiki.php#L367
|
||||
(when (and redirect-msg
|
||||
(not (equal? redirect-query-parameter "no")))
|
||||
(let* ([dest (get-attribute 'href (bits->attributes ((query-selector (λ (t a c) (eq? t 'a)) redirect-msg))))]
|
||||
[value (bytes-append #"0;url=" (string->bytes/utf-8 dest))])
|
||||
(header #"Refresh" value)))))
|
||||
(when (config-true? 'debug)
|
||||
; used for its side effects
|
||||
; convert to string with error checking, error will be raised if xexp is invalid
|
||||
|
|
37
src/static-data.rkt
Normal file
37
src/static-data.rkt
Normal file
|
@ -0,0 +1,37 @@
|
|||
#lang typed/racket/base
|
||||
(require racket/path
|
||||
racket/runtime-path
|
||||
racket/string)
|
||||
|
||||
(provide
|
||||
get-static-url
|
||||
link-header)
|
||||
|
||||
(define-runtime-path path-static "../static")
|
||||
|
||||
(define static-data
|
||||
(for/hash : (Immutable-HashTable Path Nonnegative-Integer)([f (directory-list path-static)])
|
||||
(define built (simple-form-path (build-path path-static f)))
|
||||
(values built (file-or-directory-modify-seconds built))))
|
||||
|
||||
(: get-static-url (Path-String -> String))
|
||||
(define (get-static-url path-or-filename)
|
||||
(define the-path (simple-form-path (if (path? path-or-filename)
|
||||
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)))
|
||||
|
||||
; https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types/preload
|
||||
(: link-header String)
|
||||
(define link-header
|
||||
(let* ([with-t '(("main.css" "as=style"))]
|
||||
[without-t '(("preact.js" "as=script")
|
||||
("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)]
|
||||
[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)]
|
||||
[header-parts
|
||||
(for/list : (Listof String) ([full-path all])
|
||||
(define attributes (map (λ ([s : String]) (format "; ~a" s)) (cdr full-path)))
|
||||
(format "<~a>; rel=preload~a" (car full-path) (string-join attributes "")))])
|
||||
(string-join header-parts ", ")))
|
|
@ -37,7 +37,7 @@
|
|||
[(and (pair? node) (eq? 'if/out (car node))) node]
|
||||
; -- replace if/in
|
||||
[(and (pair? node) (eq? 'if/in (car node)))
|
||||
(append '(if) (cdr node) else)]
|
||||
(append '(if) (walk (cdr node)) else)]
|
||||
; recurse down pair head and tail
|
||||
[(pair? node) (cons (walk (car node)) (walk (cdr node)))]
|
||||
; something else that can't be recursed into, so pass it through
|
||||
|
|
8
src/test-utils.rkt
Normal file
8
src/test-utils.rkt
Normal file
|
@ -0,0 +1,8 @@
|
|||
#lang racket/base
|
||||
(require web-server/http/request-structs
|
||||
net/url-structs
|
||||
(only-in racket/promise delay))
|
||||
(provide
|
||||
test-req)
|
||||
|
||||
(define test-req (request #"GET" (url "https" #f "breezewiki.com" #f #t (list (path/param "" '())) '() #f) '() (delay '()) #f "127.0.0.1" 0 "127.0.0.1"))
|
272
src/tree-updater.rkt
Normal file
272
src/tree-updater.rkt
Normal file
|
@ -0,0 +1,272 @@
|
|||
#lang racket/base
|
||||
(require racket/dict
|
||||
racket/function
|
||||
racket/match
|
||||
racket/string
|
||||
"config.rkt"
|
||||
"pure-utils.rkt"
|
||||
"url-utils.rkt"
|
||||
"xexpr-utils.rkt")
|
||||
|
||||
(provide
|
||||
update-tree-wiki)
|
||||
|
||||
(module+ test
|
||||
(require rackunit
|
||||
html-parsing)
|
||||
(define wiki-document
|
||||
'(*TOP*
|
||||
(div (@ (class "mw-parser-output"))
|
||||
(aside (@ (role "region") (class "portable-infobox pi-theme-wikia pi-layout-default"))
|
||||
(h2 (@ (class "pi-item pi-title") (data-source "title"))
|
||||
"Infobox Title")
|
||||
(figure (@ (class "pi-item pi-image") (data-source "image"))
|
||||
(a (@ (href "https://static.wikia.nocookie.net/nice-image.png") (class "image image-thumbnail") (title ""))
|
||||
(img (@ (src "https://static.wikia.nocookie.net/nice-image-thumbnail.png") (class "pi-image-thumbnail")))))
|
||||
(div (@ (class "pi-item pi-data") (data-source "description"))
|
||||
(h3 (@ (class "pi-data-label"))
|
||||
"Description")
|
||||
(div (@ (class "pi-data-value"))
|
||||
"Mystery infobox!")))
|
||||
(div (@ (data-test-collapsesection) (class "collapsible collapsetoggle-inline collapsed"))
|
||||
(i (b "This section is hidden for dramatic effect."))
|
||||
(div (@ (class "collapsible-content"))
|
||||
(p "Another page link: "
|
||||
(a (@ (data-test-wikilink) (href "https://test.fandom.com/wiki/Another_Page") (title "Another Page"))
|
||||
"Another Page"))))
|
||||
(figure (@ (class "thumb tnone"))
|
||||
(a (@ (href "https://static.wikia.nocookie.net/nice-image.png") (class "image") (data-test-figure-a))
|
||||
(img (@ (src "data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D")
|
||||
(data-src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
||||
(class "thumbimage lazyload"))))
|
||||
(noscript
|
||||
(a (@ (href "https://static.wikia.nocookie.net/nice-image.png") (title "a nice image") (three-attribs))
|
||||
(img (@ (src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
||||
(data-src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
||||
(class "thumbimage")))))
|
||||
(noscript
|
||||
(a (@ (href "https://static.wikia.nocookie.net/nice-image.png") (class "image"))
|
||||
(img (@ (src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
||||
(data-src "https://static.wikia.nocookie.net/nice-image-thumbnail.png")
|
||||
(class "thumbimage")))))
|
||||
(figcaption "Test figure!"))
|
||||
(iframe (@ (src "https://example.com/iframe-src")))))))
|
||||
|
||||
(define (updater wikiname)
|
||||
(define classlist-updater
|
||||
(compose1
|
||||
; uncollapse all navbox items (bottom of page mass navigation)
|
||||
(curry u
|
||||
(λ (classlist) (and ; removed due to scoping, would improve peformance (eq? element-type 'table)
|
||||
(member "navbox" classlist)
|
||||
(member "collapsed" classlist)))
|
||||
(λ (classlist) (filter (curry (negate equal?) "collapsed") classlist)))
|
||||
; uncollapse portable-infobox sections
|
||||
(curry u
|
||||
(λ (classlist) (and ; removed due to scoping, would improve performance (eq? element-type 'section)
|
||||
(member "pi-collapse" classlist)))
|
||||
(λ (classlist) (filter (λ (v)
|
||||
(and (not (equal? v "pi-collapse-closed"))
|
||||
(not (equal? v "pi-collapse"))))
|
||||
classlist)))
|
||||
; generic: includes article sections and tables, probably more
|
||||
(curry u
|
||||
(λ (classlist) (and (member "collapsible" classlist)
|
||||
(member "collapsed" classlist)))
|
||||
(λ (classlist) (filter (curry (negate equal?) "collapsed") classlist)))))
|
||||
|
||||
(define ((string-replace-curried from to) str)
|
||||
(string-replace str from to))
|
||||
|
||||
(define class-updater
|
||||
(compose1
|
||||
(string-replace-curried " collapsed" "")
|
||||
(string-replace-curried "pi-collapse-closed" "")
|
||||
(string-replace-curried "pi-collapse" "")))
|
||||
|
||||
(define attributes-updater
|
||||
(compose1
|
||||
; uncollapsing
|
||||
#;(curry attribute-maybe-update 'class
|
||||
(λ (class) (string-join (classlist-updater (string-split class " ")) " ")))
|
||||
(curry attribute-maybe-update 'class class-updater)
|
||||
; change links to stay on the same wiki
|
||||
(curry attribute-maybe-update 'href
|
||||
(λ (href)
|
||||
((compose1
|
||||
(λ (href) (regexp-replace #rx"^(/wiki/.*)" href (format "/~a\\1" wikiname)))
|
||||
(λ (href) (regexp-replace (pregexp (format "^https://(~a)\\.fandom\\.com(/wiki/.*)" px-wikiname)) href "/\\1\\2")))
|
||||
href)))
|
||||
; add noreferrer to a.image
|
||||
(curry u
|
||||
(λ (v) (and #;(eq? element-type 'a)
|
||||
(has-class? "image" v)))
|
||||
(λ (v) (dict-update v 'rel (λ (s)
|
||||
(list (string-append (car s) " noreferrer")))
|
||||
'(""))))
|
||||
; proxy images from inline styles, if strict_proxy is set
|
||||
(curry u
|
||||
(λ (v) (config-true? 'strict_proxy))
|
||||
(λ (v) (attribute-maybe-update
|
||||
'style
|
||||
(λ (style)
|
||||
(regexp-replace #rx"url\\(['\"]?(.*?)['\"]?\\)" style
|
||||
(λ (whole url)
|
||||
(string-append
|
||||
"url("
|
||||
(u-proxy-url url)
|
||||
")")))) v)))
|
||||
; and also their links, if strict_proxy is set
|
||||
(curry u
|
||||
(λ (v)
|
||||
(and (config-true? 'strict_proxy)
|
||||
#;(eq? element-type 'a)
|
||||
(or (has-class? "image-thumbnail" v)
|
||||
(has-class? "image" v))))
|
||||
(λ (v) (attribute-maybe-update 'href u-proxy-url v)))
|
||||
; proxy images from src attributes, if strict_proxy is set
|
||||
(curry u
|
||||
(λ (v) (config-true? 'strict_proxy))
|
||||
(λ (v) (attribute-maybe-update 'src u-proxy-url v)))
|
||||
; don't lazyload images
|
||||
(curry u
|
||||
(λ (v) (dict-has-key? v 'data-src))
|
||||
(λ (v) (attribute-maybe-update 'src (λ (_) (car (dict-ref v 'data-src))) v)))
|
||||
; don't use srcset - TODO: use srcset?
|
||||
(λ (v) (dict-remove v 'srcset))))
|
||||
|
||||
(define (children-updater attributes children)
|
||||
; more uncollapsing - sample: bandori/wiki/BanG_Dream!_Wikia
|
||||
((λ (children)
|
||||
(u
|
||||
(λ (v) (has-class? "mw-collapsible-content" attributes))
|
||||
(λ (v) (for/list ([element v])
|
||||
(u (λ (element) (pair? element))
|
||||
(λ (element)
|
||||
`(,(car element)
|
||||
(@ ,@(attribute-maybe-update 'style (λ (a) (regexp-replace #rx"display: *none" a "display:inline")) (bits->attributes element)))
|
||||
,@(filter element-is-content? (cdr element))))
|
||||
element)))
|
||||
children))
|
||||
; wrap blinking animated images in a slot so they can be animated with CSS
|
||||
((λ (children)
|
||||
(u
|
||||
(λ (v) (and (has-class? "animated" attributes)
|
||||
((length v) . > . 1)))
|
||||
(λ (v)
|
||||
`((span (@ (class "animated-slot__outer") (style ,(format "--steps: ~a" (length v))))
|
||||
(span (@ (class "animated-slot__inner"))
|
||||
,@v))))
|
||||
children))
|
||||
children)))
|
||||
|
||||
(define (updater element element-type attributes children)
|
||||
;; replace whole element?
|
||||
(cond
|
||||
; wrap tables in a div.table-scroller
|
||||
[(and (eq? element-type 'table)
|
||||
(has-class? "wikitable" attributes)
|
||||
(not (dict-has-key? attributes 'data-scrolling)))
|
||||
`(div
|
||||
((class "table-scroller"))
|
||||
((,element-type (@ (data-scrolling) ,@attributes)
|
||||
,@children)))]
|
||||
; exclude empty figcaptions
|
||||
[(and (eq? element-type 'figcaption)
|
||||
(or (eq? (length (filter element-is-element? children)) 0)
|
||||
((query-selector (λ (element-type attributes children)
|
||||
(eq? element-type 'use))
|
||||
element))))
|
||||
return-no-element]
|
||||
; exclude infobox items that are videos, and gallery items that are videos
|
||||
[(and (or (has-class? "pi-item" attributes)
|
||||
(has-class? "wikia-gallery-item" attributes))
|
||||
((query-selector (λ (element-type attributes children)
|
||||
(has-class? "video-thumbnail" attributes))
|
||||
element)))
|
||||
return-no-element]
|
||||
; exclude the invisible brackets after headings
|
||||
[(and (eq? element-type 'span)
|
||||
(has-class? "mw-editsection" attributes))
|
||||
return-no-element]
|
||||
; display a link instead of an iframe
|
||||
[(eq? element-type 'iframe)
|
||||
(define src (car (dict-ref attributes 'src null)))
|
||||
`(a
|
||||
((class "iframe-alternative") (href ,src))
|
||||
(,(format "Embedded media: ~a" src)))]
|
||||
; remove noscript versions of images because they are likely lower quality than the script versions
|
||||
[(and (eq? element-type 'noscript)
|
||||
(match children
|
||||
; either the noscript has an a, with attributes and an img as a first child, as a first child...
|
||||
[(list (list 'a (list '@ _ ...) (list 'img _))) #t]
|
||||
; or the noscript has img as a first child
|
||||
[(list (list 'img _)) #t]
|
||||
[_ #f]))
|
||||
return-no-element]
|
||||
[#t
|
||||
(list element-type
|
||||
;; attributes
|
||||
(attributes-updater #; element-type attributes)
|
||||
;; children
|
||||
(children-updater attributes children))]))
|
||||
|
||||
updater)
|
||||
|
||||
(define (update-tree-wiki tree wikiname)
|
||||
(update-tree (updater wikiname) tree))
|
||||
|
||||
(module+ test
|
||||
(define transformed
|
||||
(parameterize ([(config-parameter 'strict_proxy) "true"])
|
||||
(update-tree-wiki wiki-document "test")))
|
||||
; check that wikilinks are changed to be local
|
||||
(check-equal? (get-attribute 'href (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (dict-has-key? a 'data-test-wikilink))
|
||||
transformed))))
|
||||
"/test/wiki/Another_Page")
|
||||
; check that a.image has noreferrer
|
||||
(check-equal? (get-attribute 'rel (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (and (eq? t 'a)
|
||||
(has-class? "image" a)))
|
||||
transformed))))
|
||||
" noreferrer")
|
||||
; check that article collapse sections become uncollapsed
|
||||
(check-equal? (get-attribute 'class (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (dict-has-key? a 'data-test-collapsesection))
|
||||
transformed))))
|
||||
"collapsible collapsetoggle-inline")
|
||||
; check that iframes are gone
|
||||
(check-false ((query-selector (λ (t a c) (eq? t 'iframe)) transformed)))
|
||||
(check-equal? (let* ([alternative ((query-selector (λ (t a c) (has-class? "iframe-alternative" a)) transformed))]
|
||||
[link ((query-selector (λ (t a c) (eq? t 'a)) alternative))])
|
||||
(get-attribute 'href (bits->attributes link)))
|
||||
"https://example.com/iframe-src")
|
||||
; check that images are proxied
|
||||
(check-equal? (get-attribute 'src (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (eq? t 'img))
|
||||
transformed))))
|
||||
"/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fnice-image-thumbnail.png")
|
||||
; check that links to images are proxied
|
||||
(check-equal? (get-attribute 'href (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (and (eq? t 'a) (has-class? "image-thumbnail" a)))
|
||||
transformed))))
|
||||
"/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fnice-image.png")
|
||||
(check-equal? (get-attribute 'href (bits->attributes
|
||||
((query-selector
|
||||
(λ (t a c) (member '(data-test-figure-a) a))
|
||||
transformed))))
|
||||
"/proxy?dest=https%3A%2F%2Fstatic.wikia.nocookie.net%2Fnice-image.png")
|
||||
; check that noscript images are removed
|
||||
(check-equal? ((query-selector (λ (t a c) (eq? t 'noscript)) transformed)) #f)
|
||||
; benchmark
|
||||
(when (file-exists? "Frog.html2")
|
||||
(with-input-from-file "Frog.html2"
|
||||
(λ ()
|
||||
(define tree (html->xexp (current-input-port)))
|
||||
(time (length (update-tree-wiki tree "minecraft")))))))
|
|
@ -17,7 +17,9 @@
|
|||
; prints "out: <url>"
|
||||
log-outgoing
|
||||
; pass in a header, headers, or something useless. they'll all combine into a list
|
||||
build-headers)
|
||||
build-headers
|
||||
; try to follow wikimedia's format for which characters should be encoded/replaced in page titles for the url
|
||||
page-title->path)
|
||||
|
||||
(module+ test
|
||||
(require "typed-rackunit.rkt"))
|
||||
|
@ -26,14 +28,18 @@
|
|||
|
||||
;; https://url.spec.whatwg.org/#urlencoded-serializing
|
||||
|
||||
(define urlencoded-set '(#\! #\' #\( #\) #\~ ; urlencoded set
|
||||
#\$ #\% #\& #\+ #\, ; component set
|
||||
#\/ #\: #\; #\= #\@ #\[ #\\ #\] #\^ #\| ; userinfo set
|
||||
#\? #\` #\{ #\} ; path set
|
||||
#\ #\" #\# #\< #\> ; query set
|
||||
; c0 controls included elsewhere
|
||||
; higher ranges included elsewhere
|
||||
))
|
||||
(define path-set '(#\; ; semicolon is part of the userinfo set in the URL standard, but I'm putting it here
|
||||
#\? #\` #\{ #\} ; path set
|
||||
#\ #\" #\# #\< #\> ; query set
|
||||
; c0 controls included elsewhere
|
||||
; higher ranges included elsewhere
|
||||
))
|
||||
(define urlencoded-set (append
|
||||
'(#\! #\' #\( #\) #\~ ; urlencoded set
|
||||
#\$ #\% #\& #\+ #\, ; component set
|
||||
#\/ #\: #\= #\@ #\[ #\\ #\] #\^ #\| ; userinfo set
|
||||
)
|
||||
path-set))
|
||||
|
||||
(: percent-encode (String (Listof Char) Boolean -> Bytes))
|
||||
(define (percent-encode value set space-as-plus)
|
||||
|
@ -98,3 +104,7 @@
|
|||
[(header? f) (list f)]
|
||||
[(pair? f) f]))
|
||||
fs)))
|
||||
|
||||
(: page-title->path (String -> Bytes))
|
||||
(define (page-title->path title)
|
||||
(percent-encode (regexp-replace* " " title "_") path-set #f))
|
||||
|
|
|
@ -157,20 +157,32 @@
|
|||
(define element-type (car element))
|
||||
(define attributes (bits->attributes (cdr element)))
|
||||
(define contents (filter element-is-content? (cdr element))) ; provide elements and strings
|
||||
(if (or (equal? element-type '*DECL)
|
||||
(equal? element-type '@)
|
||||
(equal? element-type '&))
|
||||
; special element, do nothing
|
||||
element
|
||||
; regular element, transform it
|
||||
(match (transformer element element-type attributes contents)
|
||||
[(list element-type attributes contents)
|
||||
(append (list element-type)
|
||||
(if (pair? attributes) (list (append '(@) attributes)) (list))
|
||||
(map (λ (content)
|
||||
(if (element-is-element? content) (loop content) content))
|
||||
contents))]))))
|
||||
(cond
|
||||
[(equal? element-type '*DECL*)
|
||||
; declarations like <!DOCTYPE html> get mapped as attributes as if the element were (*DECL* (@ (DOCTYPE) (html)))
|
||||
(match (transformer element element-type (map list (cdr element)) null)
|
||||
[(list element-type attributes contents)
|
||||
`(*DECL* ,@(map car attributes))]
|
||||
[#f ""])]
|
||||
[(member element-type '(@ &))
|
||||
; special element, do nothing
|
||||
element]
|
||||
[#t
|
||||
; regular element, transform it
|
||||
(match (transformer element element-type attributes contents)
|
||||
[(list element-type attributes contents)
|
||||
(append (list element-type)
|
||||
(if (pair? attributes) (list (append '(@) attributes)) (list))
|
||||
(map (λ (content)
|
||||
(if (element-is-element? content) (loop content) content))
|
||||
contents))])])))
|
||||
(module+ test
|
||||
; check doctype is preserved when present
|
||||
(check-equal? (update-tree (λ (e t a c) (list t a c)) '(*TOP* (*DECL* DOCTYPE html) (html (body "Hey"))))
|
||||
'(*TOP* (*DECL* DOCTYPE html) (html (body "Hey"))))
|
||||
; check doctype can be removed if desirable
|
||||
(check-equal? (update-tree (λ (e t a c) (if (eq? t '*DECL*) #f (list t a c))) '(*TOP* (*DECL* DOCTYPE html) (html (body "Hey"))))
|
||||
'(*TOP* "" (html (body "Hey"))))
|
||||
; check (& x) sequences are preserved
|
||||
(check-equal? (update-tree (λ (e t a c) (list t a c)) '(body "Hey" (& nbsp) (a (@ (href "/")))))
|
||||
'(body "Hey" (& nbsp) (a (@ (href "/"))))))
|
||||
|
|
1
static/breezewiki-favicon.svg
Normal file
1
static/breezewiki-favicon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.6 KiB |
53
static/countdown.js
Normal file
53
static/countdown.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
// countdown timer for gacha enthusiasts
|
||||
// sample: bandori/wiki/BanG_Dream!_Wikia
|
||||
// sample: ensemble-stars/wiki/The_English_Ensemble_Stars_Wiki
|
||||
|
||||
import {h, htm, render, signal, computed, effect} from "./preact.js"
|
||||
const html = htm.bind(h)
|
||||
|
||||
const now = signal(Date.now())
|
||||
setInterval(() => now.value = Date.now(), 1000)
|
||||
|
||||
const units = [
|
||||
["w", 7*24*60*60*1000],
|
||||
["d", 24*60*60*1000],
|
||||
["h", 60*60*1000],
|
||||
["m", 60*1000],
|
||||
["s", 1000]
|
||||
]
|
||||
|
||||
function getDisplayTime(datetime, now, or) {
|
||||
let difference = datetime - now
|
||||
let foundSignificantField = false
|
||||
if (difference > 0) {
|
||||
return units.map(([letter, duration], index) => {
|
||||
const multiplier = Math.floor(difference / duration)
|
||||
difference -= multiplier * duration
|
||||
if (multiplier > 0 || foundSignificantField) {
|
||||
foundSignificantField = true
|
||||
return multiplier + letter
|
||||
}
|
||||
}).filter(s => s).join(" ")
|
||||
} else if (or) {
|
||||
return or
|
||||
} else {
|
||||
return `[timer ended on ${new Date(datetime).toLocaleString()}]`
|
||||
}
|
||||
}
|
||||
|
||||
function Countdown(props) {
|
||||
return html`<span>${props.display}</span>`
|
||||
}
|
||||
|
||||
document.querySelectorAll(".countdown").forEach(eCountdown => {
|
||||
// grab information and make variables
|
||||
const eDate = eCountdown.querySelector(".countdowndate")
|
||||
const eOr = eCountdown.nextElementSibling
|
||||
const or = eOr?.textContent
|
||||
const datetime = new Date(eDate.textContent).getTime()
|
||||
// the mapped signal
|
||||
const display = computed(() => getDisplayTime(datetime, now.value, or))
|
||||
// clear content and render
|
||||
while (eDate.childNodes[0] !== undefined) eDate.childNodes[0].remove()
|
||||
render(html`<${Countdown} display=${display} />`, eDate);
|
||||
})
|
2
static/icon-theme-dark.svg
Normal file
2
static/icon-theme-dark.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg fill="currentColor" width="32" height="32" version="1.1" viewBox="0 0 8.46667 8.46667" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -288.533)"><path transform="matrix(.264583 0 0 .264583 0 288.533)" d="m23.0039 3.875-1.31445 2.27344a11 11 0 013.31055 7.85156 11 11 0 01-11 11 11 11 0 01-7.85938-3.30664l-2.26172 1.30859c2.93591 5.08516 8.77013 7.80476 14.5527 6.78516 5.78263-1.01964 10.3378-5.57482 11.3574-11.3574 1.01965-5.7826-1.7-11.6188-6.78516-14.5547z" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" shape-rendering="auto" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;paint-order:stroke fill markers;shape-padding:0;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/><circle cx="2.11667" cy="290.65" r=".529167" style="paint-order:stroke fill markers"/><circle cx="1.5875" cy="292.237" r=".529167" style="paint-order:stroke fill markers"/><circle cx="3.70417" cy="290.121" r=".529167" style="paint-order:stroke fill markers"/></g></svg>
|
After Width: | Height: | Size: 1.3 KiB |
2
static/icon-theme-default.svg
Normal file
2
static/icon-theme-default.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="32" height="32" fill="currentColor" version="1.1" viewBox="0 0 8.46667 8.46667" xmlns="http://www.w3.org/2000/svg"><path d="m4.23242.5295c-2.03949 0-3.70313 1.66559-3.70313 3.70508 0 2.03948 1.66364 3.70312 3.70313 3.70312 2.03949 0 3.70508-1.66364 3.70508-3.70312 0-2.03949-1.66559-3.70508-3.70508-3.70508zm0 1.05859c1.46752 0 2.64648 1.17897 2.64648 2.64649s-1.17896 2.64453-2.64648 2.64453c-1.46752 0-2.64453-1.17701-2.64453-2.64453s1.17701-2.64649 2.64453-2.64649z" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" shape-rendering="auto" style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;paint-order:stroke fill markers;shape-padding:0;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/><path d="m4.23333 2.64583a1.5875 1.5875 0 00-1.495 1.05937h2.99a1.5875 1.5875 0 00-1.495-1.05937zm-1.495 2.11563a1.5875 1.5875 0 001.495 1.05937 1.5875 1.5875 0 001.495-1.05937h-2.99z" style="paint-order:stroke fill markers"/></svg>
|
After Width: | Height: | Size: 1.2 KiB |
2
static/icon-theme-light.svg
Normal file
2
static/icon-theme-light.svg
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg fill="currentColor" width="32" height="32" version="1.1" viewBox="0 0 8.46667 8.46667" xmlns="http://www.w3.org/2000/svg"><path d="m6.87891 4.23458c1e-7.46447-.121282.92002-.353516 1.32226l.916016.5293c.325095-.56308.496094-1.20137.496094-1.85156z" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" shape-rendering="auto" solid-style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;paint-order:stroke fill markers;shape-padding:0;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/><circle cx="4.23333" cy="4.234" r="2.11093" style="paint-order:stroke fill markers"/><g shape-rendering="auto"><path d="m5.55695 6.52551c-.402234.23223-.858376.35329-1.32285.35329l-.0003772 1.05794c.650186 0 1.28846-.17106 1.85154-.49615z" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" solid-style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;paint-order:stroke fill markers;shape-padding:0;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/><path d="m2.91165 6.52543c-.402243-.23223-.737813-.56603-.970048-.96827l-.916389.52865c.325097.56308.794061 1.03129 1.35714 1.35639z" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" solid-style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;paint-order:stroke fill markers;shape-padding:0;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/><path d="m1.58789 4.23458c-1e-7-.46447.121282-.92198.353516-1.32422l-.916016-.5293c-.325095.56308-.496094 1.20333-.496094 1.85352z" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" solid-style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;paint-order:stroke fill markers;shape-padding:0;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/><path d="m2.91046 1.94167c.402243-.23224.858389-.35329 1.32285-.35329l-.0005978-1.05964c-.650195 1e-5-1.28847.17106-1.85155.49616z" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" solid-style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;paint-order:stroke fill markers;shape-padding:0;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/><path d="m5.55547 1.9423c.402243.23223.736121.56504.968351.96728l.916397-.52864c-.325092-.56309-.792372-1.03031-1.35545-1.35541z" color-rendering="auto" dominant-baseline="auto" image-rendering="auto" solid-style="font-feature-settings:normal;font-variant-alternates:normal;font-variant-caps:normal;font-variant-ligatures:normal;font-variant-numeric:normal;font-variant-position:normal;isolation:auto;mix-blend-mode:normal;paint-order:stroke fill markers;shape-padding:0;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-orientation:mixed;text-transform:none;white-space:normal"/></g></svg>
|
After Width: | Height: | Size: 3.8 KiB |
151
static/main.css
151
static/main.css
|
@ -1,13 +1,9 @@
|
|||
/* reset the reset */
|
||||
blockquote, code, del, details, div, dl, dt, em, fieldset, figcaption, figure, h1, h2, h3, h4, h5, h6, li, ol, p, pre, q, span, strong, ul {
|
||||
font-family: sans-serif;
|
||||
margin: initial;
|
||||
padding: initial;
|
||||
border: initial;
|
||||
}
|
||||
input, textarea {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
pre, code {
|
||||
font-family: monospace;
|
||||
font-size: 0.85em;
|
||||
|
@ -32,6 +28,7 @@ sub {
|
|||
|
||||
/* general page appearance */
|
||||
body.skin-fandomdesktop, button, input, textarea, .wikitable, .va-table {
|
||||
font-family: "Source Sans Pro", "Segoe UI", sans-serif;
|
||||
font-size: 18px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
@ -54,7 +51,7 @@ p {
|
|||
.custom-top {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.page-title {
|
||||
|
@ -77,6 +74,7 @@ p {
|
|||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--theme-page-text-color);
|
||||
word-break: break-word;
|
||||
}
|
||||
.custom-footer__cols {
|
||||
display: grid;
|
||||
|
@ -230,6 +228,7 @@ figcaption, .lightbox-caption, .thumbcaption {
|
|||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-gap: 0px 5px;
|
||||
align-items: baseline;
|
||||
}
|
||||
.bw-ss__container {
|
||||
grid-column: 2;
|
||||
|
@ -258,6 +257,9 @@ figcaption, .lightbox-caption, .thumbcaption {
|
|||
.bw-ss__list--loading {
|
||||
background: #c0c0c0;
|
||||
}
|
||||
.bw-ss__input {
|
||||
width: 100%; /* magically makes it fit the available space */
|
||||
}
|
||||
.bw-ss__input--accepted {
|
||||
background: #fffbc0;
|
||||
}
|
||||
|
@ -269,7 +271,7 @@ figcaption, .lightbox-caption, .thumbcaption {
|
|||
.bw-ss__item {
|
||||
display: grid; /* make buttons take the full size */
|
||||
}
|
||||
.bw-ss__item:hover {
|
||||
.bw-ss__button:hover, .bw-ss__button:focus {
|
||||
background-color: #ddd;
|
||||
}
|
||||
.bw-ss__button {
|
||||
|
@ -285,6 +287,102 @@ figcaption, .lightbox-caption, .thumbcaption {
|
|||
text-align: left;
|
||||
}
|
||||
|
||||
/* (breezewiki) theme selector */
|
||||
.bw-theme__select {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
grid-gap: 0px 5px;
|
||||
justify-content: right;
|
||||
align-items: baseline;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.bw-theme__items {
|
||||
display: flex;
|
||||
}
|
||||
.bw-theme__item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
padding: 2px;
|
||||
border: 1px solid var(--theme-border-color);
|
||||
border-right-width: 0px;
|
||||
background-color: var(--custom-table-background);
|
||||
color: var(--theme-page-text-color);
|
||||
transition: none;
|
||||
}
|
||||
.bw-theme__item:hover, .bw-theme__item:focus {
|
||||
/* background-color: var(--theme-page-background-color); */
|
||||
color: var(--theme-accent-color);
|
||||
}
|
||||
.bw-theme__item:first-child {
|
||||
border-radius: 4px 0px 0px 4px;
|
||||
}
|
||||
.bw-theme__item:last-child {
|
||||
border-radius: 0px 4px 4px 0px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
.bw-theme__item--selected, .bw-theme__item--selected:hover, .bw-theme__item--selected:focus {
|
||||
background-color: var(--theme-accent-color);
|
||||
color: var(--theme-accent-label-color);
|
||||
}
|
||||
.bw-theme__icon-container svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* nintendo independent wiki alliance notice */
|
||||
.niwa__notice {
|
||||
background: #fdedd8;
|
||||
color: black;
|
||||
border: 1px dashed black;
|
||||
padding: 3vw;
|
||||
margin-bottom: 3vw;
|
||||
border-radius: 6px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.niwa__header {
|
||||
font-size: max(2.9vw, 26px);
|
||||
margin-top: 0;
|
||||
}
|
||||
.niwa__notice a {
|
||||
color: #002263;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.niwa__notice .niwa__go {
|
||||
display: inline-block;
|
||||
border-radius: 20px;
|
||||
padding: 16px 26px;
|
||||
background: #f2f65f;
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
border: 2px solid black;
|
||||
box-shadow: 0 5px 0 black;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.niwa__notice .niwa__go:hover {
|
||||
color: black;
|
||||
text-decoration: underline;
|
||||
background: #dee154;
|
||||
}
|
||||
.niwa__cols {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 8px;
|
||||
}
|
||||
.niwa__logo {
|
||||
width: 150px;
|
||||
height: auto;
|
||||
}
|
||||
.niwa__divider {
|
||||
height: 1px;
|
||||
background: #808080;
|
||||
}
|
||||
.niwa__feedback {
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* media queries */
|
||||
|
||||
/* for reference, cell phone screens are generally 400 px wide, definitely less than 500 px */
|
||||
|
@ -301,6 +399,10 @@ figcaption, .lightbox-caption, .thumbcaption {
|
|||
body.skin-fandomdesktop, button, input, textarea, .wikitable, .va-table {
|
||||
font-size: 16px;
|
||||
}
|
||||
/* niwa layout */
|
||||
.niwa__right {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 560px) { /* wider than 560 px */
|
||||
|
@ -317,3 +419,40 @@ figcaption, .lightbox-caption, .thumbcaption {
|
|||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* *****
|
||||
* Custom Font - all of windows' preinstalled fonts are garbage, so I have to add a custom one
|
||||
* Source Sans Pro © 20100, 2012, 2014 Adobe Systems Incorporated
|
||||
* font files licensed under the SIL Open Font License version 1.1 http://scripts.sil.org/OFL
|
||||
* this license notice must be distributed with the font files.
|
||||
* thanks to https://github.com/majodev/google-webfonts-helper/ for the downloader!
|
||||
***** */
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: local("Source Sans Pro"),
|
||||
url("/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-regular.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url("/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.woff2") format("woff2");
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url("/static/source-sans-pro-v21-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.woff2") format("woff2");
|
||||
}
|
||||
|
|
|
@ -13,9 +13,24 @@ const htm = (function() {
|
|||
export {htm};
|
||||
|
||||
// hooks
|
||||
const { useCallback, useContext, useDebugValue, useEffect, useErrorBoundary, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useState } = (function() {
|
||||
const { useCallback, useContext, useDebugValue, useEffect, useErrorBoundary, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState } = (function() {
|
||||
var n = options;
|
||||
var t,r,u,i,o = 0,f = [],c = [],e = n.__b,a = n.__r,v = n.diffed,l = n.__c,m = n.unmount;function d(t, u) {n.__h && n.__h(r, t, o || u), o = 0;var i = r.__H || (r.__H = { __: [], __h: [] });return t >= i.__.length && i.__.push({ __V: c }), i.__[t];}function p(n) {return o = 1, y(B, n);}function y(n, u, i) {var o = d(t++, 2);if (o.t = n, !o.__c && (o.__ = [i ? i(u) : B(void 0, u), function (n) {var t = o.__N ? o.__N[0] : o.__[0],r = o.t(t, n);t !== r && (o.__N = [r, o.__[1]], o.__c.setState({}));}], o.__c = r, !r.u)) {r.u = !0;var f = r.shouldComponentUpdate;r.shouldComponentUpdate = function (n, t, r) {if (!o.__c.__H) return !0;var u = o.__c.__H.__.filter(function (n) {return n.__c;});if (u.every(function (n) {return !n.__N;})) return !f || f.call(this, n, t, r);var i = !1;return u.forEach(function (n) {if (n.__N) {var t = n.__[0];n.__ = n.__N, n.__N = void 0, t !== n.__[0] && (i = !0);}}), !(!i && o.__c.props === n) && (!f || f.call(this, n, t, r));};}return o.__N || o.__;}function h(u, i) {var o = d(t++, 3);!n.__s && z(o.__H, i) && (o.__ = u, o.i = i, r.__H.__h.push(o));}function s(u, i) {var o = d(t++, 4);!n.__s && z(o.__H, i) && (o.__ = u, o.i = i, r.__h.push(o));}function _(n) {return o = 5, F(function () {return { current: n };}, []);}function A(n, t, r) {o = 6, s(function () {return "function" == typeof n ? (n(t()), function () {return n(null);}) : n ? (n.current = t(), function () {return n.current = null;}) : void 0;}, null == r ? r : r.concat(n));}function F(n, r) {var u = d(t++, 7);return z(u.__H, r) ? (u.__V = n(), u.i = r, u.__h = n, u.__V) : u.__;}function T(n, t) {return o = 8, F(function () {return n;}, t);}function q(n) {var u = r.context[n.__c],i = d(t++, 9);return i.c = n, u ? (null == i.__ && (i.__ = !0, u.sub(r)), u.props.value) : n.__;}function x(t, r) {n.useDebugValue && n.useDebugValue(r ? r(t) : t);}function P(n) {var u = d(t++, 10),i = p();return u.__ = n, r.componentDidCatch || (r.componentDidCatch = function (n, t) {u.__ && u.__(n, t), i[1](n);}), [i[0], function () {i[1](void 0);}];}function V() {var n = d(t++, 11);return n.__ || (n.__ = "P" + function (n) {for (var t = 0, r = n.length; r > 0;) t = (t << 5) - t + n.charCodeAt(--r) | 0;return t;}(r.__v.__m) + t), n.__;}function b() {for (var t; t = f.shift();) if (t.__P && t.__H) try {t.__H.__h.forEach(k), t.__H.__h.forEach(w), t.__H.__h = [];} catch (r) {t.__H.__h = [], n.__e(r, t.__v);}}n.__b = function (n) {"function" != typeof n.type || n.__m || null === n.__ ? n.__m || (n.__m = n.__ && n.__.__m ? n.__.__m : "") : n.__m = (n.__ && n.__.__m ? n.__.__m : "") + (n.__ && n.__.__k ? n.__.__k.indexOf(n) : 0), r = null, e && e(n);}, n.__r = function (n) {a && a(n), t = 0;var i = (r = n.__c).__H;i && (u === r ? (i.__h = [], r.__h = [], i.__.forEach(function (n) {n.__N && (n.__ = n.__N), n.__V = c, n.__N = n.i = void 0;})) : (i.__h.forEach(k), i.__h.forEach(w), i.__h = [])), u = r;}, n.diffed = function (t) {v && v(t);var o = t.__c;o && o.__H && (o.__H.__h.length && (1 !== f.push(o) && i === n.requestAnimationFrame || ((i = n.requestAnimationFrame) || j)(b)), o.__H.__.forEach(function (n) {n.i && (n.__H = n.i), n.__V !== c && (n.__ = n.__V), n.i = void 0, n.__V = c;})), u = r = null;}, n.__c = function (t, r) {r.some(function (t) {try {t.__h.forEach(k), t.__h = t.__h.filter(function (n) {return !n.__ || w(n);});} catch (u) {r.some(function (n) {n.__h && (n.__h = []);}), r = [], n.__e(u, t.__v);}}), l && l(t, r);}, n.unmount = function (t) {m && m(t);var r,u = t.__c;u && u.__H && (u.__H.__.forEach(function (n) {try {k(n);} catch (n) {r = n;}}), u.__H = void 0, r && n.__e(r, u.__v));};var g = "function" == typeof requestAnimationFrame;function j(n) {var t,r = function () {clearTimeout(u), g && cancelAnimationFrame(t), setTimeout(n);},u = setTimeout(r, 100);g && (t = requestAnimationFrame(r));}function k(n) {var t = r,u = n.__c;"function" == typeof u && (n.__c = void 0, u()), r = t;}function w(n) {var t = r;n.__c = n.__(), r = t;}function z(n, t) {return !n || n.length !== t.length || t.some(function (t, r) {return t !== n[r];});}function B(n, t) {return "function" == typeof t ? t(n) : t;}
|
||||
return { useCallback: T, useContext: q, useDebugValue: x, useEffect: h, useErrorBoundary: P, useId: V, useImperativeHandle: A, useLayoutEffect: s, useMemo: F, useReducer: y, useRef: _, useState: p };
|
||||
})();
|
||||
export { useCallback, useContext, useDebugValue, useEffect, useErrorBoundary, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useState };
|
||||
export {useCallback, useContext, useDebugValue, useEffect, useErrorBoundary, useId, useImperativeHandle, useLayoutEffect, useMemo, useReducer, useRef, useState};
|
||||
|
||||
// signals-core
|
||||
const {Signal, batch, computed, effect, signal} = (function() {
|
||||
function i(){throw new Error("Cycle detected")}function t(){if(!(n>1)){var i,t=!1;while(void 0!==r){var h=r;r=void 0;s++;while(void 0!==h){var o=h.o;h.o=void 0;h.f&=-3;if(!(8&h.f)&&d(h))try{h.c()}catch(h){if(!t){i=h;t=!0}}h=o}}s=0;n--;if(t)throw i}else n--}function h(i){if(n>0)return i();n++;try{return i()}finally{t()}}var o=void 0,r=void 0,n=0,s=0,f=0;function v(i){if(void 0!==o){var t=i.n;if(void 0===t||t.t!==o){o.s=t={i:0,S:i,p:void 0,n:o.s,t:o,e:void 0,x:void 0,r:t};i.n=t;if(32&o.f)i.S(t);return t}else if(-1===t.i){t.i=0;if(void 0!==t.p){t.p.n=t.n;if(void 0!==t.n)t.n.p=t.p;t.p=void 0;t.n=o.s;o.s.p=t;o.s=t}return t}}}function e(i){this.v=i;this.i=0;this.n=void 0;this.t=void 0}e.prototype.h=function(){return!0};e.prototype.S=function(i){if(this.t!==i&&void 0===i.e){i.x=this.t;if(void 0!==this.t)this.t.e=i;this.t=i}};e.prototype.U=function(i){var t=i.e,h=i.x;if(void 0!==t){t.x=h;i.e=void 0}if(void 0!==h){h.e=t;i.x=void 0}if(i===this.t)this.t=h};e.prototype.subscribe=function(i){var t=this;return b(function(){var h=t.value,o=32&this.f;this.f&=-33;try{i(h)}finally{this.f|=o}})};e.prototype.valueOf=function(){return this.value};e.prototype.toString=function(){return this.value+""};e.prototype.peek=function(){return this.v};Object.defineProperty(e.prototype,"value",{get:function(){var i=v(this);if(void 0!==i)i.i=this.i;return this.v},set:function(h){if(h!==this.v){if(s>100)i();this.v=h;this.i++;f++;n++;try{for(var o=this.t;void 0!==o;o=o.x)o.t.N()}finally{t()}}}});function u(i){return new e(i)}function d(i){for(var t=i.s;void 0!==t;t=t.n)if(t.S.i!==t.i||!t.S.h()||t.S.i!==t.i)return!0;return!1}function c(i){for(var t=i.s;void 0!==t;t=t.n){var h=t.S.n;if(void 0!==h)t.r=h;t.S.n=t;t.i=-1}}function a(i){var t=i.s,h=void 0;while(void 0!==t){var o=t.n;if(-1===t.i){t.S.U(t);t.n=void 0}else{if(void 0!==h)h.p=t;t.p=void 0;t.n=h;h=t}t.S.n=t.r;if(void 0!==t.r)t.r=void 0;t=o}i.s=h}function l(i){e.call(this,void 0);this.x=i;this.s=void 0;this.g=f-1;this.f=4}(l.prototype=new e).h=function(){this.f&=-3;if(1&this.f)return!1;if(32==(36&this.f))return!0;this.f&=-5;if(this.g===f)return!0;this.g=f;this.f|=1;if(this.i>0&&!d(this)){this.f&=-2;return!0}var i=o;try{c(this);o=this;var t=this.x();if(16&this.f||this.v!==t||0===this.i){this.v=t;this.f&=-17;this.i++}}catch(i){this.v=i;this.f|=16;this.i++}o=i;a(this);this.f&=-2;return!0};l.prototype.S=function(i){if(void 0===this.t){this.f|=36;for(var t=this.s;void 0!==t;t=t.n)t.S.S(t)}e.prototype.S.call(this,i)};l.prototype.U=function(i){e.prototype.U.call(this,i);if(void 0===this.t){this.f&=-33;for(var t=this.s;void 0!==t;t=t.n)t.S.U(t)}};l.prototype.N=function(){if(!(2&this.f)){this.f|=6;for(var i=this.t;void 0!==i;i=i.x)i.t.N()}};l.prototype.peek=function(){if(!this.h())i();if(16&this.f)throw this.v;return this.v};Object.defineProperty(l.prototype,"value",{get:function(){if(1&this.f)i();var t=v(this);this.h();if(void 0!==t)t.i=this.i;if(16&this.f)throw this.v;return this.v}});function w(i){return new l(i)}function y(i){var h=i.u;i.u=void 0;if("function"==typeof h){n++;var r=o;o=void 0;try{h()}catch(t){i.f&=-2;i.f|=8;_(i);throw t}finally{o=r;t()}}}function _(i){for(var t=i.s;void 0!==t;t=t.n)t.S.U(t);i.x=void 0;i.s=void 0;y(i)}function g(i){if(o!==this)throw new Error("Out-of-order effect");a(this);o=i;this.f&=-2;if(8&this.f)_(this);t()}function p(i){this.x=i;this.u=void 0;this.s=void 0;this.o=void 0;this.f=32}p.prototype.c=function(){var i=this.S();try{if(!(8&this.f)&&void 0!==this.x)this.u=this.x()}finally{i()}};p.prototype.S=function(){if(1&this.f)i();this.f|=1;this.f&=-9;y(this);c(this);n++;var t=o;o=this;return g.bind(this,t)};p.prototype.N=function(){if(!(2&this.f)){this.f|=2;this.o=r;r=this}};p.prototype.d=function(){this.f|=8;if(!(1&this.f))_(this)};function b(i){var t=new p(i);t.c();return t.d.bind(t)}
|
||||
return {Signal: e, batch: h, computed: w, effect: b, signal: u};
|
||||
})();
|
||||
export {Signal, batch, computed, effect, signal};
|
||||
|
||||
// signals
|
||||
const {useComputed, useSignal, useSignalEffect} = (function() {
|
||||
var n = Component, i = options, r = useMemo, t = useRef, f = useEffect, o = Signal, e = computed, u = signal, a = effect;
|
||||
var c,v;function s(n,r){i[n]=r.bind(null,i[n]||function(){})}function l(n){if(v)v();v=n&&n.S()}function p(n){var i=this,t=n.data,f=useSignal(t);f.value=t;var o=r(function(){var n=i.__v;while(n=n.__)if(n.__c){n.__c.__$f|=4;break}i.__$u.c=function(){i.base.data=o.peek()};return e(function(){var n=f.value.value;return 0===n?0:!0===n?"":n||""})},[]);return o.value}p.displayName="_st";Object.defineProperties(o.prototype,{constructor:{configurable:!0},type:{configurable:!0,value:p},props:{configurable:!0,get:function(){return{data:this}}},__b:{configurable:!0,value:1}});s("__b",function(n,i){if("string"==typeof i.type){var r,t=i.props;for(var f in t)if("children"!==f){var e=t[f];if(e instanceof o){if(!r)i.__np=r={};r[f]=e;t[f]=e.peek()}}}n(i)});s("__r",function(n,i){l();var r,t=i.__c;if(t){t.__$f&=-2;if(void 0===(r=t.__$u))t.__$u=r=function(n){var i;a(function(){i=this});i.c=function(){t.__$f|=1;t.setState({})};return i}()}c=t;l(r);n(i)});s("__e",function(n,i,r,t){l();c=void 0;n(i,r,t)});s("diffed",function(n,i){l();c=void 0;var r;if("string"==typeof i.type&&(r=i.__e)){var t=i.__np,f=i.props;if(t){var o=r.U;if(o)for(var e in o){var u=o[e];if(void 0!==u&&!(e in t)){u.d();o[e]=void 0}}else r.U=o={};for(var a in t){var v=o[a],s=t[a];if(void 0===v){v=d(r,a,s,f);o[a]=v}else v.o(s,f)}}}n(i)});function d(n,i,r,t){var f=i in n&&void 0===n.ownerSVGElement,o=u(r);return{o:function(n,i){o.value=n;t=i},d:a(function(){var r=o.value.value;if(t[i]!==r){t[i]=r;if(f)n[i]=r;else if(r)n.setAttribute(i,r);else n.removeAttribute(i)}})}}s("unmount",function(n,i){if("string"==typeof i.type){var r=i.__e;if(r){var t=r.U;if(t){r.U=void 0;for(var f in t){var o=t[f];if(o)o.d()}}}}else{var e=i.__c;if(e){var u=e.__$u;if(u){e.__$u=void 0;u.d()}}}n(i)});s("__h",function(n,i,r,t){if(t<3)i.__$f|=2;n(i,r,t)});n.prototype.shouldComponentUpdate=function(n,i){var r=this.__$u;if(!(r&&void 0!==r.s||4&this.__$f))return!0;if(3&this.__$f)return!0;for(var t in i)return!0;for(var f in n)if("__source"!==f&&n[f]!==this.props[f])return!0;for(var o in this.props)if(!(o in n))return!0;return!1};function useSignal(n){return r(function(){return u(n)},[])}function useComputed(n){var i=t(n);i.current=n;c.__$f|=4;return r(function(){return e(function(){return i.current()})},[])}function useSignalEffect(n){var i=t(n);i.current=n;f(function(){return a(function(){i.current()})},[])}
|
||||
return {useComputed, useSignal, useSignalEffect}
|
||||
})();
|
||||
export {useComputed, useSignal, useSignalEffect};
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
import {h, htm, render, useState, useEffect, createContext, useContext} from "./preact.js"
|
||||
import {h, htm, render, signal, computed, effect} from "./preact.js"
|
||||
const html = htm.bind(h)
|
||||
const classNames = classArr => classArr.filter(el => el).join(" ")
|
||||
|
||||
const form = document.getElementById("bw-pr-search")
|
||||
const eForm = document.getElementById("bw-pr-search-form")
|
||||
const eInput = document.getElementById("bw-pr-search-input")
|
||||
const eSuggestions = document.getElementById("bw-pr-search-suggestions")
|
||||
|
||||
const AcceptSuggestion = createContext(null)
|
||||
const hitsPromise = new Map()
|
||||
const hitsDone = new Set()
|
||||
|
||||
function Suggestion(props) {
|
||||
const acceptSuggestion = useContext(AcceptSuggestion)
|
||||
return html`<li class="bw-ss__item"><button type="button" class="bw-ss__button" onClick=${() => acceptSuggestion(props)}>${props.title}</button></li>`
|
||||
}
|
||||
const query = signal("")
|
||||
const focus = signal(false)
|
||||
const st = signal("ready")
|
||||
const suggestions = signal([])
|
||||
|
||||
// processing functions
|
||||
|
||||
function fetchSuggestions(query, setSuggestions) {
|
||||
if (query === "") query = "\0"
|
||||
|
@ -36,58 +39,53 @@ function fetchSuggestions(query, setSuggestions) {
|
|||
return promise
|
||||
}
|
||||
|
||||
function SuggestionList(props) {
|
||||
return html`
|
||||
<div class="bw-ss__container">
|
||||
<ul class=${classNames(["bw-ss__list", props.focus && "bw-ss__list--focus", `bw-ss__list--${props.st}`])}>
|
||||
${props.hits.map(hit => html`<${Suggestion} ...${hit} />`)}
|
||||
</ul>
|
||||
</div>`
|
||||
function acceptSuggestion(hit) {
|
||||
st.value = "accepted"
|
||||
query.value = hit.title
|
||||
const dest = new URL(hit.url).pathname.match("/wiki/.*")
|
||||
location = `/${BWData.wikiname}${dest}`
|
||||
}
|
||||
|
||||
function ControlledInput() {
|
||||
const [query, setQuery] = useState("")
|
||||
const [focus, setFocus] = useState(false)
|
||||
const [st, setSt] = useState("ready")
|
||||
const [suggestions, setSuggestions] = useState([])
|
||||
// suggestion list view
|
||||
|
||||
useEffect(() => {
|
||||
if (st === "accepted") return
|
||||
setSt("loading")
|
||||
fetchSuggestions(query).then(s => {
|
||||
setSuggestions(s)
|
||||
if (hitsDone.size === hitsPromise.size) {
|
||||
setSt("ready")
|
||||
}
|
||||
})
|
||||
}, [query])
|
||||
function Suggestion(hit) {
|
||||
return html`<li class="bw-ss__item"><button type="button" class="bw-ss__button" onClick=${() => acceptSuggestion(hit)}>${hit.title}</button></li>`
|
||||
}
|
||||
|
||||
function acceptSuggestion(suggestion) {
|
||||
setQuery(suggestion.title)
|
||||
setSt("accepted")
|
||||
const dest = new URL(suggestion.url).pathname.match("/wiki/.*")
|
||||
location = `/${BWData.wikiname}${dest}`
|
||||
}
|
||||
function SuggestionList() {
|
||||
return html`
|
||||
<ul class=${classNames(["bw-ss__list", focus.value && "bw-ss__list--focus", `bw-ss__list--${st.value}`])}>
|
||||
${suggestions.value.map(hit => html`<${Suggestion} ...${hit} />`)}
|
||||
</ul>`
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function listener(event) {
|
||||
if (event.type === "focusin") setFocus(true)
|
||||
else setFocus(false)
|
||||
}
|
||||
form.addEventListener("focusin", listener)
|
||||
form.addEventListener("focusout", listener)
|
||||
return () => {
|
||||
form.removeEventListener("focusin", listener)
|
||||
form.removeEventListener("focusout", listener)
|
||||
render(html`<${SuggestionList} />`, eSuggestions)
|
||||
|
||||
// input view
|
||||
|
||||
effect(() => {
|
||||
if (st.peek() === "accepted") return // lock results from changing during navigation
|
||||
st.value = "loading"
|
||||
fetchSuggestions(query.value).then(res => {
|
||||
suggestions.value = res
|
||||
if (hitsDone.size === hitsPromise.size) {
|
||||
st.value = "ready"
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener("pageshow", () => {
|
||||
st.value = "ready" // unlock results from changing after returning to page
|
||||
})
|
||||
|
||||
function SuggestionInput() {
|
||||
return html`
|
||||
<${AcceptSuggestion.Provider} value=${acceptSuggestion}>
|
||||
<label for="bw-search-input">Search </label>
|
||||
<input type="text" name="q" id="bw-search-input" autocomplete="off" onInput=${e => setQuery(e.target.value)} value=${query} class=${classNames(["bw-ss__input", `bw-ss__input--${st}`])} />
|
||||
<${SuggestionList} hits=${suggestions} focus=${focus} st=${st}/>
|
||||
</${AcceptSuggestion.Provider}`
|
||||
<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`<${ControlledInput} />`, form)
|
||||
render(html`<${SuggestionInput} />`, eInput)
|
||||
|
||||
// form focus
|
||||
|
||||
eForm.addEventListener("focusin", () => focus.value = true)
|
||||
eForm.addEventListener("focusout", () => focus.value = false)
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in a new issue