Compare commits

..

30 commits

Author SHA1 Message Date
95418613e5
Loosen criteria for noscript images 2022-12-04 19:31:34 +07:00
f216a1996a
Stop redirecting pages if there's a redirect=no parameter 2022-12-05 00:15:03 +13:00
99b5d8d8f6
Allow breaking lines in the URL in the footer 2022-12-04 23:55:18 +13:00
9fd2b4699d
Optimise update-tree-wiki somewhat 2022-12-04 23:46:24 +13:00
ba3b39242c
Rename ref parameter to next_location 2022-12-04 23:44:35 +13:00
a661ddb313
Make the search box magically fit the space 2022-11-30 00:37:07 +13:00
50a3bc819a
Fix tests by providing a fake req 2022-11-30 00:36:53 +13:00
9afccbb9cd
Support light/dark themes as per Fandom's styles 2022-11-30 00:03:54 +13:00
92591a5eab
Fix some special characters in titles like ? and ; 2022-11-17 23:25:06 +13:00
1c83c0b4d3
Support X-Canonical-Origin for instance admins 2022-11-15 22:21:12 +13:00
324e34eb72
Remove unused imports 2022-11-15 20:50:07 +13:00
bf6efde979
Add countdown timer for gacha enthusiasts 2022-11-15 20:46:56 +13:00
ef12faf72d
Uncollapse more wiki sections 2022-11-15 20:45:44 +13:00
8c7a045830
NIWA notice: say brought to you by breezewiki 2022-11-13 22:52:29 +13:00
565d0a439a
Add types to Link header 2022-11-11 23:33:56 +13:00
cea9d2082b
Try to add Link header to preload resources 2022-11-11 23:08:06 +13:00
ac17f40329
Fix font local definitions (?) 2022-11-11 22:34:45 +13:00
b0e90f5cf9
Use Source Sans Pro font, selfhosted 2022-11-10 23:55:31 +13:00
02819a7459
Allow searching for nothing on BreezeWiki homepage 2022-11-10 23:20:09 +13:00
aab52bd92b
Use <!DOCTYPE html> standards mode on all pages 2022-11-05 23:40:05 +13:00
1219334d06
New icon, specifically designed for small size 2022-10-31 20:03:50 +13:00
e812f2082c
Deduplicate body-class logic into head-data struct 2022-10-31 19:39:19 +13:00
645fe1beee
Search results page use query as title 2022-10-31 00:03:13 +13:00
15b41c24f7
Add breezewiki-icon.svg 2022-10-31 00:00:07 +13:00
63d37d5e4f
Fix: "Cache-busting" for static files 2022-10-30 23:56:25 +13:00
d3c5498d47
"Cache-busting" for static files 2022-10-30 23:51:15 +13:00
8b200d621a
Link out to NIWA's wikis where available 2022-10-30 23:15:26 +13:00
3c7a2f8453
Rewrite search suggestions module 2022-10-24 18:13:54 +13:00
d8d4e4375e
Fix imports in wiki name downloader 2022-10-24 18:13:14 +13:00
bf055836cc
Add download wiki names script 2022-10-24 00:22:47 +13:00
35 changed files with 1196 additions and 443 deletions

View file

@ -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

View file

@ -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

View file

@ -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"))

View 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)))

View file

@ -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

View file

@ -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))))))

View file

@ -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
View 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)))

View file

@ -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"

View file

@ -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

View file

@ -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)))))])))

View file

@ -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)))

View file

@ -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)))))

View 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)))

View file

@ -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))

View file

@ -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)]))

View file

@ -19,6 +19,7 @@
"data.rkt"
"pure-utils.rkt"
"syntax.rkt"
"tree-updater.rkt"
"xexpr-utils.rkt"
"url-utils.rkt")
@ -30,56 +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") (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"))
(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>")
@ -87,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 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
((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))
@ -284,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
@ -294,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
View 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 ", ")))

View file

@ -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
View 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
View 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")))))))

View file

@ -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))

View file

@ -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 "/"))))))

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.6 KiB

53
static/countdown.js Normal file
View 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);
})

View 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

View 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

View 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

View file

@ -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");
}

View file

@ -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};

View file

@ -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)