Extract Cooklang types into Data.CookLang module
- Create src/Data/CookLang.hs with all recipe types (Recipe, Section, StepItem, Ingredient, Cookware, Timer, RecipeRef, Quantity, Duration, Metadata, emptyMetadata) - Data.CookLang has zero Roux imports — can be extracted to its own package - Roux.Types now just re-exports from Data.CookLang - Update all Roux.* modules and tests to import Data.CookLang directly - All 30 tests passing, hlint clean
This commit is contained in:
@@ -16,6 +16,7 @@ build-type: Simple
|
||||
|
||||
library
|
||||
exposed-modules:
|
||||
Data.CookLang
|
||||
Roux
|
||||
Roux.Html
|
||||
Roux.NYTimes
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
{- | Data types for the Cooklang recipe markup language.
|
||||
|
||||
Models the structure of a parsed @.cook@ recipe file as specified at
|
||||
<https://cooklang.org/docs/spec/>. This module has no dependencies on
|
||||
Roux-specific modules and can be extracted to its own package.
|
||||
|
||||
Hierarchy:
|
||||
|
||||
@
|
||||
Recipe
|
||||
Metadata — YAML front matter (title, tags, servings, …)
|
||||
NonEmpty Section — at least one section per recipe
|
||||
|
||||
Section
|
||||
Maybe Text — optional section name ("Dough", "Filling")
|
||||
NonEmpty SectionBodyItem — interleaved steps, comments, and notes
|
||||
|
||||
SectionBodyItem
|
||||
SecStep Step — a cooking step (paragraph)
|
||||
SecComment Text — full-line @--@ comment
|
||||
SecNote Text — @>@ note / commentary
|
||||
|
||||
Step
|
||||
[StepItem]
|
||||
|
||||
StepItem
|
||||
StepText — plain text
|
||||
StepIngredient — \@name{amount%unit}(preparation)
|
||||
StepCookware — \#name{}
|
||||
StepTimer — \~name{duration%unit}
|
||||
StepRecipeRef — \@.\/path\/to\/recipe{amount}
|
||||
StepEndComment — @-- end of line comment@
|
||||
StepComment — @[- block comment -]@
|
||||
StepBreak — \\ at end of line
|
||||
@
|
||||
-}
|
||||
module Data.CookLang (
|
||||
-- * Recipe
|
||||
Recipe (..),
|
||||
|
||||
-- * Sections and body items
|
||||
Section (..),
|
||||
SectionBodyItem (..),
|
||||
Step (..),
|
||||
|
||||
-- * Inline elements
|
||||
StepItem (..),
|
||||
Ingredient (..),
|
||||
Cookware (..),
|
||||
Timer (..),
|
||||
RecipeRef (..),
|
||||
|
||||
-- * Quantities and durations
|
||||
Quantity (..),
|
||||
Duration (..),
|
||||
|
||||
-- * 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)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Top-level
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- | A fully parsed Cooklang recipe file.
|
||||
data Recipe = Recipe
|
||||
{ recipeMetadata :: !Metadata
|
||||
, recipeSections :: !(NonEmpty Section)
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Sections and body items
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
{- | A named or unnamed section within a recipe. Each section contains
|
||||
interleaved steps, full-line comments, and notes.
|
||||
-}
|
||||
data Section = Section
|
||||
{ sectionName :: !(Maybe Text)
|
||||
, sectionBody :: !(NonEmpty SectionBodyItem)
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- | An item that appears at the top-level of a section (between blank lines).
|
||||
data SectionBodyItem
|
||||
= SecStep !Step
|
||||
| SecComment !Text
|
||||
| SecNote !Text
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A single cooking step — one paragraph of text in the Cooklang file,
|
||||
separated from other steps by a blank line.
|
||||
-}
|
||||
newtype Step = Step
|
||||
{ unStep :: [StepItem]
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Inline elements
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- | An element that appears within a step.
|
||||
data StepItem
|
||||
= StepText !Text
|
||||
| StepIngredient !Ingredient
|
||||
| StepCookware !Cookware
|
||||
| StepTimer !Timer
|
||||
| StepRecipeRef !RecipeRef
|
||||
| StepEndComment !Text
|
||||
| StepComment !Text
|
||||
| StepBreak
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | An ingredient reference.
|
||||
|
||||
@\@salt\@ground black pepper{}\@potato{2}\@bacon strips{1%kg}@
|
||||
-}
|
||||
data Ingredient = Ingredient
|
||||
{ ingName :: !Text
|
||||
, ingQuantity :: !(Maybe Quantity)
|
||||
, ingPreparation :: !(Maybe Text)
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A piece of cookware.
|
||||
|
||||
@\#pot\#potato masher{}@
|
||||
-}
|
||||
newtype Cookware = Cookware
|
||||
{ cwName :: Text
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A timer.
|
||||
|
||||
@\{25%minutes\}\~eggs{3%minutes}@
|
||||
-}
|
||||
data Timer = Timer
|
||||
{ timerName :: !(Maybe Text)
|
||||
, timerDuration :: !Duration
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A reference to another recipe, signalled by a path starting with @.\/@.
|
||||
|
||||
@\@.\/sauces\/Hollandaise{150%g}@
|
||||
-}
|
||||
data RecipeRef = RecipeRef
|
||||
{ refPath :: !FilePath
|
||||
, refQuantity :: !(Maybe Quantity)
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Quantities and durations
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
{- | An ingredient quantity with optional unit and scaling lock.
|
||||
|
||||
@{2}\{1%kg}\{1\/2%tbsp}\{=1%tsp}@
|
||||
-}
|
||||
data Quantity = Quantity
|
||||
{ quantityAmount :: !Rational
|
||||
-- ^ e.g. @1\/2@, @3@, @3\/4@
|
||||
, quantityUnit :: !(Maybe Text)
|
||||
-- ^ e.g. @\"kg\"@, @\"tbsp\"@
|
||||
, quantityFixed :: !Bool
|
||||
-- ^ @{=1%tsp}@ — locked from scaling
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A timer duration — structurally similar to 'Quantity' but semantically
|
||||
distinct. Timers are not scaled with serving size.
|
||||
-}
|
||||
data Duration = Duration
|
||||
{ durationAmount :: !Rational
|
||||
-- ^ e.g. @25@, @3@
|
||||
, durationUnit :: !(Maybe Text)
|
||||
-- ^ e.g. @\"minutes\"@, @\"hours\"@
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Metadata
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
{- | Metadata parsed from YAML front matter (the @---@ … @---@ block at the
|
||||
top of a @.cook@ file).
|
||||
|
||||
Canonical metadata keys are lifted to dedicated fields; everything else
|
||||
lives in 'metaCustom'.
|
||||
-}
|
||||
data Metadata = Metadata
|
||||
{ metaTitle :: !(Maybe Text)
|
||||
, metaServings :: !(Maybe (Int, Maybe Text))
|
||||
, metaAuthor :: !(Maybe Text)
|
||||
, metaSource :: !(Maybe Text)
|
||||
, metaTotalTime :: !(Maybe Duration)
|
||||
, metaPrepTime :: !(Maybe Duration)
|
||||
, metaCookTime :: !(Maybe Duration)
|
||||
, metaDifficulty :: !(Maybe Text)
|
||||
, metaCuisine :: !(Maybe Text)
|
||||
, metaCourse :: !(Maybe Text)
|
||||
, metaDiet :: ![Text]
|
||||
, metaTags :: ![Text]
|
||||
, metaImage :: !(Maybe Text)
|
||||
, metaDescription :: !(Maybe Text)
|
||||
, 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
|
||||
, metaCourse = Nothing
|
||||
, metaDiet = []
|
||||
, metaTags = []
|
||||
, metaImage = Nothing
|
||||
, metaDescription = Nothing
|
||||
, metaCustom = Map.empty
|
||||
}
|
||||
+1
-1
@@ -5,4 +5,4 @@ module Roux (
|
||||
) where
|
||||
|
||||
import Roux.Server (app)
|
||||
import Roux.Types as X
|
||||
import Data.CookLang as X
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ import Text.Blaze.Html5 as H
|
||||
import Text.Blaze.Html5.Attributes as A
|
||||
|
||||
import qualified Roux.RecipeIndex as Idx
|
||||
import Roux.Types
|
||||
import Data.CookLang
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Sort mode for the index page
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ import qualified Data.Text as T
|
||||
import qualified Text.Parsec as P
|
||||
import Text.Parsec.Text (Parser)
|
||||
|
||||
import Roux.Types
|
||||
import Data.CookLang
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Internal raw-block types (parser only)
|
||||
|
||||
@@ -10,7 +10,7 @@ import System.Directory (listDirectory)
|
||||
import System.FilePath (takeBaseName, takeExtension, (</>))
|
||||
|
||||
import Roux.Parser (parseCookFile)
|
||||
import Roux.Types
|
||||
import Data.CookLang
|
||||
|
||||
-- | Metadata about a single recipe discovered on disk.
|
||||
data RecipeInfo = RecipeInfo
|
||||
|
||||
+3
-234
@@ -1,236 +1,5 @@
|
||||
{- | Core types for Roux. Models the structure of a parsed Cooklang (.cook)
|
||||
recipe file as specified at <https://cooklang.org/docs/spec/>.
|
||||
|
||||
Hierarchy:
|
||||
|
||||
@
|
||||
Recipe
|
||||
Metadata — YAML front matter (title, tags, servings, …)
|
||||
NonEmpty Section — at least one section per recipe
|
||||
|
||||
Section
|
||||
Maybe Text — optional section name ("Dough", "Filling")
|
||||
NonEmpty SectionBodyItem — interleaved steps, comments, and notes
|
||||
|
||||
SectionBodyItem
|
||||
SecStep Step — a cooking step (paragraph)
|
||||
SecComment Text — full-line @--@ comment
|
||||
SecNote Text — @>@ note / commentary
|
||||
|
||||
Step
|
||||
[StepItem]
|
||||
|
||||
StepItem
|
||||
StepText — plain text
|
||||
StepIngredient — \@name{amount%unit}(preparation)
|
||||
StepCookware — \#name{}
|
||||
StepTimer — \~name{duration%unit}
|
||||
StepRecipeRef — \@.\/path\/to\/recipe{amount}
|
||||
StepEndComment — @-- end of line comment@
|
||||
StepComment — @[- block comment -]@
|
||||
StepBreak — \\ at end of line
|
||||
@
|
||||
{- | Re-exports all types from 'Data.CookLang' for backward compatibility.
|
||||
-}
|
||||
module Roux.Types (
|
||||
-- * Recipe
|
||||
Recipe (..),
|
||||
module Roux.Types (module X) where
|
||||
|
||||
-- * Sections and body items
|
||||
Section (..),
|
||||
SectionBodyItem (..),
|
||||
Step (..),
|
||||
|
||||
-- * Inline elements
|
||||
StepItem (..),
|
||||
Ingredient (..),
|
||||
Cookware (..),
|
||||
Timer (..),
|
||||
RecipeRef (..),
|
||||
|
||||
-- * Quantities and durations
|
||||
Quantity (..),
|
||||
Duration (..),
|
||||
|
||||
-- * 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)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Top-level
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- | A fully parsed Cooklang recipe file.
|
||||
data Recipe = Recipe
|
||||
{ recipeMetadata :: !Metadata
|
||||
, recipeSections :: !(NonEmpty Section)
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Sections and body items
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
{- | A named or unnamed section within a recipe. Each section contains
|
||||
interleaved steps, full-line comments, and notes.
|
||||
-}
|
||||
data Section = Section
|
||||
{ sectionName :: !(Maybe Text)
|
||||
, sectionBody :: !(NonEmpty SectionBodyItem)
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- | An item that appears at the top-level of a section (between blank lines).
|
||||
data SectionBodyItem
|
||||
= SecStep !Step
|
||||
| SecComment !Text
|
||||
| SecNote !Text
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A single cooking step — one paragraph of text in the Cooklang file,
|
||||
separated from other steps by a blank line.
|
||||
-}
|
||||
newtype Step = Step
|
||||
{ unStep :: [StepItem]
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Inline elements
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- | An element that appears within a step.
|
||||
data StepItem
|
||||
= StepText !Text
|
||||
| StepIngredient !Ingredient
|
||||
| StepCookware !Cookware
|
||||
| StepTimer !Timer
|
||||
| StepRecipeRef !RecipeRef
|
||||
| StepEndComment !Text
|
||||
| StepComment !Text
|
||||
| StepBreak
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | An ingredient reference.
|
||||
|
||||
@\@salt\@ground black pepper{}\@potato{2}\@bacon strips{1%kg}@
|
||||
-}
|
||||
data Ingredient = Ingredient
|
||||
{ ingName :: !Text
|
||||
, ingQuantity :: !(Maybe Quantity)
|
||||
, ingPreparation :: !(Maybe Text)
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A piece of cookware.
|
||||
|
||||
@\#pot\#potato masher{}@
|
||||
-}
|
||||
newtype Cookware = Cookware
|
||||
{ cwName :: Text
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A timer.
|
||||
|
||||
@\{25%minutes\}\~eggs{3%minutes}@
|
||||
-}
|
||||
data Timer = Timer
|
||||
{ timerName :: !(Maybe Text)
|
||||
, timerDuration :: !Duration
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A reference to another recipe, signalled by a path starting with @.\/@.
|
||||
|
||||
@\@.\/sauces\/Hollandaise{150%g}@
|
||||
-}
|
||||
data RecipeRef = RecipeRef
|
||||
{ refPath :: !FilePath
|
||||
, refQuantity :: !(Maybe Quantity)
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Quantities and durations
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
{- | An ingredient quantity with optional unit and scaling lock.
|
||||
|
||||
@{2}\{1%kg}\{1\/2%tbsp}\{=1%tsp}@
|
||||
-}
|
||||
data Quantity = Quantity
|
||||
{ quantityAmount :: !Rational
|
||||
-- ^ e.g. @1\/2@, @3@, @3\/4@
|
||||
, quantityUnit :: !(Maybe Text)
|
||||
-- ^ e.g. @\"kg\"@, @\"tbsp\"@
|
||||
, quantityFixed :: !Bool
|
||||
-- ^ @{=1%tsp}@ — locked from scaling
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
{- | A timer duration — structurally similar to 'Quantity' but semantically
|
||||
distinct. Timers are not scaled with serving size.
|
||||
-}
|
||||
data Duration = Duration
|
||||
{ durationAmount :: !Rational
|
||||
-- ^ e.g. @25@, @3@
|
||||
, durationUnit :: !(Maybe Text)
|
||||
-- ^ e.g. @\"minutes\"@, @\"hours\"@
|
||||
}
|
||||
deriving stock (Eq, Show)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Metadata
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
{- | Metadata parsed from YAML front matter (the @---@ … @---@ block at the
|
||||
top of a @.cook@ file).
|
||||
|
||||
Canonical metadata keys are lifted to dedicated fields; everything else
|
||||
lives in 'metaCustom'.
|
||||
-}
|
||||
data Metadata = Metadata
|
||||
{ metaTitle :: !(Maybe Text)
|
||||
, metaServings :: !(Maybe (Int, Maybe Text))
|
||||
, metaAuthor :: !(Maybe Text)
|
||||
, metaSource :: !(Maybe Text)
|
||||
, metaTotalTime :: !(Maybe Duration)
|
||||
, metaPrepTime :: !(Maybe Duration)
|
||||
, metaCookTime :: !(Maybe Duration)
|
||||
, metaDifficulty :: !(Maybe Text)
|
||||
, metaCuisine :: !(Maybe Text)
|
||||
, metaCourse :: !(Maybe Text)
|
||||
, metaDiet :: ![Text]
|
||||
, metaTags :: ![Text]
|
||||
, metaImage :: !(Maybe Text)
|
||||
, metaDescription :: !(Maybe Text)
|
||||
, 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
|
||||
, metaCourse = Nothing
|
||||
, metaDiet = []
|
||||
, metaTags = []
|
||||
, metaImage = Nothing
|
||||
, metaDescription = Nothing
|
||||
, metaCustom = Map.empty
|
||||
}
|
||||
import Data.CookLang as X
|
||||
|
||||
@@ -7,7 +7,7 @@ import qualified Data.Text.IO as TIO
|
||||
import Test.Hspec (Spec, describe, expectationFailure, it, shouldBe)
|
||||
|
||||
import Roux.Parser (parseCookFile)
|
||||
import Roux.Types
|
||||
import Data.CookLang
|
||||
|
||||
spec :: Spec
|
||||
spec = do
|
||||
|
||||
Reference in New Issue
Block a user