{-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE ViewPatterns #-} module Snipcheck where import Control.Monad import Control.Exception import Control.Monad.IO.Class import Data.Char (isSpace) import Data.List (dropWhileEnd) import Data.Maybe import System.Process(readCreateProcess, shell) import Text.Pandoc (Block(..)) import qualified Data.Text.IO as Text import qualified Data.Map as Map import qualified Text.Pandoc as Pandoc data Sloppy a = Skip | Must a deriving (Show, Functor) sloppyString :: String -> Sloppy String sloppyString "..." = Skip sloppyString str = Must str checkSloppy :: Eq a => [a] -> [Sloppy a] -> Bool checkSloppy (a:as) (Must a':as') | a == a' = checkSloppy as as' | otherwise = False checkSloppy (a:as) as'@(Skip:Must a':as'') | a == a' = checkSloppy as as'' | otherwise = checkSloppy as as' checkSloppy as (Skip:Skip:as') = checkSloppy as (Skip:as') checkSloppy [] (Must{}:_) = False checkSloppy [] (Skip:as') = checkSloppy [] as' checkSloppy [] [] = True checkSloppy (_:_) [] = False checkSloppy _ [Skip] = True checkMarkdownFile :: FilePath -> IO () checkMarkdownFile fp = do content <- Text.readFile fp eres <- Pandoc.runIO $ do Pandoc.Pandoc meta blocks <- Pandoc.readMarkdown Pandoc.def content let sections = findSections meta blocks' = if null sections then blocks else filterBlocksBySectionName sections blocks forM_ blocks' check case eres of Right () -> pure () Left e -> throwIO $ userError $ show e data AcceptSection = GoodSection | BadSection | Dunno filterBlocksBySectionName :: [String] -> [Pandoc.Block] -> [Pandoc.Block] filterBlocksBySectionName secs = skipThese where skipThese, keepThese :: [Pandoc.Block] -> [Pandoc.Block] skipThese (b:bs) = case acceptSection b of GoodSection -> keepThese bs _ -> skipThese bs skipThese [] = [] keepThese (b:bs) = b : case acceptSection b of BadSection -> skipThese bs _ -> keepThese bs keepThese [] = [] acceptSection :: Pandoc.Block -> AcceptSection acceptSection (Pandoc.Header _ (hName,_,_) _) | hName `elem` secs = GoodSection | otherwise = BadSection acceptSection _ = Dunno findSections :: Pandoc.Meta -> [String] findSections (Pandoc.unMeta -> meta) = case Map.lookup "sc_check-sections" meta of Just (Pandoc.MetaList ss) -> join $ unMetaString <$> ss _ -> [] where unMetaString :: Pandoc.MetaValue -> [String] unMetaString (Pandoc.MetaString s) =[s] unMetaString (Pandoc.MetaInlines is) = mapMaybe unMetaStr is unMetaString _ = [] unMetaStr :: Pandoc.Inline -> Maybe String unMetaStr (Pandoc.Str s) = Just s unMetaStr _ = Nothing trim :: String -> String trim = dropWhile isSpace . dropWhileEnd isSpace check :: MonadIO m => Pandoc.Block -> m () check (CodeBlock (typ, classes, kvs) content) | "shell" `elem` classes = do let Right cmds = extractCommands content forM_ cmds $ \(cmd, expected) -> do actual <- (fmap trim . lines) <$> liftIO (readCreateProcess (shell cmd) "") let expected' = (sloppyString . trim) <$> expected unless (checkSloppy actual expected') $ error $ mconcat [ "Couldnt match expected ", show expected' , " with " <> show actual ] | otherwise = liftIO $ print (typ, classes, kvs) check _ = return () extractCommands :: String -> Either String [(String, [String])] extractCommands str = go (lines str) where go :: [String] -> Either String [(String, [String])] go (l:ls) | Just cmd <- toCommand l = let (output, rest) = break isCommand ls in ((cmd,output):) <$> go rest | otherwise = Left $ "Expected a command, got " <> l go [] = Right [] toCommand :: String -> Maybe String toCommand ('$':cmd) = Just cmd toCommand _ = Nothing isCommand :: String -> Bool isCommand = isJust . toCommand someFunc :: IO () someFunc = putStrLn "someFunc"