packages feed

Phsu-0.1.0.1: src/Search.hs

{-# LANGUAGE OverloadedStrings #-}
module Search(serveSearch, editSearch) where

import BoilerplateDB (App, URLSubDb, URLSub, exportURLSub, exportAllURLSubRules, exportAddURLSub, exportRemoveURLSub, exportUpdateURLSub, exportHitRule, exportReorderURLSub, regex, substitute, ruleId, hits)
import Control.Applicative (optional, (<$>))
import Control.Concurrent.MVar (takeMVar)
import Control.Monad (forM_)
import Control.Monad.Trans  (MonadIO(..))
import Data.List (sortBy)
import Data.List.Utils (startswith)
import Data.Maybe (mapMaybe)
import Data.Ord (comparing)
import Data.String.Conversions (cs)
import Happstack.Server
import Happstack.Server.SURI (parse, SURI)
import Network.URI (escapeURIString, isUnescapedInURI)
import Template (template, myPolicy)
import Text.Blaze.Html5 (Html, (!), toHtml, toValue)
import Text.Blaze.Html5.Attributes (action, name, size, type_, value)
import Text.Regex.PCRE ((=~))
import qualified Data.IntMap as IntMap
import qualified Data.IntSet as IntSet
import qualified Data.Map as Map
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A

defaultSearch str = (++) "https://www.google.com/search?q=" $ str

groups :: String -> String -> [String]
groups regx str = let (_, _, _, g) = ((str =~ regx)::(String, String, String, [String])) in g

repl :: String -> String -> String -> String
repl "" g id = ""
repl ('\\':'\\':x) g id = (++) "\\\\" $ repl x g id
repl ('\\':x) g id =
	if startswith id x then g ++ (repl (drop (length id) x) g id) else '\\':(repl x g id)
repl (x:xs) g id = x:(repl xs g id)

applySub :: String -> [String] -> String
applySub str grps = foldl (\s -> \(g, i) -> repl s g $ show i) str $ zip grps [1..]


applyRule :: String -> URLSub -> Maybe (String, URLSub)
applyRule q rule =
	let pat = regex rule
	    sub = substitute rule
	in if q =~ ("^"++pat++"$") then Just (applySub sub $ groups pat q, rule) else Nothing

myparse :: String -> SURI
myparse str = case (parse $ escapeURIString isUnescapedInURI str) of Just x -> x

urlResponse url = seeOther (myparse url) $ template "Happstack Server Utils" "HSU"
serveSearch :: App Response
serveSearch = do
	q <- lookText "q"
	rules <- (sortBy (comparing $ \x -> ruleId x)) <$> exportAllURLSubRules
	let matches = (mapMaybe (applyRule $ cs q) rules) in
		case matches of
			(url, r):_ -> do
							exportHitRule $ ruleId r
							urlResponse url
			[]         -> urlResponse (defaultSearch $ cs q)

renderRule::URLSub -> Html
renderRule rule = do
	H.td $ H.input ! type_ "text"
	               ! (name . toValue) ("reorder-" ++ (show $ ruleId rule))
	               ! (value . toValue) (show $ ruleId rule)
	               ! size "2"
	H.td $ H.input ! type_ "checkbox"
	               ! (name . toValue) ("remove-" ++ (show $ ruleId rule))
	               ! (value . toValue) ("yes" :: String)
	H.td (H.label (toHtml $ show (ruleId rule)))
	H.td $ H.input ! type_ "text"
	               ! (name . toValue) ("regex-" ++ (show $ ruleId rule))
	               ! size "20"
	               ! value (toValue $ regex rule)
	H.td $ H.input ! type_ "text"
	               ! (name . toValue) ("substitute-" ++ (show $ ruleId rule))
	               ! size "100"
	               ! value (toValue $ substitute rule)
	H.td (H.label (toHtml $ show (hits rule)))

rulesHeader::Html
rulesHeader = do
	H.td (H.label "Re-ord")
	H.td (H.label "Delete")
	H.td (H.label "Rule ID")
	H.td (H.label "Regex")
	H.td (H.label "Substitution Rule")
	H.td (H.label "Hits so far")

rulesFoot::Html
rulesFoot = do
	H.td $ do
		H.button ! name "op" ! value (toValue ("reorder"::String)) $ do 
			toHtml ("Reoder"::String)
	H.td ! A.colspan (toValue ("5"::String))
	     ! A.style (toValue ("text-align:right"::String)) $ do
		H.button ! name "op" ! value (toValue ("edit"::String)) $ toHtml ("Save Changes"::String)

serveEditSearch::String -> App Response
serveEditSearch str = do
	rules <- (sortBy (comparing $ \x -> ruleId x)) <$> exportAllURLSubRules
	ok $
		template "Search Pattern Settings" $ do
			H.body $ do
				H.h1 . toHtml $ "Total rules :: " ++ (show $ length rules)
				H.form ! action "/edit-search" ! A.method (toValue ("POST"::String)) $ do
					H.table $ do
						H.thead rulesHeader
						H.tbody (forM_ rules (H.tr . renderRule))
						H.tfoot ! A.style (toValue $ ("border:0px"::String)) $ rulesFoot
				H.form ! action "/edit-search" ! A.method (toValue ("POST"::String)) $ do
					H.table $ do
						H.thead $ do
							H.td ! A.colspan (toValue ("2"::String))
							     ! A.style (toValue ("padding-left:80px"::String)) $ do
							     	H.b $ toHtml ("Add a new Rule"::String)
						H.tbody $ do
							H.tr $ do
								H.td $ H.label . toHtml $ ("regex"::String)
								H.td $ H.input ! type_ "text" ! name "regex" ! size "100"
							H.tr $ do
								H.td $ H.label . toHtml $ ("substitution regex"::String)
								H.td $ H.input ! type_ "text" ! name "substitute" ! size "100"
						H.tfoot $ do
							H.td ! A.colspan (toValue ("2"::String)) $ do
								H.button ! name "op" ! value (toValue ("add"::String)) $ toHtml ("Add Rule"::String)

data Edit = Delete Int | Ed URLSub deriving (Show)

rid :: String -> String -> Int
rid str "" = read str
rid (a:as) (b:bs) = rid as bs

defaultEdit :: Int -> URLSub
defaultEdit id = exportURLSub undefined undefined 0 id

--extractStr value
handleRemove intMap id = IntMap.insert id (Delete id) intMap
handleEditRegex intMap reg id = case IntMap.findWithDefault (Ed $ defaultEdit id) id intMap of
			Delete _ -> intMap
			Ed r -> IntMap.insert id (Ed r{regex=reg}) intMap
handleEditSubstitute intMap sub id = case IntMap.findWithDefault (Ed $ defaultEdit id) id intMap of
			Delete _ -> intMap
			Ed r -> IntMap.insert id (Ed r{substitute=sub}) intMap

handleSingleParam param value intMap
	| startswith "remove-" param = handleRemove intMap $ rid param "remove-"
	| startswith "regex-" param = handleEditRegex intMap (extractStr value) $ rid param "regex-"
	| startswith "substitute-" param = handleEditSubstitute intMap (extractStr value) $ rid param "substitute-"
	| otherwise = intMap

prepareEdits reqMap = Map.foldWithKey handleSingleParam IntMap.empty reqMap

processEdits (Delete id) = (:) $ exportRemoveURLSub id
processEdits (Ed rule) = (:) $ exportUpdateURLSub rule
reqMapToMods reqMap = IntMap.fold processEdits [] $ prepareEdits reqMap

handleReorderParam param value intMap
	| startswith "reorder-" param = IntMap.insert (rid param "reorder-") (read $ extractStr value) intMap
	| otherwise = intMap

fetchReMapping reqMap = Map.foldWithKey handleReorderParam IntMap.empty reqMap

checkNoOverLap :: (IntMap.IntMap Int) -> (IntMap.IntMap Int)
checkNoOverLap = fst . (IntMap.foldWithKey func (IntMap.empty, IntSet.empty)) where
	func key val (mp, st)
		| IntSet.member val st = (mp, st)
		| otherwise = (IntMap.insert key val mp, IntSet.insert val st)

processEditForm reqMap = do
	case readKey reqMap "op" of
		"add" -> let regex = cs $ readKey reqMap "regex"
		             sub = cs $ readKey reqMap "substitute"
		         in exportAddURLSub $ exportURLSub regex sub 0 0
		"edit" -> foldl (>>) (return ()) $ reqMapToMods reqMap
		"reorder" -> exportReorderURLSub . checkNoOverLap . fetchReMapping $ reqMap

extractStr :: Input -> String
extractStr input = case (inputValue input) of
	Right x -> cs x
readKey reqMap key =
	extractStr (reqMap Map.! key) where

editSearch :: App Response
editSearch = do
	m <- rqMethod <$> askRq
	case m of
		GET ->
			serveEditSearch $ "Meh"
		POST -> do
			decodeBody myPolicy
			all <- rqInputsBody <$> askRq
			dat <- Map.fromList <$> (liftIO $ takeMVar all)
			processEditForm dat
			serveEditSearch $ (show.prepareEdits) dat