30dd268f2c
- Update Roux.Types: add SectionBodyItem (SecStep/SecComment/SecNote), StepEndComment, remove StepNote (notes are now section-level) - Rewrite Roux.Parser using Parsec for inline elements, text-splitting for file-level structure - Handle full-line -- comments, > notes, = Name = section headers - Handle = Name (w/ and w/o closing =), == Name == variations - Handle end-of-line backslash line breaks within steps - Update tests to match new model + 3 EasyPancakes integration tests - Add expectTextStep helper at module level
198 lines
9.1 KiB
Haskell
198 lines
9.1 KiB
Haskell
module Roux.ParserSpec (spec) where
|
|
|
|
import Data.List.NonEmpty (NonEmpty ((:|)))
|
|
import qualified Data.List.NonEmpty as NE
|
|
import Test.Hspec (Expectation, Spec, describe, expectationFailure, it, shouldBe)
|
|
|
|
import Roux.Parser (parseCookFile)
|
|
import Roux.Types
|
|
|
|
spec :: Spec
|
|
spec = do
|
|
describe "parseCookFile" $ do
|
|
it "parses a single step of plain text" $ do
|
|
let input = "Hello, world."
|
|
expected =
|
|
Right
|
|
Recipe
|
|
{ recipeMetadata = emptyMetadata
|
|
, recipeSections =
|
|
Section
|
|
{ sectionName = Nothing
|
|
, sectionBody =
|
|
SecStep
|
|
(Step [StepText "Hello, world."])
|
|
:| []
|
|
}
|
|
:| []
|
|
}
|
|
parseCookFile input `shouldBe` expected
|
|
|
|
it "parses multiple steps separated by a blank line" $ do
|
|
let input = "Wash the potatoes.\n\nPeel and dice them."
|
|
expected =
|
|
Right
|
|
Recipe
|
|
{ recipeMetadata = emptyMetadata
|
|
, recipeSections =
|
|
Section
|
|
{ sectionName = Nothing
|
|
, sectionBody =
|
|
SecStep (Step [StepText "Wash the potatoes."])
|
|
:| [ SecStep
|
|
(Step [StepText "Peel and dice them."])
|
|
]
|
|
}
|
|
:| []
|
|
}
|
|
parseCookFile input `shouldBe` expected
|
|
|
|
it "handles empty input by returning an empty recipe" $ do
|
|
let expected =
|
|
Right
|
|
Recipe
|
|
{ recipeMetadata = emptyMetadata
|
|
, recipeSections =
|
|
Section
|
|
{ sectionName = Nothing
|
|
, sectionBody = SecStep (Step []) :| []
|
|
}
|
|
:| []
|
|
}
|
|
parseCookFile "" `shouldBe` expected
|
|
|
|
it "trims leading and trailing whitespace from each block" $ do
|
|
let input = "\n\n Mix well. \n\n"
|
|
expected =
|
|
Right
|
|
Recipe
|
|
{ recipeMetadata = emptyMetadata
|
|
, recipeSections =
|
|
Section
|
|
{ sectionName = Nothing
|
|
, sectionBody =
|
|
SecStep (Step [StepText "Mix well."]) :| []
|
|
}
|
|
:| []
|
|
}
|
|
parseCookFile input `shouldBe` expected
|
|
|
|
it "parses a full-line comment" $ do
|
|
let input = "-- TODO add source"
|
|
expected =
|
|
Right
|
|
Recipe
|
|
{ recipeMetadata = emptyMetadata
|
|
, recipeSections =
|
|
Section
|
|
{ sectionName = Nothing
|
|
, sectionBody = SecComment "TODO add source" :| []
|
|
}
|
|
:| []
|
|
}
|
|
parseCookFile input `shouldBe` expected
|
|
|
|
it "parses a note block" $ do
|
|
let input = "> Don't burn the roux!"
|
|
expected =
|
|
Right
|
|
Recipe
|
|
{ recipeMetadata = emptyMetadata
|
|
, recipeSections =
|
|
Section
|
|
{ sectionName = Nothing
|
|
, sectionBody = SecNote "Don't burn the roux!" :| []
|
|
}
|
|
:| []
|
|
}
|
|
parseCookFile input `shouldBe` expected
|
|
|
|
it "parses a section header" $ do
|
|
let input = "= Dough"
|
|
expected =
|
|
Right
|
|
Recipe
|
|
{ recipeMetadata = emptyMetadata
|
|
, recipeSections =
|
|
Section
|
|
{ sectionName = Just "Dough"
|
|
, sectionBody = SecStep (Step []) :| []
|
|
}
|
|
:| []
|
|
}
|
|
parseCookFile input `shouldBe` expected
|
|
|
|
it "groups items before a section header into an unnamed section" $ do
|
|
let input = "Mix flour and water.\n\n= Filling\n\nCombine cheese and spinach."
|
|
expected =
|
|
Right
|
|
Recipe
|
|
{ recipeMetadata = emptyMetadata
|
|
, recipeSections =
|
|
Section
|
|
{ sectionName = Nothing
|
|
, sectionBody =
|
|
SecStep
|
|
(Step [StepText "Mix flour and water."])
|
|
:| []
|
|
}
|
|
:| [ Section
|
|
{ sectionName = Just "Filling"
|
|
, sectionBody =
|
|
SecStep
|
|
(Step [StepText "Combine cheese and spinach."])
|
|
:| []
|
|
}
|
|
]
|
|
}
|
|
parseCookFile input `shouldBe` expected
|
|
|
|
describe "EasyPancakes example" $ do
|
|
let pancakeInput =
|
|
"-- TODO add source\n"
|
|
<> "\n"
|
|
<> "Crack the @eggs{3} into a blender, then add the @flour{125%g}, @milk{250%ml} and @sea salt{1%pinch}, and blitz until smooth.\n"
|
|
<> "\n"
|
|
<> "Pour into a #bowl and leave to stand for ~{15%minutes}.\n"
|
|
<> "\n"
|
|
<> "Melt the butter (or a drizzle of @oil if you want to be a bit healthier) in a #large non-stick frying pan{} on a medium heat, then tilt the pan so the butter coats the surface.\n"
|
|
<> "\n"
|
|
<> "Pour in 1 ladle of batter and tilt again, so that the batter spreads all over the base, then cook for 1 to 2 minutes, or until it starts to come away from the sides.\n"
|
|
<> "\n"
|
|
<> "Once golden underneath, flip the pancake over and cook for 1 further minute, or until cooked through.\n"
|
|
<> "\n"
|
|
<> "Serve straightaway with your favourite topping.\n"
|
|
|
|
it "parses into 1 section with 7 body items" $ do
|
|
case parseCookFile pancakeInput of
|
|
Left err -> expectationFailure ("parse failed: " <> err)
|
|
Right recipe -> do
|
|
let section = NE.head (recipeSections recipe)
|
|
body = NE.toList (sectionBody section)
|
|
length body `shouldBe` 7
|
|
|
|
it "first body item is the full-line comment" $ do
|
|
case parseCookFile pancakeInput of
|
|
Left err -> expectationFailure ("parse failed: " <> err)
|
|
Right recipe -> do
|
|
let body = NE.toList (sectionBody (NE.head (recipeSections recipe)))
|
|
case body of
|
|
(SecComment t : _) -> t `shouldBe` "TODO add source"
|
|
other -> expectationFailure ("expected SecComment first, got " <> show other)
|
|
|
|
it "remaining 6 items are text-only steps" $ do
|
|
case parseCookFile pancakeInput of
|
|
Left err -> expectationFailure ("parse failed: " <> err)
|
|
Right recipe -> do
|
|
let body = NE.toList (sectionBody (NE.head (recipeSections recipe)))
|
|
case body of
|
|
(_comment : steps) -> mapM_ expectTextStep steps
|
|
[] -> expectationFailure "empty body"
|
|
|
|
-- | Check that a body item is a 'SecStep' containing a single 'StepText'.
|
|
expectTextStep :: SectionBodyItem -> Expectation
|
|
expectTextStep (SecStep (Step [StepText _])) = pure ()
|
|
expectTextStep other =
|
|
expectationFailure
|
|
("expected SecStep with single StepText, got " <> show other)
|