Phase 1: Model update + file-level parser (text-only steps)
- 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
This commit is contained in:
+133
-13
@@ -1,13 +1,14 @@
|
||||
module Roux.ParserSpec (spec) where
|
||||
|
||||
import Data.List.NonEmpty (NonEmpty ((:|)))
|
||||
import Test.Hspec (Spec, describe, it, shouldBe)
|
||||
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 =
|
||||
spec = do
|
||||
describe "parseCookFile" $ do
|
||||
it "parses a single step of plain text" $ do
|
||||
let input = "Hello, world."
|
||||
@@ -18,10 +19,9 @@ spec =
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionSteps =
|
||||
Step
|
||||
{ unStep = [StepText "Hello, world."]
|
||||
}
|
||||
, sectionBody =
|
||||
SecStep
|
||||
(Step [StepText "Hello, world."])
|
||||
:| []
|
||||
}
|
||||
:| []
|
||||
@@ -37,9 +37,10 @@ spec =
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionSteps =
|
||||
Step{unStep = [StepText "Wash the potatoes."]}
|
||||
:| [ Step{unStep = [StepText "Peel and dice them."]}
|
||||
, sectionBody =
|
||||
SecStep (Step [StepText "Wash the potatoes."])
|
||||
:| [ SecStep
|
||||
(Step [StepText "Peel and dice them."])
|
||||
]
|
||||
}
|
||||
:| []
|
||||
@@ -54,13 +55,13 @@ spec =
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionSteps = Step{unStep = []} :| []
|
||||
, sectionBody = SecStep (Step []) :| []
|
||||
}
|
||||
:| []
|
||||
}
|
||||
parseCookFile "" `shouldBe` expected
|
||||
|
||||
it "trims leading and trailing whitespace from each step" $ do
|
||||
it "trims leading and trailing whitespace from each block" $ do
|
||||
let input = "\n\n Mix well. \n\n"
|
||||
expected =
|
||||
Right
|
||||
@@ -69,9 +70,128 @@ spec =
|
||||
, recipeSections =
|
||||
Section
|
||||
{ sectionName = Nothing
|
||||
, sectionSteps =
|
||||
Step{unStep = [StepText "Mix well."]} :| []
|
||||
, 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)
|
||||
|
||||
Reference in New Issue
Block a user