1
1
mirror of https://github.com/aelve/guide.git synced 2024-12-19 19:01:51 +03:00
guide/src/Main.hs
2016-02-21 14:37:17 +03:00

760 lines
24 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{-# LANGUAGE
OverloadedStrings,
TemplateHaskell,
RecordWildCards,
RankNTypes,
FlexibleInstances,
FlexibleContexts,
QuasiQuotes,
ScopedTypeVariables,
MultiParamTypeClasses,
FunctionalDependencies,
DataKinds,
NoImplicitPrelude
#-}
module Main (main) where
-- General
import BasePrelude hiding (Category)
-- Monads and monad transformers
import Control.Monad.State
-- Lenses
import Lens.Micro.Platform
-- Text
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import Data.Text.Format hiding (format)
import qualified Data.Text.Format as Format
import qualified Data.Text.Format.Params as Format
import NeatInterpolation
-- Randomness
import System.Random
-- Web
import Lucid hiding (for_)
import Web.Spock hiding (get, text)
import qualified Web.Spock as Spock
import Network.Wai.Middleware.Static
import Web.PathPieces
import Text.HTML.SanitizeXSS (sanitaryURI)
type Url = Text
-- | Unique id, used for many things categories, items, and anchor ids.
-- Note that in HTML 5 using numeric ids for divs, spans, etc is okay.
type Uid = Int
randomUid :: MonadIO m => m Uid
randomUid = liftIO $ randomRIO (0, 10^(9::Int))
data Trait = Trait {
_traitUid :: Uid,
_traitContent :: Text }
makeFields ''Trait
data ItemKind = HackageLibrary | Library | Unknown
data Item = Item {
_itemUid :: Uid,
_itemName :: Text,
_itemPros :: [Trait],
_itemCons :: [Trait],
_itemLink :: Maybe Url,
_itemKind :: ItemKind }
makeFields ''Item
traitById :: Uid -> Lens' Item Trait
traitById uid' = singular $
(pros.each . filtered ((== uid') . view uid)) `failing`
(cons.each . filtered ((== uid') . view uid))
data Category = Category {
_categoryUid :: Uid,
_categoryTitle :: Text,
_categoryDescription :: Text,
_categoryItems :: [Item] }
makeFields ''Category
data GlobalState = GlobalState {
_categories :: [Category] }
makeLenses ''GlobalState
categoryById :: Uid -> Lens' GlobalState Category
categoryById uid' = singular $
categories.each . filtered ((== uid') . view uid)
itemById :: Uid -> Lens' GlobalState Item
itemById uid' = singular $
categories.each . items.each . filtered ((== uid') . view uid)
emptyState :: GlobalState
emptyState = GlobalState {
_categories = [] }
sampleState :: GlobalState
sampleState = do
let lensItem = Item {
_itemUid = 12,
_itemName = "lens",
_itemPros = [Trait 121 "the standard lenses library",
Trait 122 "batteries included"],
_itemCons = [Trait 123 "huge"],
_itemLink = Nothing,
_itemKind = HackageLibrary }
let microlensItem = Item {
_itemUid = 13,
_itemName = "microlens",
_itemPros = [Trait 131 "very small",
Trait 132 "good for libraries"],
_itemCons = [Trait 133 "doesn't have advanced features"],
_itemLink = Just "https://github.com/aelve/microlens",
_itemKind = HackageLibrary }
let lensesCategory = Category {
_categoryUid = 1,
_categoryTitle = "lenses",
_categoryDescription = "Lenses are first-class composable accessors.",
_categoryItems = [lensItem, microlensItem] }
GlobalState {_categories = [lensesCategory]}
itemVar :: Path '[Uid]
itemVar = "item" <//> var
categoryVar :: Path '[Uid]
categoryVar = "category" <//> var
traitVar :: Path '[Uid]
traitVar = "trait" <//> var
main :: IO ()
main = runSpock 8080 $ spockT id $ do
middleware (staticPolicy (addBase "static"))
stateVar <- liftIO $ newIORef sampleState
let withGlobal :: MonadIO m => State GlobalState a -> m a
withGlobal f = liftIO $ atomicModifyIORef' stateVar (swap . runState f)
-- Main page
Spock.get root $ do
s <- liftIO $ readIORef stateVar
lucid $ renderRoot s
-- Render methods
Spock.subcomponent "render" $ do
-- Title of a category
Spock.get (categoryVar <//> "title") $ \catId -> do
category <- withGlobal $ use (categoryById catId)
renderMode <- param' "mode"
lucid $ renderCategoryTitle renderMode category
-- Description of a category
Spock.get (categoryVar <//> "description") $ \catId -> do
category <- withGlobal $ use (categoryById catId)
renderMode <- param' "mode"
lucid $ renderCategoryDescription renderMode category
-- Item
Spock.get itemVar $ \itemId -> do
item <- withGlobal $ use (itemById itemId)
lucid $ renderItem item
-- Item info
Spock.get (itemVar <//> "info") $ \itemId -> do
item <- withGlobal $ use (itemById itemId)
renderMode <- param' "mode"
lucid $ renderItemInfo renderMode item
-- All item traits
Spock.get (itemVar <//> "traits") $ \itemId -> do
item <- withGlobal $ use (itemById itemId)
renderMode <- param' "mode"
lucid $ renderItemTraits renderMode item
-- A single trait
Spock.get (itemVar <//> traitVar) $ \itemId traitId -> do
trait <- withGlobal $ use (itemById itemId . traitById traitId)
renderMode <- param' "mode"
lucid $ renderTrait renderMode itemId trait
-- The add/set methods return rendered parts of the structure (added
-- categories, changed items, etc) so that the Javascript part could take
-- them and inject into the page. We don't want to duplicate rendering on
-- server side and on client side.
-- Set methods
Spock.subcomponent "set" $ do
-- Title of a category
Spock.post (categoryVar <//> "title") $ \catId -> do
content' <- param' "content"
changedCategory <- withGlobal $ do
categoryById catId . title .= content'
use (categoryById catId)
lucid $ renderCategoryTitle Editable changedCategory
-- Description of a category
Spock.post (categoryVar <//> "description") $ \catId -> do
content' <- param' "content"
changedCategory <- withGlobal $ do
categoryById catId . description .= content'
use (categoryById catId)
lucid $ renderCategoryDescription Editable changedCategory
-- Item info
Spock.post (itemVar <//> "info") $ \itemId -> do
name' <- T.strip <$> param' "name"
link' <- T.strip <$> param' "link"
changedItem <- withGlobal $ do
-- TODO: actually validate the form and report errors
unless (T.null name') $
itemById itemId . name .= name'
case (T.null link', sanitiseUrl link') of
(True, _) -> itemById itemId . link .= Nothing
(_, Just l) -> itemById itemId . link .= Just l
_otherwise -> return ()
use (itemById itemId)
lucid $ renderItemInfo Normal changedItem
-- Trait
Spock.post (itemVar <//> traitVar) $ \itemId traitId -> do
content' <- param' "content"
changedTrait <- withGlobal $ do
itemById itemId . traitById traitId . content .= content'
use (itemById itemId . traitById traitId)
lucid $ renderTrait Editable itemId changedTrait
-- Add methods
Spock.subcomponent "add" $ do
-- New category
Spock.post "category" $ do
content' <- param' "content"
uid' <- randomUid
let newCategory = Category {
_categoryUid = uid',
_categoryTitle = content',
_categoryDescription = "<write a description here>",
_categoryItems = [] }
withGlobal $ categories %= (++ [newCategory])
lucid $ renderCategory newCategory
-- New library in a category
Spock.post (categoryVar <//> "library") $ \catId -> do
name' <- param' "name"
uid' <- randomUid
let newItem = Item {
_itemUid = uid',
_itemName = name',
_itemPros = [],
_itemCons = [],
_itemLink = Nothing,
_itemKind = HackageLibrary }
-- TODO: maybe do something if the category doesn't exist (e.g. has been
-- already deleted)
withGlobal $ categoryById catId . items %= (++ [newItem])
lucid $ renderItem newItem
-- Pro (argument in favor of a library)
Spock.post (itemVar <//> "pro") $ \itemId -> do
content' <- param' "content"
uid' <- randomUid
let newTrait = Trait uid' content'
withGlobal $ itemById itemId . pros %= (++ [newTrait])
lucid $ renderTrait Editable itemId newTrait
-- Con (argument against a library)
Spock.post (itemVar <//> "con") $ \itemId -> do
content' <- param' "content"
uid' <- randomUid
let newTrait = Trait uid' content'
withGlobal $ itemById itemId . cons %= (++ [newTrait])
lucid $ renderTrait Editable itemId newTrait
-- Moving things
Spock.subcomponent "move" $ do
-- Move trait
Spock.post (itemVar <//> traitVar) $ \itemId traitId -> do
direction :: Text <- param' "direction"
let move = if direction == "up" then moveUp else moveDown
withGlobal $ do
itemById itemId . pros %= move ((== traitId) . view uid)
itemById itemId . cons %= move ((== traitId) . view uid)
renderRoot :: GlobalState -> HtmlT IO ()
renderRoot globalState = do
includeJS "https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"
includeCSS "/css.css"
-- Include definitions of all Javascript functions that we have defined in
-- this file.
script_ $ T.unlines (map snd (allJSFunctions :: [(Text, JS)]))
categoriesNode <- div_ [id_ "categories"] $ do
mapM_ renderCategory (globalState ^. categories)
thisNode
let handler s = js_addCategory (categoriesNode, s)
input_ [type_ "text", placeholder_ "new category", onInputSubmit handler]
renderCategoryTitle :: Editable -> Category -> HtmlT IO ()
renderCategoryTitle editable category =
h2_ [id_ (tshow (category^.uid))] $ do
a_ [class_ "anchor", href_ ("#" <> tshow (category^.uid))] "#"
titleNode <- thisNode
case editable of
Editable -> do
toHtml (category^.title)
textButton "edit" $
js_setCategoryTitleMode (titleNode, category^.uid, InEdit)
InEdit -> do
let handler s = js_submitCategoryTitle (titleNode, category^.uid, s)
input_ [type_ "text", value_ (category^.title), onInputSubmit handler]
textButton "cancel" $
js_setCategoryTitleMode (titleNode, category^.uid, Editable)
-- TODO: render descriptions and traits as Markdown
renderCategoryDescription :: Editable -> Category -> HtmlT IO ()
renderCategoryDescription editable category =
p_ $ do
descrNode <- thisNode
case editable of
Editable -> do
toHtml (category^.description)
textButton "edit" $
js_setCategoryDescriptionMode (descrNode, category^.uid, InEdit)
InEdit -> do
let handler s = js_submitCategoryDescription
(descrNode, category^.uid, s)
input_ [type_ "text", value_ (category^.description), onInputSubmit handler]
textButton "cancel" $
js_setCategoryDescriptionMode (descrNode, category^.uid, Editable)
renderCategory :: Category -> HtmlT IO ()
renderCategory category =
div_ $ do
renderCategoryTitle Editable category
renderCategoryDescription Editable category
itemsNode <- div_ [class_ "items"] $ do
mapM_ renderItem (category^.items)
thisNode
let handler s = js_addLibrary (itemsNode, category^.uid, s)
input_ [type_ "text", placeholder_ "new item", onInputSubmit handler]
-- TODO: add arrows for moving items left-and-right in the category
renderItem :: Item -> HtmlT IO ()
renderItem item =
div_ [class_ "item"] $ do
renderItemInfo Normal item
renderItemTraits Normal item
-- TODO: warn when a library isn't on Hackage but is supposed to be
-- TODO: give a link to oldest available docs when the new docs aren't there
renderItemInfo :: Editable -> Item -> HtmlT IO ()
renderItemInfo editable item =
div_ $ do
this <- thisNode
case editable of
Normal -> h3_ $ do
-- If the library is on Hackage, the title links to its Hackage page;
-- otherwise, it doesn't link anywhere. Even if the link field is
-- present, it's going to be rendered as “(site)”, not linked in the
-- title.
let hackageLink = "https://hackage.haskell.org/package/" <> item^.name
case item^.kind of
HackageLibrary -> a_ [href_ hackageLink] (toHtml (item^.name))
_otherwise -> toHtml (item^.name)
case item^.link of
Just l -> " (" >> a_ [href_ l] "site" >> ")"
Nothing -> return ()
textButton "edit" $
js_setItemInfoMode (this, item^.uid, Editable)
-- TODO: this should actually be InEdit
Editable -> do
let handler s = js_submitItemInfo (this, item^.uid, s)
form_ [onFormSubmit handler] $ do
label_ $ do
"Package name: "
input_ [type_ "text", name_ "name",
value_ (item^.name)]
br_ []
label_ $ do
"Site (optional): "
input_ [type_ "text", name_ "link",
value_ (fromMaybe "" (item^.link))]
br_ []
input_ [type_ "submit", value_ "Submit"]
let cancelHandler = js_setItemInfoMode (this, item^.uid, Normal)
input_ [type_ "button", value_ "Cancel", onclick_ cancelHandler]
-- TODO: categories that don't directly compare libraries but just list all
-- libraries about something (e.g. Yesod plugins, or whatever)
renderItemTraits :: Editable -> Item -> HtmlT IO ()
renderItemTraits editable item =
div_ [class_ "traits"] $ do
this <- thisNode
case editable of
Normal -> textButton "edit" $
js_setItemTraitsMode (this, item^.uid, Editable)
Editable -> textButton "edit off" $
js_setItemTraitsMode (this, item^.uid, Normal)
div_ [class_ "traits-group"] $ do
p_ "Pros:"
case editable of
Normal ->
ul_ $ mapM_ (renderTrait Normal (item^.uid)) (item^.pros)
Editable -> do
listNode <- ul_ $ do
mapM_ (renderTrait Editable (item^.uid)) (item^.pros)
thisNode
let handler s = js_addPro (listNode, item^.uid, s)
input_ [type_ "text", placeholder_ "add pro", onInputSubmit handler]
div_ [class_ "traits-group"] $ do
p_ "Cons:"
case editable of
Normal ->
ul_ $ mapM_ (renderTrait Normal (item^.uid)) (item^.cons)
Editable -> do
listNode <- ul_ $ do
mapM_ (renderTrait Editable (item^.uid)) (item^.cons)
thisNode
let handler s = js_addCon (listNode, item^.uid, s)
input_ [type_ "text", placeholder_ "add con", onInputSubmit handler]
renderTrait :: Editable -> Uid -> Trait -> HtmlT IO ()
renderTrait Normal _itemId trait = li_ (toHtml (trait^.content))
renderTrait Editable itemId trait = li_ $ do
this <- thisNode
toHtml (trait^.content)
imgButton "/arrow-thick-top.svg" [width_ "12px"] $
js_moveTraitUp (itemId, trait^.uid, this)
imgButton "/arrow-thick-bottom.svg" [width_ "12px"] $
js_moveTraitDown (itemId, trait^.uid, this)
textButton "edit" $
js_setTraitMode (this, itemId, trait^.uid, InEdit)
renderTrait InEdit itemId trait = li_ $ do
this <- thisNode
let handler s = js_submitTrait (this, itemId, trait^.uid, s)
input_ [type_ "text", value_ (trait^.content), onInputSubmit handler]
textButton "cancel" $
js_setTraitMode (this, itemId, trait^.uid, Editable)
-- Utils
includeJS :: Monad m => Url -> HtmlT m ()
includeJS url = with (script_ "") [src_ url]
includeCSS :: Monad m => Url -> HtmlT m ()
includeCSS url = link_ [rel_ "stylesheet", type_ "text/css", href_ url]
-- The function is passed a JS expression that refers to text being submitted.
onInputSubmit :: (JS -> JS) -> Attribute
onInputSubmit f = onkeyup_ $ format
"if (event.keyCode == 13) {\
\ {}\
\ this.value = ''; }"
[f "this.value"]
onFormSubmit :: (JS -> JS) -> Attribute
onFormSubmit f = onsubmit_ $ format "{} return false;" [f "this"]
-- Javascript
-- TODO: try to make them more type-safe somehow?
class JSFunction a where
makeJSFunction
:: Text -- ^ Name
-> [Text] -- ^ Parameter names
-> JS -- ^ Definition
-> a
-- This generates function name
instance JSFunction Text where
makeJSFunction fName _fParams _fDef = fName
-- This generates function name and definition
instance JSFunction (Text, JS) where
makeJSFunction fName fParams fDef = (fName, fullDef)
where
fullDef = format "function {}({}) {\n{}}\n"
(fName, T.intercalate "," fParams, fDef)
-- This generates a function that takes arguments and produces a Javascript
-- function call
instance JSParams a => JSFunction (a -> JS) where
makeJSFunction fName _fParams _fDef = \args ->
format "{}({});" (fName, T.intercalate "," (jsParams args))
allJSFunctions :: JSFunction a => [a]
allJSFunctions = [
-- Utilities
js_replaceWithData, js_appendData,
js_moveNodeUp, js_moveNodeDown,
-- Add methods
js_addLibrary, js_addCategory,
js_addPro, js_addCon,
-- Render-as-editable methods
js_setCategoryTitleMode, js_setCategoryDescriptionMode,
js_setItemInfoMode, js_setItemTraitsMode,
js_setTraitMode,
-- Set methods
js_submitCategoryTitle, js_submitCategoryDescription,
js_submitTrait,
js_submitItemInfo,
-- Other things
js_moveTraitUp, js_moveTraitDown ]
js_replaceWithData :: JSFunction a => a
js_replaceWithData =
makeJSFunction "replaceWithData" ["node"]
[text|
return function(data) {$(node).replaceWith(data);};
|]
js_appendData :: JSFunction a => a
js_appendData =
makeJSFunction "appendData" ["node"]
[text|
return function(data) {$(node).append(data);};
|]
-- | Move node up (in a list of sibling nodes), ignoring anchor elements
-- inserted by 'thisNode'.
js_moveNodeUp :: JSFunction a => a
js_moveNodeUp =
makeJSFunction "moveNodeUp" ["node"]
[text|
var el = $(node);
while (el.prev().is(".dummy"))
el.prev().before(el);
if (el.not(':first-child'))
el.prev().before(el);
|]
-- | Move node down (in a list of sibling nodes), ignoring anchor elements
-- inserted by 'thisNode'.
js_moveNodeDown :: JSFunction a => a
js_moveNodeDown =
makeJSFunction "moveNodeDown" ["node"]
[text|
var el = $(node);
while (el.next().is(".dummy"))
el.next().after(el);
if (el.not(':last-child'))
el.next().after(el);
|]
-- | Create a new category.
js_addCategory :: JSFunction a => a
js_addCategory =
makeJSFunction "addCategory" ["node", "s"]
[text|
$.post("/add/category", {content: s})
.done(appendData(node));
|]
-- | Add a new library to some category.
js_addLibrary :: JSFunction a => a
js_addLibrary =
makeJSFunction "addLibrary" ["node", "catId", "s"]
[text|
$.post("/add/category/"+catId+"/library", {name: s})
.done(appendData(node));
|]
js_setCategoryTitleMode :: JSFunction a => a
js_setCategoryTitleMode =
makeJSFunction "setCategoryTitleMode" ["node", "catId", "mode"]
[text|
$.get("/render/category/"+catId+"/title", {mode: mode})
.done(replaceWithData(node));
|]
{- |
Finish category title editing (this happens when you submit the field).
This turns the title with the editbox back into a simple text title.
-}
js_submitCategoryTitle :: JSFunction a => a
js_submitCategoryTitle =
makeJSFunction "submitCategoryTitle" ["node", "catId", "s"]
[text|
$.post("/set/category/"+catId+"/title", {content: s})
.done(replaceWithData(node));
|]
js_setCategoryDescriptionMode :: JSFunction a => a
js_setCategoryDescriptionMode =
makeJSFunction "setCategoryDescriptionMode" ["node", "catId", "mode"]
[text|
$.get("/render/category/"+catId+"/description", {mode: mode})
.done(replaceWithData(node));
|]
js_submitCategoryDescription :: JSFunction a => a
js_submitCategoryDescription =
makeJSFunction "submitCategoryDescription" ["node", "catId", "s"]
[text|
$.post("/set/category/"+catId+"/description", {content: s})
.done(replaceWithData(node));
|]
-- | Add a pro to some item.
js_addPro :: JSFunction a => a
js_addPro =
makeJSFunction "addPro" ["node", "itemId", "s"]
[text|
$.post("/add/item/"+itemId+"/pro", {content: s})
.done(appendData(node));
|]
-- | Add a con to some item.
js_addCon :: JSFunction a => a
js_addCon =
makeJSFunction "addCon" ["node", "itemId", "s"]
[text|
$.post("/add/item/"+itemId+"/con", {content: s})
.done(appendData(node));
|]
js_setItemInfoMode :: JSFunction a => a
js_setItemInfoMode =
makeJSFunction "setItemInfoMode" ["node", "itemId", "mode"]
[text|
$.get("/render/item/"+itemId+"/info", {mode: mode})
.done(replaceWithData(node));
|]
js_setItemTraitsMode :: JSFunction a => a
js_setItemTraitsMode =
makeJSFunction "setItemTraitsMode" ["node", "itemId", "mode"]
[text|
$.get("/render/item/"+itemId+"/traits", {mode: mode})
.done(replaceWithData(node));
|]
js_setTraitMode :: JSFunction a => a
js_setTraitMode =
makeJSFunction "setTraitMode" ["node", "itemId", "traitId", "mode"]
[text|
$.get("/render/item/"+itemId+"/trait/"+traitId, {mode: mode})
.done(replaceWithData(node));
|]
js_submitTrait :: JSFunction a => a
js_submitTrait =
makeJSFunction "submitTrait" ["node", "itemId", "traitId", "s"]
[text|
$.post("/set/item/"+itemId+"/trait/"+traitId, {content: s})
.done(replaceWithData(node));
|]
js_submitItemInfo :: JSFunction a => a
js_submitItemInfo =
makeJSFunction "submitItemInfo" ["node", "itemId", "form"]
[text|
$.post("/set/item/"+itemId+"/info", $(form).serialize())
.done(replaceWithData(node));
|]
js_moveTraitUp :: JSFunction a => a
js_moveTraitUp =
makeJSFunction "moveTraitUp" ["itemId", "traitId", "traitNode"]
[text|
$.post("/move/item/"+itemId+"/trait/"+traitId, {direction: "up"});
moveNodeUp(traitNode);
|]
js_moveTraitDown :: JSFunction a => a
js_moveTraitDown =
makeJSFunction "moveTraitDown" ["itemId", "traitId", "traitNode"]
[text|
$.post("/move/item/"+itemId+"/trait/"+traitId, {direction: "down"});
moveNodeDown(traitNode);
|]
-- When adding a function, don't forget to add it to 'allJSFunctions'!
type JS = Text
-- A text button looks like “[cancel]”
textButton
:: Text -- ^ Button text
-> JS -- ^ Onclick handler
-> HtmlT IO ()
textButton caption handler =
span_ [class_ "textButton"] $
a_ [href_ "javascript:void(0)", onclick_ handler] (toHtml caption)
-- So far all icons used here have been from <https://useiconic.com/open/>
imgButton :: Url -> [Attribute] -> JS -> HtmlT IO ()
imgButton src attrs handler =
a_ [href_ "javascript:void(0)", onclick_ handler] (img_ (src_ src : attrs))
type JQuerySelector = Text
thisNode :: HtmlT IO JQuerySelector
thisNode = do
uid' <- randomUid
-- If the class name ever changes, fix 'js_moveNodeUp' and
-- 'js_moveNodeDown'.
span_ [id_ (tshow uid'), class_ "dummy"] mempty
return (T.pack (show (format ":has(> #{})" [uid'])))
lucid :: HtmlT IO a -> ActionT IO a
lucid h = do
htmlText <- liftIO (renderTextT h)
html (TL.toStrict htmlText)
-- | Format a string (a bit 'Text.Printf.printf' but with different syntax).
format :: Format.Params ps => Format -> ps -> Text
format f ps = TL.toStrict (Format.format f ps)
tshow :: Show a => a -> Text
tshow = T.pack . show
data Editable = Normal | Editable | InEdit
instance PathPiece Editable where
fromPathPiece "normal" = Just Normal
fromPathPiece "editable" = Just Editable
fromPathPiece "in-edit" = Just InEdit
fromPathPiece _ = Nothing
toPathPiece Normal = "normal"
toPathPiece Editable = "editable"
toPathPiece InEdit = "in-edit"
class LiftJS a where liftJS :: a -> Text
instance LiftJS Text where liftJS = id
instance LiftJS Integer where liftJS = tshow
instance LiftJS Int where liftJS = tshow
instance LiftJS Editable where liftJS = tshow . toPathPiece
class JSParams a where
jsParams :: a -> [Text]
instance JSParams () where
jsParams () = []
instance LiftJS a => JSParams [a] where
jsParams = map liftJS
instance (LiftJS a, LiftJS b) => JSParams (a,b) where
jsParams (a,b) = [liftJS a, liftJS b]
instance (LiftJS a, LiftJS b, LiftJS c) => JSParams (a,b,c) where
jsParams (a,b,c) = [liftJS a, liftJS b, liftJS c]
instance (LiftJS a, LiftJS b, LiftJS c, LiftJS d) => JSParams (a,b,c,d) where
jsParams (a,b,c,d) = [liftJS a, liftJS b, liftJS c, liftJS d]
-- TODO: why not compare Haskellers too?
sanitiseUrl :: Url -> Maybe Url
sanitiseUrl u
| not (sanitaryURI u) = Nothing
| "http:" `T.isPrefixOf` u = Just u
| "https:" `T.isPrefixOf` u = Just u
| otherwise = Just ("http://" <> u)
-- | Move the -1st element that satisfies the predicate- up.
moveUp :: (a -> Bool) -> [a] -> [a]
moveUp p (x:y:xs) = if p y then (y:x:xs) else x : moveUp p (y:xs)
moveUp _ xs = xs
-- | Move the -1st element that satisfies the predicate- down.
moveDown :: (a -> Bool) -> [a] -> [a]
moveDown p (x:y:xs) = if p x then (y:x:xs) else x : moveDown p (y:xs)
moveDown _ xs = xs