import Chez.Grater.Internal.Prelude import Chez.Grater (scrapeAndParseUrl, scrapeUrl) import Chez.Grater.Manager (createManager) import Chez.Grater.Parser (mkIngredients, quantityP, runParser) import Chez.Grater.Parser.Types (ParsedQuantity(..)) import Chez.Grater.Readable.Types (mkReadableIngredient, mkReadableQuantity, showReadableIngredient, showReadableQuantity) import Chez.Grater.Scraper.Site (allScrapers) import Chez.Grater.Scraper.Types (ScrapedIngredient(..), ScrapedRecipeName(..), ScrapedStep(..)) import Chez.Grater.Types (unIngredientName, Ingredient(..), IngredientName(..), Quantity(..), RecipeName(..), Step(..)) import Data.Aeson ((.=), object) import Data.Aeson.Encode.Pretty (encodePretty) import Network.URI (parseURI) import Options.Applicative ((<**>)) import qualified Data.ByteString.Lazy.Char8 as LC8 import qualified Data.CaseInsensitive as CI import qualified Data.Text as Text import qualified Options.Applicative as Opt data Output = OutputText | OutputJson deriving (Eq) instance Show Output where show = \case OutputText -> "text" OutputJson -> "json" data Opts = Opts { optsUrl :: String , optsNoParse :: Bool , optsScale :: Maybe Double , optsOutput :: Output } parseArgs :: IO Opts parseArgs = Opt.execParser (Opt.info (parser <**> Opt.helper) (Opt.progDesc "Scrape a recipe from a URL")) where parser = Opts <$> Opt.strArgument ( Opt.metavar "URL" ) <*> Opt.switch ( Opt.long "no-parse" <> Opt.help "Don't parse the recipe" ) <*> Opt.option readScale ( Opt.short 's' <> Opt.long "scale" <> Opt.value Nothing <> Opt.showDefault <> Opt.help "Adjust ingredient quantities" ) <*> Opt.option readOutput ( Opt.short 'o' <> Opt.long "output" <> Opt.value OutputText <> Opt.showDefault <> Opt.help "Output format for the recipe (text or json)" ) readOutput = Opt.maybeReader $ \case "text" -> Just OutputText "json" -> Just OutputJson _ -> Nothing readScale = Opt.eitherReader $ \str -> case (runParser quantityP . Text.pack) str of Right (ParsedQuantity 1) -> Right Nothing Right (ParsedQuantity q) -> Right (Just q) Right _ -> Left "Expected numeric quantity" Left err -> Left err scaleIngredientBy :: Double -> Ingredient -> Ingredient scaleIngredientBy scale (Ingredient name quantity unit) = case quantity of Quantity q -> Ingredient name (Quantity (q*scale)) unit QuantityMissing -> Ingredient (IngredientName . (prefix <>) . unIngredientName $ name) quantity unit where prefix = case (showReadableQuantity . mkReadableQuantity . Quantity) scale of Just str -> CI.mk $ "(" <> str <> "x) " -- FIXME: should be "×" but unicode 😵‍💫 Nothing -> "" showRecipe :: RecipeName -> [Ingredient] -> [Step] -> IO () showRecipe (RecipeName name) ingredients steps = do let width = length (Text.unpack name) putStrLn $ Text.unpack name putStrLn $ replicate width '-' traverse_ putStrLn $ fmap (Text.unpack . showReadableIngredient . mkReadableIngredient) ingredients putStrLn $ replicate width '-' traverse_ putStrLn $ fmap (("- " <>) . Text.unpack . unStep) steps jsonRecipe :: RecipeName -> [Ingredient] -> [Step] -> IO () jsonRecipe name ingredients steps = putStrLn . LC8.unpack . encodePretty . object $ [ "name" .= name , "ingredients" .= fmap mkReadableIngredient ingredients , "steps" .= fmap unStep steps ] renderRecipe :: Output -> RecipeName -> [Ingredient] -> [Step] -> IO () renderRecipe = \case OutputText -> showRecipe OutputJson -> jsonRecipe main :: IO () main = do Opts {..} <- parseArgs manager <- createManager url <- maybe (fail "Invalid URL") pure $ parseURI optsUrl case optsNoParse of True -> do (name, ingredients, steps, _) <- scrapeUrl allScrapers manager url renderRecipe optsOutput (RecipeName . unScrapedRecipeName $ name) (concatMap (mkIngredients . unScrapedIngredient) ingredients) (fmap (Step . unScrapedStep) steps) False -> do (name, ingredients, steps, _) <- scrapeAndParseUrl allScrapers manager url let scaledIngredients :: [Ingredient] = (maybe id (fmap . scaleIngredientBy) optsScale) ingredients renderRecipe optsOutput name scaledIngredients steps