diff --git a/Makefile b/Makefile index fef804fddd..0a7071a52f 100644 --- a/Makefile +++ b/Makefile @@ -236,3 +236,11 @@ nix/install.sig: nix/install mv $@.tmp $@; \ fi touch $@ + +nixos/options.html: elm + +elm: elm-src/index.elm + nix-build ./elm-src --out-link ./elm + +%.elm: + true diff --git a/elm-src/.gitignore b/elm-src/.gitignore new file mode 100644 index 0000000000..59752f5409 --- /dev/null +++ b/elm-src/.gitignore @@ -0,0 +1,3 @@ +elm-stuff/* +!elm-stuff/exact-dependencies.json +elm.js \ No newline at end of file diff --git a/elm-src/NixJSON.elm b/elm-src/NixJSON.elm new file mode 100644 index 0000000000..6e482fd7f4 --- /dev/null +++ b/elm-src/NixJSON.elm @@ -0,0 +1,158 @@ +module NixJSON exposing (Gson, gnull, nixFromGson, gsonDecoder) + +import Dict exposing (Dict, empty, foldr, filter) +import Json.Decode as Decode + + +type Gson + = GString String + | GBool Bool + | GInt Int + | GFloat Float + | GList (List Gson) + | GDict (Dict String Gson) + | GNull + + +gnull : Gson +gnull = + GNull + + +nixFromGson : Gson -> String +nixFromGson gson = + case gson of + GNull -> + "null" + + GString x -> + nix_string x + + GBool True -> + "true" + + GBool False -> + "false" + + GInt x -> + toString x + + GFloat x -> + toString x + + GList list -> + nix_list list + + GDict dict -> + nix_dict dict + + +nix_string : String -> String +nix_string x = + if String.contains "\n" x then + String.concat + (List.concat + [ [ "''\n" ] + , [ x ] + |> segments_to_lines + |> indent + , [ "''" ] + ] + ) + else + "\"" ++ x ++ "\"" + + +nix_list : List Gson -> String +nix_list list = + let + present : List String -> String + present strings = + case strings of + [] -> + "" + + [ x ] -> + " " ++ x ++ " " + + _ -> + "\n" ++ (indent strings |> String.join "\n") ++ "\n" + in + "[" ++ (List.map nixFromGson list |> present) ++ "]" + + +nix_dict : Dict String Gson -> String +nix_dict dict = + case ( Dict.get "_type" dict, Dict.get "text" dict ) of + ( Just (GString "literalExample"), Just (GString txt) ) -> + txt + + ( Just (GString "literalExample"), _ ) -> + "Error: Literal Example did not contain plain text" + + otherwise -> + let + entries = + Dict.toList dict + in + if List.isEmpty entries then + "{ }" + else + String.concat + (List.concat + [ [ "{\n" ] + , indent + (List.concatMap + nix_enc_dict + entries + ) + , [ "}" ] + ] + ) + + +indent : List String -> List String +indent lines = + List.map (\line -> String.append " " line) lines + + +nix_enc_dict : ( String, Gson ) -> List String +nix_enc_dict ( key, value ) = + [ "\"" + , key + , "\" = " + , (nixFromGson value) + , ";" + ] + |> segments_to_lines + + +segments_to_lines : List String -> List String +segments_to_lines segments = + segments + |> String.concat + |> String.lines + |> List.map (\x -> String.append x "\n") + + +gsonDecoder : Decode.Decoder Gson +gsonDecoder = + Decode.oneOf + [ Decode.null GNull + , Decode.string |> Decode.map GString + , Decode.bool |> Decode.map GBool + , Decode.int |> Decode.map GInt + , Decode.float |> Decode.map GFloat + , Decode.list (Decode.lazy (\_ -> gsonDecoder)) |> Decode.map mkGsonList + , Decode.keyValuePairs (Decode.lazy (\_ -> gsonDecoder)) |> Decode.map mkGsonObject + ] + + +mkGsonList : List Gson -> Gson +mkGsonList data = + GList (List.map (\x -> x) data) + + +mkGsonObject : List ( String, Gson ) -> Gson +mkGsonObject fields = + GDict (Dict.fromList fields) diff --git a/elm-src/NixOptions.elm b/elm-src/NixOptions.elm new file mode 100644 index 0000000000..74fd5aa2e7 --- /dev/null +++ b/elm-src/NixOptions.elm @@ -0,0 +1,60 @@ +module NixOptions exposing (..) + +import NixJSON exposing (Gson, gnull, gsonDecoder) +import Json.Decode as Decode +import Json.Decode.Pipeline exposing (decode, required, hardcoded, optional) + + +type alias Option = + { declarations : List String + , default : Gson + , description : String + , example : Gson + , readOnly : Bool + , datatype : String + } + + +type alias NixOptions = + List ( String, Option ) + + +option_sort_key : ( String, Option ) -> String +option_sort_key ( name, option ) = + name + + +option_filter : List String -> ( String, Option ) -> Bool +option_filter terms ( name, option ) = + List.all + (\term -> + term_matches name term + || term_matches option.description term + ) + terms + + +term_matches : String -> String -> Bool +term_matches name term = + String.contains term (String.toLower name) + + +filtered : NixOptions -> List String -> NixOptions +filtered options terms = + List.filter (option_filter terms) options + + +optionDecoder : Decode.Decoder Option +optionDecoder = + decode Option + |> required "declarations" (Decode.list Decode.string) + |> optional "default" gsonDecoder gnull + |> required "description" Decode.string + |> optional "example" gsonDecoder gnull + |> required "readOnly" Decode.bool + |> required "type" Decode.string + + +decodeOptions : Decode.Decoder NixOptions +decodeOptions = + (Decode.keyValuePairs <| optionDecoder) diff --git a/elm-src/Paginate.elm b/elm-src/Paginate.elm new file mode 100644 index 0000000000..100c1311ca --- /dev/null +++ b/elm-src/Paginate.elm @@ -0,0 +1,27 @@ +module Paginate exposing (..) + +import List.Extra + + +which_page : List a -> (a -> Bool) -> Maybe Int +which_page options matcher = + let + pos = + List.Extra.findIndex matcher options + in + case pos of + Just position -> + Just (ceiling ((toFloat position) / 15.0)) + + Nothing -> + Nothing + + +fetch_page : Int -> List a -> List a +fetch_page page content = + List.drop ((page - 1) * 15) (List.take (page * 15) content) + + +total_pages : List a -> Int +total_pages content = + ceiling ((toFloat (List.length content)) / 15) diff --git a/elm-src/default.nix b/elm-src/default.nix new file mode 100644 index 0000000000..d289ad7f51 --- /dev/null +++ b/elm-src/default.nix @@ -0,0 +1,83 @@ +{ pkgs ? import {} }: +let + inherit (pkgs) stdenv elmPackages lib runCommand fetchzip; + + hashes = { + "crazymykl/ex-em-elm-1.4.0" = "0qxp7kfqap4n8b2pv88v5a4j8vd0azbr1gf6bv54svycmyn2ghs0"; + "NoRedInk/elm-decode-pipeline-3.0.0" = "11dpynbv90b3yh33dvpx2c09sb6hxj6ffjq6f8a4wyf7lv2vb8nm"; + "bcardiff/elm-debounce-1.1.3" = "0drklpkxvsddg3fkj1hjlmx4215yy58g0ig45i3h8hp6aix8ycfh"; + "Bogdanp/elm-querystring-1.0.0" = "0895hmaz7cmqi7k1zb61y0hmarbhzg8k5p792sm0az8myqkicjs1"; + "Bogdanp/elm-combine-3.1.1" = "0cpd50qab54hlnc1c3pf2j7j45cr3y4nhkf53p8d1w7rm7kyqssb"; + "elm-community/list-extra-6.1.0" = "082rxwicx4ndah48bv49w53fkp5dmcdgraz9z2z2lkr5fkwgcrvx"; + "elm-lang/html-2.0.0" = "08mxkcb1548fcvc1p7r3g1ycrdwmfkyma24jf72238lf7j4ps4ng"; + "elm-lang/dom-1.1.1" = "181yams19hf391dgvjxvamfxdkh49i83qfdwj9big243ljd08mpv"; + "elm-lang/core-5.1.1" = "0iww5kfxwymwj1748q0i629vyr1yjdqsx1fvymra6866gr2m3n19"; + "elm-lang/navigation-2.1.0" = "08rb6b740yxz5jpgkf5qad1raf7qqnx77hzn3bkk0s4kqyirhfcl"; + "elm-lang/http-1.0.0" = "1bhhh81ih7psvs324i5cdac5w79nc9jmykm76cmy6awfp7zhqb4b"; + "elm-lang/lazy-2.0.0" = "09439a2pkzbwvk2lfrg229jwjvc4d5dc32562q276z012bw1a2lc"; + "elm-lang/virtual-dom-2.0.4" = "1zydzzhpxivpzddnxjsjlwc18kaya1l4f8zsgs22wkq25izkin33"; + }; + + fetchhash = name: version: + let + key = "${name}-${version}"; + in + if builtins.hasAttr key hashes + then hashes."${key}" + else "0000000000000000000000000000000000000000000000000000"; + + + deps = lib.mapAttrs' + (pname: version: { + name = "${pname}/${version}"; + value = fetchzip { + name = (builtins.replaceStrings ["/"] ["-"] "${pname}-${version}"); + url = "https://github.com/${pname}/archive/${version}.zip"; + sha256 = fetchhash pname version; + }; + }) + (builtins.fromJSON (builtins.readFile ./elm-stuff/exact-dependencies.json)); + + depCopyInstructions = builtins.concatStringsSep "\n" + (lib.attrValues (lib.mapAttrs + (dir: src: + '' + mkdir -p "$out/packages/${dir}" + cp -r ${src}/* "$out/packages/${dir}/" # */ + '' + ) + deps)); + elm_env = runCommand "elm_env" {} + '' + mkdir $out + cp ${./elm-stuff/exact-dependencies.json} $out/exact-dependencies.json + ${depCopyInstructions} + ''; + + +in +stdenv.mkDerivation { + name = "nixos-org"; + src = lib.cleanSource ./.; + + buildInputs = (with elmPackages; [ + elm + ]); + + buildPhase = '' + mkdir "$out" + rm -rf elm-stuff + cp -r ${elm_env} elm-stuff + chmod a+w ./elm-stuff + elm-make ./index.elm --output "$out/options.js" + ''; + + shellHook = '' + elm-package install + elm-reactor + ''; + + installPhase = '' + echo ":)" + ''; +} diff --git a/elm-src/elm-package.json b/elm-src/elm-package.json new file mode 100644 index 0000000000..057604b5b1 --- /dev/null +++ b/elm-src/elm-package.json @@ -0,0 +1,22 @@ +{ + "version": "1.0.0", + "summary": "nixos.org", + "repository": "https://github.com/nixos/nixos-homepage.git", + "license": "apache-2.0", + "source-directories": [ + "." + ], + "exposed-modules": [], + "dependencies": { + "Bogdanp/elm-querystring": "1.0.0 <= v < 2.0.0", + "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0", + "bcardiff/elm-debounce": "1.1.3 <= v < 2.0.0", + "crazymykl/ex-em-elm": "1.4.0 <= v < 2.0.0", + "elm-community/list-extra": "6.1.0 <= v < 7.0.0", + "elm-lang/core": "5.0.0 <= v < 6.0.0", + "elm-lang/html": "2.0.0 <= v < 3.0.0", + "elm-lang/http": "1.0.0 <= v < 2.0.0", + "elm-lang/navigation": "2.1.0 <= v < 3.0.0" + }, + "elm-version": "0.18.0 <= v < 0.19.0" +} diff --git a/elm-src/elm-stuff/exact-dependencies.json b/elm-src/elm-stuff/exact-dependencies.json new file mode 100644 index 0000000000..0ab5728d6f --- /dev/null +++ b/elm-src/elm-stuff/exact-dependencies.json @@ -0,0 +1,15 @@ +{ + "elm-lang/navigation": "2.1.0", + "Bogdanp/elm-querystring": "1.0.0", + "elm-lang/virtual-dom": "2.0.4", + "elm-lang/lazy": "2.0.0", + "elm-lang/dom": "1.1.1", + "elm-lang/html": "2.0.0", + "elm-lang/http": "1.0.0", + "bcardiff/elm-debounce": "1.1.3", + "elm-community/list-extra": "6.1.0", + "NoRedInk/elm-decode-pipeline": "3.0.0", + "Bogdanp/elm-combine": "3.1.1", + "crazymykl/ex-em-elm": "1.4.0", + "elm-lang/core": "5.1.1" +} \ No newline at end of file diff --git a/elm-src/index.elm b/elm-src/index.elm new file mode 100644 index 0000000000..aecacc3436 --- /dev/null +++ b/elm-src/index.elm @@ -0,0 +1,477 @@ +module Main exposing (..) + +import NixOptions exposing (..) +import NixJSON exposing (Gson, gnull, gsonDecoder, nixFromGson) +import Paginate exposing (..) +import Html exposing (node, Attribute, Html, a, text, em, table, tr, th, td, tbody, thead, input, ul, li, button, h1, h2, div, pre, p, hr) +import Html.Attributes exposing (placeholder, src, disabled, href, class, autofocus, value, id) +import Html.Events exposing (onInput, onClick) +import Http +import Json.Encode exposing (encode, Value, null) +import Dict exposing (Dict, empty, foldr, filter) +import Navigation +import QueryString +import Debug +import Maybe +import Array +import ExEmElm.Parser +import ExEmElm.Traverse +import Debounce + + +main = + Navigation.program ChangeUrl + { init = init + , view = view + , update = update + , subscriptions = subscriptions + } + + + +-- MODEL + + +type alias Model = + { query : String + , terms : List String + , options : NixOptions + , matchingOptions : NixOptions + , page : Int + , location : Navigation.Location + , selected : Maybe String + , status : PageStatus + , debounce_state : Debounce.State + } + + +type alias UrlState = + { query : String + , page : Int + , selected : Maybe String + } + + +clamped : Model -> Model +clamped model = + { model | page = (clamp 1 (total_pages model.matchingOptions) model.page) } + + +update_options : Model -> NixOptions -> Model +update_options model unsorted_options = + let + options : NixOptions + options = + List.sortBy option_sort_key unsorted_options + + matchingOptions = + filtered options model.terms + + page = + case model.selected of + Just option -> + case which_page matchingOptions (opt_tuple_matches_name option) of + Just p -> + p + + Nothing -> + model.page + + Nothing -> + model.page + in + clamped + ({ model + | options = options + , matchingOptions = matchingOptions + , page = page + , status = Loaded + } + ) + + +update_query : Model -> String -> Model +update_query model query = + { model + | query = query + , terms = splitQuery query + , matchingOptions = filtered model.options model.terms + , page = 1 + , selected = Nothing + } + + +update_page : Model -> Int -> Model +update_page model newPage = + clamped + { model + | page = newPage + , selected = Nothing + } + + +select_option : Model -> String -> Model +select_option model opt = + { model + | selected = Just opt + } + + +deselect_option : Model -> Model +deselect_option model = + { model + | selected = Nothing + } + + +init : Navigation.Location -> ( Model, Cmd Msg ) +init location = + let + state = + url_state_from_location location + in + ( Model state.query (splitQuery state.query) [] [] state.page location state.selected Loading Debounce.init + , getOptions + ) + + +splitQuery : String -> List String +splitQuery query = + String.words (String.toLower query) + + + +-- UPDATE + + +url_state_from_location : Navigation.Location -> UrlState +url_state_from_location location = + let + qs = + QueryString.parse location.search + in + UrlState + (Maybe.withDefault "" (QueryString.one QueryString.string "query" qs)) + (Maybe.withDefault 1 (QueryString.one QueryString.int "page" qs)) + (QueryString.one QueryString.string "selected" qs) + + +updateFromUrl : Navigation.Location -> Model -> Model +updateFromUrl location model = + let + state = + url_state_from_location location + in + case state.selected of + Just opt -> + select_option model opt + + Nothing -> + (update_page (update_query model state.query) state.page) + + +updateUrl : Model -> Cmd Msg +updateUrl model = + let + querystring = + case model.selected of + Nothing -> + String.append + (String.append "?query=" (Http.encodeUri model.query)) + (if model.page > 1 then + (String.append "&page=" (Http.encodeUri (toString model.page))) + else + "" + ) + + Just opt -> + (String.append "?selected=" (Http.encodeUri opt)) + in + Navigation.newUrl querystring + + +type Msg + = ChangeQuery String + | ChangePage Int + | ChangeUrl Navigation.Location + | FetchedOptions (Result Http.Error NixOptions) + | SelectOption String + | DeselectOption + | Bounce (Debounce.Msg Msg) + | SyncQueryToUrl + + +type PageStatus + = Loading + | Loaded + | Error String + + +update : Msg -> Model -> ( Model, Cmd Msg ) +update msg model = + case msg of + FetchedOptions (Ok options) -> + let + newModel = + update_options model options + in + ( newModel, updateUrl newModel ) + + FetchedOptions (Err e) -> + ( { model | status = Error (toString e) }, Cmd.none ) + + SelectOption option_name -> + let + newModel = + select_option model option_name + in + ( newModel, updateUrl newModel ) + + DeselectOption -> + let + newModel = + deselect_option model + in + ( newModel, updateUrl newModel ) + + ChangeUrl location -> + let + newModel = + updateFromUrl location model + in + ( newModel, Cmd.none ) + + ChangePage newPage -> + let + newModel = + update_page model newPage + in + ( newModel, updateUrl newModel ) + + Bounce bouncer -> + (Debounce.update debounce_cfg bouncer model) + + ChangeQuery newQuery -> + let + newModel = + update_query model newQuery + in + ( newModel, debounce <| SyncQueryToUrl ) + + SyncQueryToUrl -> + ( model, updateUrl model ) + + + +-- VIEW + + +tt : List (Attribute msg) -> List (Html msg) -> Html msg +tt x y = + node "tt" x y + + +declaration_link : String -> Html Msg +declaration_link path = + a [ href (String.append "https://github.com/NixOS/nixpkgs/tree/release-17.03/" path) ] + [ (tt [] [ text path ]) ] + + +null_is_not_given : (Gson -> Html Msg) -> Gson -> Html Msg +null_is_not_given fn x = + if x == gnull then + (em [] [ text "Not given" ]) + else + fn x + + +nix : Gson -> Html Msg +nix content = + pre [] [ text (nixFromGson content) ] + + +description_to_text : String -> String +description_to_text input = + let + text = + "" + ++ input + ++ "" + in + case ExEmElm.Parser.parse text of + Ok doc -> + ExEmElm.Traverse.innerText (ExEmElm.Parser.root doc) + + otherwise -> + ":)" + + +describe_option : Option -> Html Msg +describe_option option = + div [ class "search-details" ] + [ table [] + [ tbody [] + [ tr [] + [ th [] [ text "Description:" ] + , td [] [] + , td [ class "description docbook" ] [ text (description_to_text option.description) ] + ] + , tr [] + [ th [] [ text "Default Value:" ] + , td [] [] + , td [ class "default" ] [ null_is_not_given nix option.default ] + ] + , tr [] + [ th [] [ text "Example value:" ] + , td [] [] + , td [ class "example" ] [ null_is_not_given nix option.example ] + ] + , tr [] + [ th [] [ text "Declared In:" ] + , td [] [] + , td [ class "declared-in" ] (List.map declaration_link option.declarations) + ] + ] + ] + ] + + +optionToTd : Maybe String -> ( String, Option ) -> List (Html Msg) +optionToTd selected ( name, option ) = + let + isSelected = + (case selected of + Nothing -> + False + + Just selopt -> + selopt == name + ) + in + (List.concat + [ [ (tr [] + [ td + [ onClick + (if isSelected then + (DeselectOption) + else + (SelectOption name) + ) + ] + [ tt [] [ text name ] ] + ] + ) + ] + , (if not isSelected then + [] + else + [ tr [] + [ td [ class "details" ] [ describe_option option ] + ] + ] + ) + ] + ) + + +opt_tuple_matches_name : String -> ( String, Option ) -> Bool +opt_tuple_matches_name term ( name, option ) = + term == name + + +changePageIf : Bool -> Int -> String -> Html Msg +changePageIf cond page txt = + button + (if cond then + [ onClick (ChangePage page) ] + else + [ disabled True ] + ) + [ text txt ] + + +view : Model -> Html Msg +view model = + if List.isEmpty model.options then + div [] [ h1 [] [ text (toString model.status) ] ] + else + viewOptions model + + +debounce = + Debounce.debounceCmd debounce_cfg + + +debounce_cfg : Debounce.Config Model Msg +debounce_cfg = + Debounce.config + -- getState : Model -> Debounce.State + .debounce_state + -- setState : Debounce.State -> Model -> Debounce.State + (\model s -> { model | debounce_state = s }) + -- msgWrapper : Msg a -> Msg + Bounce + 200 + + + +-- timeout ms : Float + + +viewOptions : Model -> Html Msg +viewOptions model = + div [] + [ p [] + [ input [ id "search", class "search-query span3", autofocus True, value model.query, onInput ChangeQuery ] [] + ] + , hr [] [] + , p [ id "how-many" ] + [ em [] + [ text + (if List.isEmpty model.matchingOptions then + "Showing no results" + else + "Showing results " + ++ (toString (((model.page - 1) * 15) + 1)) + ++ "-" + ++ (toString (min (List.length model.matchingOptions) ((model.page) * 15))) + ++ " of " + ++ (toString (List.length model.matchingOptions)) + ++ "." + ) + ] + ] + , table [ class "options table table-hover" ] + [ thead [] + [ tr [] + [ th [] [ text "Option name" ] + ] + ] + , tbody [] + (List.concat (List.map (optionToTd model.selected) (fetch_page model.page model.matchingOptions))) + ] + , ul [ class "pager" ] + [ li [] [ (changePageIf (not (model.page <= 1)) 1 "« First") ] + , li [] [ (changePageIf (model.page > 1) (model.page - 1) "‹ Previous") ] + , li [] [ (changePageIf (model.page < (total_pages model.matchingOptions)) (model.page + 1) "Next ›") ] + , li [] [ (changePageIf (model.page < (total_pages model.matchingOptions)) (total_pages model.matchingOptions) "Last »") ] + ] + ] + + + +-- SUBSCRIPTIONS + + +subscriptions : Model -> Sub Msg +subscriptions model = + Sub.none + + + +-- HTTP + + +getOptions : Cmd Msg +getOptions = + Http.send FetchedOptions (Http.get "./options.json.gz" decodeOptions) diff --git a/nixos/options.tt b/nixos/options.tt index c124334579..a1d097f184 100644 --- a/nixos/options.tt +++ b/nixos/options.tt @@ -1,340 +1,10 @@ [% WRAPPER layout.tt title='Search NixOS options' menu='nixos' %] -

- -

+
-
- -

Loading…

- -
- - - - - - - - - -
Option name
- -[% INCLUDE renderPager %] - -
- -
- - - - - - - - - - - - - - - - - - - - - -
Description: - Not given
Default value: - Not given
Example value: - Not given
Declared in: - Unknown
-
- - + + [% END %]