Add minimal Cooklang parser with first tests

- Create Roux.Parser with parseCookFile (splits paragraphs, treats
  each as a plain-text step, no metadata parsing yet)
- Add emptyMetadata to Roux.Types for convenience
- Add test dependencies: hspec, tasty-golden, tasty-hspec
- Add ParserSpec with 4 tests covering single step, multiple steps,
  empty input, and whitespace trimming
- Wire ParserSpec into test runner via tasty-hspec
This commit is contained in:
2026-05-18 22:21:32 -04:00
parent e907a03b09
commit 235adce596
6 changed files with 149 additions and 2 deletions
+39
View File
@@ -0,0 +1,39 @@
{- | Parser for Cooklang (.cook) recipe files.
Currently a minimal placeholder that splits input into paragraphs and
treats them as plain-text steps. Proper inline parsing (ingredients,
cookware, timers, …) and metadata support will be added incrementally.
-}
module Roux.Parser
( parseCookFile
) where
import Data.List.NonEmpty (NonEmpty ((:|)))
import Data.Text (Text)
import qualified Data.Text as T
import Roux.Types
-- | Parse a complete @.cook@ file into a 'Recipe'.
--
-- Returns 'Left' with an error message on invalid input (currently never
-- — the placeholder always succeeds).
parseCookFile :: Text -> Either String Recipe
parseCookFile input =
let paragraphs = filter (not . T.null) (map T.strip (T.splitOn "\n\n" (T.strip input)))
in case paragraphs of
[] ->
Right Recipe
{ recipeMetadata = emptyMetadata
, recipeSections = Section Nothing (Step [] :| []) :| []
}
(p : ps) ->
Right Recipe
{ recipeMetadata = emptyMetadata
, recipeSections = Section Nothing (stepFromText p :| fmap stepFromText ps) :| []
}
-- | Turn a paragraph of text into a single 'Step' containing nothing but
-- 'StepText'. This is a placeholder until inline parsing is implemented.
stepFromText :: Text -> Step
stepFromText t = Step [StepText t]
+22
View File
@@ -47,10 +47,12 @@ module Roux.Types
-- * Metadata
, Metadata (..)
, emptyMetadata
) where
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Text (Text)
-- ---------------------------------------------------------------------------
@@ -183,3 +185,23 @@ data Metadata = Metadata
, metaCustom :: !(Map Text Text)
}
deriving stock (Eq, Show)
-- | Metadata with all fields set to their default / empty values.
emptyMetadata :: Metadata
emptyMetadata =
Metadata
{ metaTitle = Nothing
, metaServings = Nothing
, metaAuthor = Nothing
, metaSource = Nothing
, metaTotalTime = Nothing
, metaPrepTime = Nothing
, metaCookTime = Nothing
, metaDifficulty = Nothing
, metaCuisine = Nothing
, metaDiet = []
, metaTags = []
, metaImage = Nothing
, metaDescription = Nothing
, metaCustom = Map.empty
}