diff --git a/src/html-utils.rkt b/src/html-utils.rkt
index e49f317..717e04b 100644
--- a/src/html-utils.rkt
+++ b/src/html-utils.rkt
@@ -14,7 +14,20 @@
;; Extracts named single-valued bindings from the given request.
;; If a given binding is missing, the extracted value will be #f.
-(define-syntax-rule (define-form-bindings req (name ...))
+(define-syntax-rule (define-form-bindings req (specs ...))
(begin (define bs (request-bindings req))
- (define name (and (exists-binding? 'name bs) (extract-binding/single 'name bs)))
- ...))
+ (define-form-bindings* bs (specs ...))))
+
+(define-syntax define-form-bindings*
+ (syntax-rules ()
+ [(_ bs ())
+ (begin)]
+ [(_ bs ([name fieldname defaultval] rest ...))
+ (begin (define name (if (exists-binding? 'fieldname bs)
+ (extract-binding/single 'fieldname bs)
+ defaultval))
+ (define-form-bindings* bs (rest ...)))]
+ [(_ bs ([name defaultval] rest ...))
+ (define-form-bindings* bs ([name name defaultval] rest ...))]
+ [(_ bs (name rest ...))
+ (define-form-bindings* bs ([name #f] rest ...))]))
diff --git a/src/main.rkt b/src/main.rkt
index 37c4dc3..e6fe5a6 100644
--- a/src/main.rkt
+++ b/src/main.rkt
@@ -5,6 +5,8 @@
(require racket/format)
(require racket/date)
(require racket/port)
+(require racket/string)
+(require net/uri-codec)
(require web-server/servlet)
(require "bootstrap.rkt")
(require "html-utils.rkt")
@@ -51,6 +53,7 @@
(define-values (request-handler named-url)
(dispatch-rules
[("") main-page]
+ [("search") search-page]
[("package" (string-arg)) package-page]
))
@@ -64,9 +67,6 @@
(define package-name-str (~a package-name))
`(a ((href ,(named-url package-page package-name-str))) ,package-name-str))
-(define (author-link author-name)
- `(a ((href "TODO")) ,author-name))
-
(define (doc-destruct doc)
(match doc
[(list _ n u) (values n u)]
@@ -78,8 +78,16 @@
(buildhost-link docset-url docset-name)
`(del ,docset-name)))
+(define (tags-page-url tags)
+ (format "~a?~a"
+ (named-url search-page)
+ (alist->form-urlencoded (list (cons 'tags (string-join tags))))))
+
+(define (author-link author-name)
+ `(a ((href ,(tags-page-url (list (format "author:~a" author-name))))) ,author-name))
+
(define (tag-link tag-name)
- `(a ((href "TODO")) ,tag-name))
+ `(a ((href ,(tags-page-url (list tag-name)))) ,tag-name))
(define (buildhost-link #:attributes [attributes '()] url-suffix label)
`(a (,@attributes
@@ -104,15 +112,52 @@
(define (utc->string utc)
(string-append (date->string (seconds->date utc #f) #t) " (UTC)"))
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-(define-syntax @
- (syntax-rules ()
- [(_ v) v]
- [(_ v k rest ...) (@ (@ref v 'k) rest ...)]))
-
-(define (@ref v k)
- (and v (hash-ref v k (lambda () #f))))
+(define (package-summary-table package-names)
+ `(table
+ ((class "packages sortable"))
+ (tr
+ (th "Package")
+ (th "Description")
+ (th "Build"))
+ ,@(maybe-splice (null? package-names)
+ `(tr (td ((colspan "3"))
+ (div ((class "alert alert-info"))
+ "No packages found."))))
+ ,@(for/list ((package-name package-names))
+ (define pkg (package-detail package-name))
+ `(tr
+ (td (h2 ,(package-link package-name))
+ ,(authors-list (@ pkg authors))
+ ;; recently-updated?
+ )
+ (td (p ,(@ pkg description))
+ ,@(maybe-splice
+ (pair? (@ pkg build docs))
+ `(div
+ (span ((class "doctags-label")) "Docs: ")
+ ,(doc-links (@ pkg build docs))))
+ ,@(maybe-splice
+ (pair? (@ pkg tags))
+ `(div
+ (span ((class "doctags-label")) "Tags: ")
+ ,(tag-links (@ pkg tags)))))
+ ,(cond
+ [(@ pkg build failure-log)
+ `(td ((class "build_red"))
+ ,(buildhost-link (@ pkg build failure-log) "fails"))]
+ [(and (@ pkg build success-log)
+ (@ pkg build dep-failure-log))
+ `(td ((class "build_yellow"))
+ ,(buildhost-link (@ pkg build success-log)
+ "succeeds")
+ " with "
+ ,(buildhost-link (@ pkg build dep-failure-log)
+ "dependency problems"))]
+ [(@ pkg build success-log)
+ `(td ((class "build_green"))
+ ,(buildhost-link (@ pkg build success-log) "succeeds"))]
+ [else
+ `(td)])))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -121,51 +166,22 @@
#:title-element ""
`(div ((class "jumbotron"))
(h1 "Racket Package Index")
- (p "These are the currently-registered packages available via the "
+ (p "These are the packages available via the "
(a ((href "docs.racket-lang.org/pkg/getting-started.html"))
- "Racket package system") "."))
-
- `(table
- ((class "packages sortable"))
- (tr
- (th "Package")
- (th "Description")
- (th "Build"))
- ,@(for/list ((package-name (sorted-package-names)))
- (define pkg (package-detail package-name))
- `(tr
- (td (h2 ,(package-link package-name))
- ,(authors-list (@ pkg authors))
- ;; recently-updated?
- )
- (td (p ,(@ pkg description))
- ,@(maybe-splice
- (pair? (@ pkg build docs))
- `(div
- (span ((class "doctags-label")) "Docs: ")
- ,(doc-links (@ pkg build docs))))
- ,@(maybe-splice
- (pair? (@ pkg tags))
- `(div
- (span ((class "doctags-label")) "Tags: ")
- ,(tag-links (@ pkg tags)))))
- ,(cond
- [(@ pkg build failure-log)
- `(td ((class "build_red"))
- ,(buildhost-link (@ pkg build failure-log) "fails"))]
- [(and (@ pkg build success-log)
- (@ pkg build dep-failure-log))
- `(td ((class "build_yellow"))
- ,(buildhost-link (@ pkg build success-log)
- "succeeds")
- " with "
- ,(buildhost-link (@ pkg build dep-failure-log)
- "dependency problems"))]
- [(@ pkg build success-log)
- `(td ((class "build_green"))
- ,(buildhost-link (@ pkg build success-log) "succeeds"))]
- [else
- `(td)]))))))
+ "Racket package system") ".")
+ (p "Simply run " (kbd "raco pkg install " (var "package-name"))
+ " to install a package.")
+ (form ((role "form")
+ (action ,(named-url search-page)))
+ (div ((class "form-group"))
+ (input ((class "form-control")
+ (type "text")
+ (placeholder "Search packages")
+ (name "q")
+ (value "")
+ (id "q"))))
+ ))
+ (package-summary-table (package-search "" '((main-distribution #f))))))
(define (package-page request package-name-str)
(define package-name (string->symbol package-name-str))
@@ -230,7 +246,7 @@
(tr (th "Documentation")
(td ,(doc-links (@ pkg build docs))))
(tr (th "Tags")
- (td ,@(for/list ((tag (@ pkg tags))) (tag-link tag))))
+ (td ,(tag-links (@ pkg tags))))
(tr (th "Last updated")
(td ,(utc->string (@ pkg last-updated))))
(tr (th "Ring")
@@ -288,3 +304,46 @@
(tr (th "Last edited")
(td ,(utc->string (@ pkg last-edit))))
))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define (search-page request)
+ (define-form-bindings request ([search-text q ""]
+ [tags-input tags ""]))
+ (define tags (for/list ((t (string-split tags-input)))
+ (match t
+ [(pregexp #px"!(.*)" (list _ tag)) (list (string->symbol tag) #f)]
+ [tag (list (string->symbol tag) #t)])))
+ (bootstrap-response "Search Racket Package Index"
+ `(form ((class "form-horizontal")
+ (role "form"))
+ (div ((class "form-group"))
+ (label ((class "col-sm-2 control-label")
+ (for "q")) "Search terms")
+ (div ((class "col-sm-10"))
+ (input ((class "form-control")
+ (type "text")
+ (placeholder "Enter free-form text to match here")
+ (name "q")
+ (value ,search-text)
+ (id "q")))))
+ (div ((class "form-group"))
+ (label ((class "col-sm-2 control-label")
+ (for "tags")) "Tags")
+ (div ((class "col-sm-10"))
+ (input ((class "form-control")
+ (type "text")
+ (placeholder "tag1 tag2 tag3 ...")
+ (name "tags")
+ (value ,tags-input)
+ (id "tags")))))
+ (div ((class "form-group"))
+ (div ((class "col-sm-offset-2 col-sm-10"))
+ (button ((type "submit")
+ (class "btn btn-primary"))
+ (span ((class "glyphicon glyphicon-search")))
+ " Search")))
+ (div ((class "search-results"))
+ ,@(maybe-splice
+ (or (pair? tags) (not (equal? search-text "")))
+ (package-summary-table (package-search search-text tags)))))))
diff --git a/src/packages.rkt b/src/packages.rkt
index 42624c2..d2c6cb2 100644
--- a/src/packages.rkt
+++ b/src/packages.rkt
@@ -1,29 +1,92 @@
#lang racket/base
-(provide all-package-names
+(provide @
+ @ref
+ all-package-names
+ all-tags
sorted-package-names
package-detail
+ package-search
refresh-packages!)
(require json)
+(require racket/set)
+(require racket/match)
(require racket/file)
+(require racket/string)
+(require racket/list)
(require web-server/private/gzip)
-(define packages #f)
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(define (refresh-packages!)
- (set! packages (bytes->jsexpr (gunzip/bytes (file->bytes "../pkgs-all.json.gz")))))
+(define-syntax @
+ (syntax-rules ()
+ [(_ v) v]
+ [(_ v k rest ...) (@ (@ref v 'k) rest ...)]))
+
+(define (@ref v k)
+ (and v (hash-ref v k (lambda () #f))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(define packages (hash))
+(define all-tags* (set))
(define (all-package-names)
(hash-keys packages))
+(define (all-tags)
+ all-tags*)
+
+(define (sort-package-names names)
+ (sort names (lambda (a b) (string (symbol->string a) (symbol->string b)))))
+
(define (sorted-package-names)
- (sort (all-package-names)
- (lambda (a b)
- (string (symbol->string a) (symbol->string b)))))
+ (sort-package-names (all-package-names)))
(define (package-detail package-name)
(hash-ref packages package-name (lambda () #f)))
+(define (pkg->searchable-text pkg)
+ (string-join (flatten (list (or (@ pkg authors) '())
+ (map (match-lambda
+ [(list _ path) path]
+ [_ '()])
+ (or (@ pkg modules) '()))
+ (or (@ pkg name) '())
+ (or (@ pkg description) '())
+ (or (@ pkg source) '())
+ (or (@ pkg tags) '())
+ (map (match-lambda
+ [(list _ n _) n]
+ [_ '()])
+ (or (@ pkg build docs) '()))))))
+
+(define ((package-text-matches? pkg) re)
+ (regexp-match? re (@ pkg _SEARCHABLE-TEXT_)))
+
+(define (package-search text tags)
+ (define res (map (lambda (r) (pregexp (format "(?i:~a)" r))) (string-split text)))
+ (sort-package-names
+ (filter (lambda (package-name)
+ (define pkg (hash-ref packages package-name))
+ (andmap (package-text-matches? pkg) res))
+ (hash-keys
+ (for/fold ((ps packages)) ((tag-spec tags))
+ (match-define (list tag-name include?) tag-spec)
+ (for/hash (((package-name pkg) (in-hash ps))
+ #:when ((if include? values not) (@ref (@ pkg search-terms) tag-name)))
+ (values package-name pkg)))))))
+
+(define (refresh-packages!)
+ (set! packages
+ (for/hash (((package-name pkg)
+ (in-hash (bytes->jsexpr (gunzip/bytes (file->bytes "../pkgs-all.json.gz"))))))
+ (values package-name
+ (hash-set pkg '_SEARCHABLE-TEXT_ (pkg->searchable-text pkg)))))
+ (set! all-tags*
+ (for/fold ((ts (set))) ((pkg (in-hash-values packages)))
+ (set-union ts (list->set (or (@ pkg tags) '()))))))
+
(refresh-packages!)
diff --git a/static/style.css b/static/style.css
index b4200d9..3b9042a 100644
--- a/static/style.css
+++ b/static/style.css
@@ -43,3 +43,5 @@ ul.build-results,
ul.module-list {
list-style: none; padding: 0;
}
+
+.search-results table { margin-top: 3em; }