{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Yesod.PureScript ( PureScriptSite, YesodPureScript, YesodPureScriptOptions(..), addPureScriptWidget, createYesodPureScriptSite, defaultYesodPureScriptOptions, getPureScriptRoute ) where -- PureScript Yesod Subsite -- goal is to serve eg ./purs/foo.purs file at /purs/foo.js url. import Control.Applicative ((<$>)) import Control.Exception (catch, SomeException) import Control.Monad (forever, forM, forM_) import Control.Monad.IO.Class (liftIO) import Data.Either (rights) import Data.List (isSuffixOf) import Data.Maybe (catMaybes, mapMaybe) import Data.Text (Text) import Data.Time (UTCTime, getCurrentTime) import Filesystem.Path (()) import Formatting import Formatting.Time import Language.Haskell.TH import Language.PureScript (Module(Module)) import Prelude import Text.Julius import Text.Regex.TDFA ((=~)) import Text.Regex.TDFA.Text () import Yesod.Core ( HandlerT , Route , TypedContent (TypedContent) , Yesod , YesodSubDispatch , getYesod , mkYesodSubDispatch , shamlet , toContent , toTypedContent , toWidget , yesodSubDispatch ) import qualified Control.Concurrent as C import qualified Control.Concurrent.MVar as CM import qualified Data.Aeson.Types as AesonTypes import qualified Data.ByteString as BS import qualified Data.Default as DD import qualified Data.Map.Strict as M import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Text.IO as TIO import qualified Data.Text.Lazy as TL import qualified Filesystem as FS import qualified Filesystem.Path as FSP import qualified Filesystem.Path.CurrentOS as FSPC import qualified Language.PureScript as P import qualified System.FSNotify as SFN import Yesod.PureScript.Data -- | Things that are Yesod master can also be YesodPureScript master. class Yesod master => YesodPureScript master -- | All things that are "YesodPureScript master", are also this other thing, -- because they /nobody knows, because TH/. instance YesodPureScript master => YesodSubDispatch PureScriptSite (HandlerT master IO) where yesodSubDispatch = $(mkYesodSubDispatch resourcesPureScriptSite) -- | YesodPureScriptOptions thing is also Data.Default.Default, because it has "def", -- which is defaultYesodPureScriptOptions. instance DD.Default YesodPureScriptOptions where def = defaultYesodPureScriptOptions -- | Shortcut type for handlers of this subsite. type PureScriptHandler a = (YesodPureScript master) => HandlerT PureScriptSite (HandlerT master IO) a -- | Default options for YesodPureScript. -- Needed when creating PureScriptSite. -- Please don't create YesodPureScriptOptions by calling constructor directly, -- so I can add more options without breaking your code. defaultYesodPureScriptOptions :: YesodPureScriptOptions defaultYesodPureScriptOptions = YesodPureScriptOptions { ypsoSourceDirectories = ["purs", "bower_components"] , ypsoSourceIgnores = [] , ypsoErrorDivId = Nothing , ypsoVerboseErrors = False , ypsoMode = Dynamic , ypsoCompileOptions = [] } -- | Create pure script site. -- Initialises MVar of compiled modules to empty map. createYesodPureScriptSite :: YesodPureScriptOptions -> IO PureScriptSite createYesodPureScriptSite opts = do let state = PureScriptSiteState { psssWatchStarted = False , psssModules = M.empty , psssCompiledModules = M.empty } mv <- CM.newMVar state return $ PureScriptSite { pssState = mv , pssOptions = opts } -- | For convenience: turns a path as list into a route. getPureScriptRoute :: [Text] -> Route PureScriptSite getPureScriptRoute p = PureScriptCompiledR p -- | Create JS error report that tries to insert itself into parent page. createJavaScriptError :: TL.Text -> TL.Text -> TL.Text createJavaScriptError errorDivId errorText = renderJavascriptUrl render tmpl where render _ _ = error "no links supported here" _s _tl = AesonTypes.String (TL.toStrict _tl) tmpl = [julius| var err = #{_s errorText}; var errorDiv = document.getElementById(#{_s errorDivId}); if (window && window.console && window.console.log) { window.console.log(err); } if (errorDiv) { var errnode = document.createTextNode(err); var prenode = document.createElement("pre"); prenode.appendChild(errnode); if (errorDiv.firstChild) { errorDiv.insertBefore(prenode, errorDiv.firstChild); } else { errorDiv.appendChild(errnode); } } |] getPureScriptInfo :: PureScriptSite -> PureScriptHandler TypedContent getPureScriptInfo site = do -- map of filename to either err module moduleMap <- liftIO $ CM.withMVar (pssState site) $ \state -> return (psssModules state) -- this is to filter map items whose values are tuples of (a0, Either ...), -- and return those items with keys as tuples of (key, (a0, unboxed right)). -- so this is pattern match on snd of tuple that either gives tuple of unboxed right in just or nothing... let _justSndRight (_k, (_t, _mv)) = case _mv of Right _v -> Just (_k, (_t, _v)) _ -> Nothing -- similar to _justSndRight above, but returns value from Left (or nothing) let _justSndLeft (_k, (_t, _mv)) = case _mv of Left _v -> Just (_k, (_t, _v)) _ -> Nothing -- (fn, [module]) let fnsmodules = mapMaybe _justSndRight (M.toAscList moduleMap) :: [(FSP.FilePath, (UTCTime, [Module]))] -- (fileName, error) let fnerrs = mapMaybe _justSndLeft (M.toAscList moduleMap) :: [(FSP.FilePath, (UTCTime, Text))] let _formatTime _t = format (dateDash % " " % hms) _t _t let filePathToText fp = case FSPC.toText fp of Left _t -> _t Right _t -> _t return $ toTypedContent $ [shamlet| $doctype 5