Support search engine escaping of spaces (+) (#215)

This commit is contained in:
Marek Fajkus 2020-10-28 19:51:01 +01:00 committed by GitHub
parent adbb224490
commit e412085ea8
Failed to generate hash of commit
6 changed files with 207 additions and 100 deletions

View file

@ -1,7 +1,5 @@
module Main exposing (main) module Main exposing (main)
--exposing (UrlRequest(..))
import Browser import Browser
import Browser.Navigation import Browser.Navigation
import Html import Html

View file

@ -43,6 +43,7 @@ import Html.Parser.Util
import Json.Decode import Json.Decode
import Json.Encode import Json.Encode
import Regex import Regex
import Route
import Search import Search
@ -102,7 +103,7 @@ update navKey msg model =
let let
( newModel, newCmd ) = ( newModel, newCmd ) =
Search.update Search.update
"options" Route.Options
navKey navKey
subMsg subMsg
model model
@ -116,8 +117,7 @@ update navKey msg model =
view : Model -> Html Msg view : Model -> Html Msg
view model = view model =
Search.view Search.view { toRoute = Route.Options, categoryName = "options" }
"options"
"Search NixOS options" "Search NixOS options"
model model
viewSuccess viewSuccess

View file

@ -43,6 +43,7 @@ import Json.Decode
import Json.Decode.Pipeline import Json.Decode.Pipeline
import Json.Encode import Json.Encode
import Regex import Regex
import Route
import Search import Search
@ -139,7 +140,7 @@ update navKey msg model =
let let
( newModel, newCmd ) = ( newModel, newCmd ) =
Search.update Search.update
"packages" Route.Packages
navKey navKey
subMsg subMsg
model model
@ -153,8 +154,7 @@ update navKey msg model =
view : Model -> Html Msg view : Model -> Html Msg
view model = view model =
Search.view Search.view { toRoute = Route.Packages, categoryName = "packages" }
"packages"
"Search NixOS packages" "Search NixOS packages"
model model
viewSuccess viewSuccess

View file

@ -1,9 +1,11 @@
module Route exposing (Route(..), fromUrl, href, replaceUrl) module Route exposing (Route(..), fromUrl, href, replaceUrl, routeToString)
import Browser.Navigation import Browser.Navigation
import Html import Html
import Html.Attributes import Html.Attributes
import Route.SearchQuery
import Url import Url
import Url.Builder exposing (QueryParameter)
import Url.Parser exposing ((<?>)) import Url.Parser exposing ((<?>))
import Url.Parser.Query import Url.Parser.Query
@ -19,30 +21,32 @@ type Route
| Options (Maybe String) (Maybe String) (Maybe String) (Maybe Int) (Maybe Int) (Maybe String) | Options (Maybe String) (Maybe String) (Maybe String) (Maybe Int) (Maybe Int) (Maybe String)
parser : Url.Parser.Parser (Route -> msg) msg parser : Url.Url -> Url.Parser.Parser (Route -> msg) msg
parser = parser url =
let
rawQuery =
Route.SearchQuery.toRawQuery url
withSearchQuery : (a -> Maybe String -> b) -> a -> b
withSearchQuery f channel =
f channel <|
Maybe.andThen Route.SearchQuery.searchQueryToString <|
Maybe.andThen (Route.SearchQuery.searchString "query") rawQuery
in
Url.Parser.oneOf Url.Parser.oneOf
[ Url.Parser.map [ Url.Parser.map Home Url.Parser.top
Home , Url.Parser.map NotFound (Url.Parser.s "not-found")
Url.Parser.top , Url.Parser.map (withSearchQuery Packages)
, Url.Parser.map
NotFound
(Url.Parser.s "not-found")
, Url.Parser.map
Packages
(Url.Parser.s "packages" (Url.Parser.s "packages"
<?> Url.Parser.Query.string "channel" <?> Url.Parser.Query.string "channel"
<?> Url.Parser.Query.string "query"
<?> Url.Parser.Query.string "show" <?> Url.Parser.Query.string "show"
<?> Url.Parser.Query.int "from" <?> Url.Parser.Query.int "from"
<?> Url.Parser.Query.int "size" <?> Url.Parser.Query.int "size"
<?> Url.Parser.Query.string "sort" <?> Url.Parser.Query.string "sort"
) )
, Url.Parser.map , Url.Parser.map (withSearchQuery Options)
Options
(Url.Parser.s "options" (Url.Parser.s "options"
<?> Url.Parser.Query.string "channel" <?> Url.Parser.Query.string "channel"
<?> Url.Parser.Query.string "query"
<?> Url.Parser.Query.string "show" <?> Url.Parser.Query.string "show"
<?> Url.Parser.Query.int "from" <?> Url.Parser.Query.int "from"
<?> Url.Parser.Query.int "size" <?> Url.Parser.Query.int "size"
@ -71,7 +75,7 @@ fromUrl url =
-- This makes it *literally* the path, so we can proceed -- This makes it *literally* the path, so we can proceed
-- with parsing as if it had been a normal path all along. -- with parsing as if it had been a normal path all along.
--{ url | path = Maybe.withDefault "" url.fragment, fragment = Nothing } --{ url | path = Maybe.withDefault "" url.fragment, fragment = Nothing }
Url.Parser.parse parser url Url.Parser.parse (parser url) url
@ -79,41 +83,63 @@ fromUrl url =
routeToString : Route -> String routeToString : Route -> String
routeToString page = routeToString =
let let
( path, query ) = buildString ( path, query, searchQuery ) =
routeToPieces page Route.SearchQuery.absolute path query <|
Maybe.withDefault [] <|
Maybe.map List.singleton searchQuery
in in
"/" ++ String.join "/" path ++ "?" ++ String.join "&" (List.filterMap Basics.identity query) buildString << routeToPieces
routeToPieces : Route -> ( List String, List (Maybe String) ) routeToPieces : Route -> ( List String, List QueryParameter, Maybe ( String, Route.SearchQuery.SearchQuery ) )
routeToPieces page = routeToPieces page =
case page of let
Home -> channelQ =
( [], [] ) Maybe.map (Url.Builder.string "channel")
NotFound -> queryQ =
( [ "not-found" ], [] ) Maybe.map (Route.SearchQuery.toSearchQuery "query")
Packages channel query show from size sort -> showQ =
( [ "packages" ] Maybe.map (Url.Builder.string "show")
, [ channel
, query
, show
, Maybe.map String.fromInt from
, Maybe.map String.fromInt size
, sort
]
)
Options channel query show from size sort -> fromQ =
( [ "options" ] Maybe.map (Url.Builder.int "from")
, [ channel
, query sizeQ =
, show Maybe.map (Url.Builder.int "size")
, Maybe.map String.fromInt from
, Maybe.map String.fromInt size sortQ =
, sort Maybe.map (Url.Builder.string "sort")
] in
) (\( path, urlQ, searchQuery ) -> ( path, List.filterMap identity urlQ, searchQuery )) <|
case page of
Home ->
( [], [], Nothing )
NotFound ->
( [ "not-found" ], [], Nothing )
Packages channel query show from size sort ->
( [ "packages" ]
, [ channelQ channel
, showQ show
, fromQ from
, sizeQ size
, sortQ sort
]
, queryQ query
)
Options channel query show from size sort ->
( [ "options" ]
, [ channelQ channel
, showQ show
, fromQ from
, sizeQ size
, sortQ sort
]
, queryQ query
)

87
src/Route/SearchQuery.elm Normal file
View file

@ -0,0 +1,87 @@
module Route.SearchQuery exposing
( RawQuery
, SearchQuery
, absolute
, searchQueryToString
, searchString
, toRawQuery
, toSearchQuery
)
import Dict exposing (Dict)
import Url
import Url.Builder
-- RawQuery
type RawQuery
= RawQuery (Dict String String)
chunk : String -> String -> Maybe ( String, String )
chunk sep str =
case String.split sep str of
[] ->
Nothing
[ key ] ->
Just ( key, "" )
key :: xs ->
Just ( key, String.join sep xs )
toRawQuery : Url.Url -> Maybe RawQuery
toRawQuery =
Maybe.map (RawQuery << Dict.fromList << List.filterMap (chunk "=") << String.split "&")
<< .query
-- SearchQuery
{-| This is type safe wrapper for working with search queries in url
-}
type SearchQuery
= SearchQuery String
searchString : String -> RawQuery -> Maybe SearchQuery
searchString name (RawQuery dict) =
Maybe.map SearchQuery <| Dict.get name dict
searchQueryToString : SearchQuery -> Maybe String
searchQueryToString (SearchQuery str) =
Url.percentDecode <| String.replace "+" "%20" str
toSearchQuery : String -> String -> ( String, SearchQuery )
toSearchQuery name query =
( name, SearchQuery <| String.replace "%20" "+" <| Url.percentEncode query )
{-| Build absolute URL with support for search query strings
-}
absolute : List String -> List Url.Builder.QueryParameter -> List ( String, SearchQuery ) -> String
absolute path query searchQuery =
let
searchStrings =
List.map (\( name, SearchQuery val ) -> name ++ "=" ++ val) searchQuery
|> String.join "&"
in
Url.Builder.absolute path query
|> (\str ->
str
++ (case query of
[] ->
"?" ++ searchStrings
_ ->
"&" ++ searchStrings
)
)

View file

@ -62,11 +62,23 @@ import Http
import Json.Decode import Json.Decode
import Json.Encode import Json.Encode
import RemoteData import RemoteData
import Route exposing (Route)
import Set import Set
import Task import Task
import Url
import Url.Builder import Url.Builder
type alias SearchRoute =
Maybe String
-> Maybe String
-> Maybe String
-> Maybe Int
-> Maybe Int
-> Maybe String
-> Route
type alias Model a = type alias Model a =
{ channel : String { channel : String
, query : Maybe String , query : Maybe String
@ -174,12 +186,12 @@ type Msg a
update : update :
String SearchRoute
-> Browser.Navigation.Key -> Browser.Navigation.Key
-> Msg a -> Msg a
-> Model a -> Model a
-> ( Model a, Cmd (Msg a) ) -> ( Model a, Cmd (Msg a) )
update path navKey msg model = update toRoute navKey msg model =
case msg of case msg of
NoOp -> NoOp ->
( model ( model
@ -193,7 +205,7 @@ update path navKey msg model =
in in
( { model | sort = sort } ( { model | sort = sort }
, createUrl , createUrl
path toRoute
model.channel model.channel
model.query model.query
model.show model.show
@ -218,7 +230,7 @@ update path navKey msg model =
else else
createUrl createUrl
path toRoute
channel channel
model.query model.query
model.show model.show
@ -240,7 +252,7 @@ update path navKey msg model =
else else
( { model | result = RemoteData.Loading } ( { model | result = RemoteData.Loading }
, createUrl , createUrl
path toRoute
model.channel model.channel
model.query model.query
model.show model.show
@ -258,7 +270,7 @@ update path navKey msg model =
ShowDetails selected -> ShowDetails selected ->
( model ( model
, createUrl , createUrl
path toRoute
model.channel model.channel
model.query model.query
(if model.show == Just selected then (if model.show == Just selected then
@ -275,8 +287,10 @@ update path navKey msg model =
) )
{-| TODO: Sort should be part of Route type
-}
createUrl : createUrl :
String SearchRoute
-> String -> String
-> Maybe String -> Maybe String
-> Maybe String -> Maybe String
@ -284,30 +298,9 @@ createUrl :
-> Int -> Int
-> Sort -> Sort
-> String -> String
createUrl path channel query show from size sort = createUrl toRoute channel query show from size sort =
[ Url.Builder.int "from" from toRoute (Just channel) query show (Just from) (Just size) (Just <| toSortId sort)
, Url.Builder.int "size" size |> Route.routeToString
, Url.Builder.string "sort" <| toSortId sort
, Url.Builder.string "channel" channel
]
|> List.append
(query
|> Maybe.map
(\x ->
[ Url.Builder.string "query" x ]
)
|> Maybe.withDefault []
)
|> List.append
(show
|> Maybe.map
(\x ->
[ Url.Builder.string "show" x
]
)
|> Maybe.withDefault []
)
|> Url.Builder.absolute [ path ]
@ -320,6 +313,7 @@ type Channel
| Release_20_03 | Release_20_03
| Release_20_09 | Release_20_09
type alias ChannelDetails = type alias ChannelDetails =
{ id : String { id : String
, title : String , title : String
@ -455,13 +449,15 @@ fromSortId id =
view : view :
String { toRoute : SearchRoute
, categoryName : String
}
-> String -> String
-> Model a -> Model a
-> (String -> Maybe String -> SearchResult a -> Html b) -> (String -> Maybe String -> SearchResult a -> Html b)
-> (Msg a -> b) -> (Msg a -> b)
-> Html b -> Html b
view path title model viewSuccess outMsg = view { toRoute, categoryName } title model viewSuccess outMsg =
div div
[ class "search-page" [ class "search-page"
] ]
@ -520,14 +516,15 @@ view path title model viewSuccess outMsg =
[ type_ "text" [ type_ "text"
, id "search-query-input" , id "search-query-input"
, autofocus True , autofocus True
, placeholder <| "Search for " ++ path , placeholder <| "Search for " ++ categoryName
, onInput (\x -> outMsg (QueryInput x)) , onInput (outMsg << QueryInput)
, value <| Maybe.withDefault "" model.query , value <| Maybe.withDefault "" model.query
] ]
[] []
, div [ class "loader" ] [] , div [ class "loader" ] []
, div [ class "btn-group" ] , div [ class "btn-group" ]
[ button [ class "btn" ] [ text "Search" ] [ button [ class "btn", type_ "submit" ]
[ text "Search" ]
] ]
] ]
] ]
@ -542,8 +539,7 @@ view path title model viewSuccess outMsg =
RemoteData.Success result -> RemoteData.Success result ->
if result.hits.total.value == 0 then if result.hits.total.value == 0 then
div [] div []
[ h4 [] [ text <| "No " ++ path ++ " found!" ] [ h4 [] [ text <| "No " ++ categoryName ++ " found!" ] ]
]
else else
div [] div []
@ -594,9 +590,9 @@ view path title model viewSuccess outMsg =
] ]
] ]
] ]
, viewPager outMsg model result path , viewPager outMsg model result toRoute
, viewSuccess model.channel model.show result , viewSuccess model.channel model.show result
, viewPager outMsg model result path , viewPager outMsg model result toRoute
] ]
RemoteData.Failure error -> RemoteData.Failure error ->
@ -629,9 +625,9 @@ viewPager :
(Msg a -> b) (Msg a -> b)
-> Model a -> Model a
-> SearchResult a -> SearchResult a
-> String -> SearchRoute
-> Html b -> Html b
viewPager _ model result path = viewPager _ model result toRoute =
ul [ class "pager" ] ul [ class "pager" ]
[ li [ li
[ classList [ classList
@ -645,7 +641,7 @@ viewPager _ model result path =
else else
href <| href <|
createUrl createUrl
path toRoute
model.channel model.channel
model.query model.query
model.show model.show
@ -667,7 +663,7 @@ viewPager _ model result path =
else else
createUrl createUrl
path toRoute
model.channel model.channel
model.query model.query
model.show model.show
@ -689,7 +685,7 @@ viewPager _ model result path =
else else
createUrl createUrl
path toRoute
model.channel model.channel
model.query model.query
model.show model.show
@ -719,7 +715,7 @@ viewPager _ model result path =
0 0
in in
createUrl createUrl
path toRoute
model.channel model.channel
model.query model.query
model.show model.show