module Settings.WellKnownFiles.TH ( mkWellKnown ) where import ClassyPrelude.Yesod import Utils import Language.Haskell.TH import Language.Haskell.TH.Syntax hiding (Lift(..)) import qualified Language.Haskell.TH.Syntax as TH (Lift(..)) import System.Directory.Tree import qualified Data.ByteString as BS import Utils.Lens.TH import Control.Lens import Data.Set.Lens import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import Data.Char as Char (isAlphaNum, toUpper) import qualified Data.Map as Map import qualified Data.Set as Set import Data.List.NonEmpty (NonEmpty(..)) import qualified Data.HashMap.Strict as HashMap import qualified Data.HashSet as HashSet import System.FilePath (splitDirectories, makeRelative) import Settings.Mime import Text.Blaze.Html (preEscapedToHtml) import Control.Monad.Fail nWellKnownFileName :: Name nWellKnownFileName = mkName "WellKnownFileName" nwellKnownFileNames :: Name nwellKnownFileNames = mkName "wellKnownFileNames" ngetWellKnownR :: Name ngetWellKnownR = mkName "getWellKnownR" nwellKnownHtmlLinks :: Name nwellKnownHtmlLinks = mkName "wellKnownHtmlLinks" mkWellKnown :: Lang -- ^ Default language -> FilePath -- ^ Base directory -> FilePath -- ^ Link file (@html_code.html@) -> DecsQ mkWellKnown defLang wellKnownBase wellKnownLinks = do inputFiles <- fmap dirTree . liftIO $ readDirectoryWith (\f -> (f, ) <$> BS.readFile f) wellKnownBase mapM_ qAddDependentFile $ inputFiles ^.. folded . _1 -- languageFiles :: Map Lang [(FilePath, ByteString)] languageFiles <- if | Dir{contents} <- inputFiles -> return . Map.fromList $ do (language, lContents) <- contents ^.. folded . $(multifocusL 2) _name id Dir{} <- pure lContents let lContents' :: [(FilePath, ByteString)] lContents' = flip mapMaybe (flattenDir lContents) $ \pFile -> do File{..} <- pure pFile guard $ name /= wellKnownLinks return $ file & _1 %~ makeRelative (wellKnownBase language) return (Text.pack language, lContents') | otherwise -> fail "wellKnownBase is not a directory" fLanguages <- if | defLang `Set.member` Map.keysSet languageFiles , let languages' = Set.delete defLang $ Map.keysSet languageFiles -> return $ defLang :| Set.toList languages' | otherwise -> fail "default language is missing in wellKnownBase" -- languageLinks :: Map Lang ByteString languageLinks <- if | Dir{contents} <- inputFiles -> return . Map.fromList $ do (language, lContents) <- contents ^.. folded . $(multifocusL 2) _name id Dir{} <- pure lContents let lContents' :: [ByteString] lContents' = flip mapMaybe (flattenDir lContents) $ \pFile -> do File{..} <- pure pFile guard $ name == wellKnownLinks return $ file ^. _2 c <- lContents' return (Text.pack language, c) | otherwise -> fail "wellKnownBase is not a directory" lLanguages <- if | defLang `Set.member` Map.keysSet languageLinks , let languages' = Set.delete defLang $ Map.keysSet languageLinks -> return $ defLang :| Set.toList languages' | otherwise -> fail "default language is missing in wellKnownBase" fVar <- newName "f" hVar <- newName "h" lVar <- newName "l" let fileNames = setOf (folded . folded . _1) languageFiles fileContents = Map.fromListWith (<>) $ do (lang, fs) <- Map.toList languageFiles (fName, fContent) <- fs return ((fContent, mimeLookup $ Text.pack fName), Set.singleton (lang, fName)) wellKnownFileName = dataD (cxt []) nWellKnownFileName [] Nothing [ normalC (mkName $ fNameManip fName) [] | fName <- Set.toList fileNames ] (pure $ derivClause Nothing [[t|Eq|], [t|Ord|], [t|Bounded|], [t|Enum|], [t|Read|], [t|Show|], [t|Generic|], [t|Typeable|]]) wellKnownFileNameMapSig = sigD nwellKnownFileNames [t|HashMap [Text] $(conT nWellKnownFileName)|] wellKnownFileNameMap = funD nwellKnownFileNames [ clause [] (normalB $ [e|HashMap.fromList|] `appE` listE [ [e|($(TH.lift . map Text.pack $ splitDirectories fName), $(conE . mkName $ fNameManip fName))|] | fName <- Set.toList fileNames ]) [] ] wellKnownFileNamePathMultiPiece = instanceD (cxt []) (conT ''PathMultiPiece `appT` conT nWellKnownFileName) [ funD 'toPathMultiPiece [ clause [conP (mkName $ fNameManip fName) []] (normalB . TH.lift . map Text.pack $ splitDirectories fName) [] | fName <- Set.toList fileNames ] , funD 'fromPathMultiPiece [ clause [] (normalB [e|flip HashMap.lookup $(varE nwellKnownFileNames)|]) [] ] ] wellKnownFileNameHashable = instanceD (cxt []) (conT ''Hashable `appT` conT nWellKnownFileName) [] getWellKnownRSig = sigD ngetWellKnownR [t|forall m. MonadHandler m => $(conT nWellKnownFileName) -> m TypedContent|] getWellKnownR = funD ngetWellKnownR [ clause [varP fVar] (normalB [e|$(varE hVar) =<< selectLanguage fLanguages|]) [ funD hVar $ [ clause [varP lVar] (guardedB [ (,) <$> normalG [e|HashSet.member ($(varE lVar), $(varE fVar)) $ HashSet.fromList $(listE [ tupE [TH.lift l, conE . mkName $ fNameManip fName] | (l, fName) <- Set.toList xs ])|] <*> [e|TypedContent mime (toContent fContent) <$ setEtag $(TH.lift $ hashToText (mime, fContent))|] ]) [] | ((fContent, mime), xs) <- Map.toList fileContents ] ++ pure (clause [wildP] (normalB [e|notFound|]) []) ] ] wellKnownHtmlLinksSig = sigD nwellKnownHtmlLinks [t|forall m. MonadWidget m => m ()|] wellKnownHtmlLinks = funD nwellKnownHtmlLinks [ clause [] (normalB [e|toWidgetHead . preEscapedToHtml . $(varE hVar) =<< selectLanguage lLanguages|]) [ sigD hVar [t|Text -> Text|] , funD hVar $ [ clause [varP lVar] (guardedB [ (,) <$> normalG [|$(varE lVar) == lang|] <*> TH.lift (Text.filter (`notElem` ['\r', '\n']) $ Text.decodeUtf8 c) ]) [] | (lang, c) <- Map.toList languageLinks ] ++ pure (clause [wildP] (normalB [e|mempty|]) []) ] ] sequence [ wellKnownFileName, wellKnownFileNameMapSig, wellKnownFileNameMap, wellKnownFileNamePathMultiPiece, wellKnownFileNameHashable , getWellKnownRSig, getWellKnownR , wellKnownHtmlLinksSig, wellKnownHtmlLinks ] where fNameManip = Text.unpack . mconcat . over (traverse . _head) Char.toUpper . filter (not . null) . Text.split (not . isAlphaNum) . Text.pack