feat: parse schema.org ingredient strings into structured Cooklang ingredients
Build and Deploy / build-and-deploy (push) Successful in 1m45s
Build and Deploy / build-and-deploy (push) Successful in 1m45s
Replaces buildIngredientSection's plain-text dump with proper
StepIngredient annotation. Parses ingredient strings like
"3/4 cup extra-virgin olive oil" into:
- quantity: 3/4
- unit: "cup"
- name: "extra-virgin olive oil"
Handles fractions ("3/4"), compound quantities ("1,1/2"),
unit recognition (cup, ounce, pound, etc.), and strips
preparation notes after the first comma.
This commit is contained in:
+135
-3
@@ -295,13 +295,145 @@ buildSections r =
|
|||||||
(s : ss) -> s :| ss
|
(s : ss) -> s :| ss
|
||||||
[] -> Section Nothing (SecStep (Step []) :| []) :| []
|
[] -> Section Nothing (SecStep (Step []) :| []) :| []
|
||||||
|
|
||||||
|
-- | Known units of measure for ingredient parsing.
|
||||||
|
ingredientUnits :: [Text]
|
||||||
|
ingredientUnits =
|
||||||
|
[ "cup"
|
||||||
|
, "cups"
|
||||||
|
, "tablespoon"
|
||||||
|
, "tablespoons"
|
||||||
|
, "tbsp"
|
||||||
|
, "teaspoon"
|
||||||
|
, "teaspoons"
|
||||||
|
, "tsp"
|
||||||
|
, "ounce"
|
||||||
|
, "ounces"
|
||||||
|
, "oz"
|
||||||
|
, "pound"
|
||||||
|
, "pounds"
|
||||||
|
, "lb"
|
||||||
|
, "lbs"
|
||||||
|
, "gram"
|
||||||
|
, "grams"
|
||||||
|
, "g"
|
||||||
|
, "kilogram"
|
||||||
|
, "kilograms"
|
||||||
|
, "kg"
|
||||||
|
, "milliliter"
|
||||||
|
, "milliliters"
|
||||||
|
, "ml"
|
||||||
|
, "liter"
|
||||||
|
, "liters"
|
||||||
|
, "l"
|
||||||
|
, "pinch"
|
||||||
|
, "pinches"
|
||||||
|
, "clove"
|
||||||
|
, "cloves"
|
||||||
|
, "slice"
|
||||||
|
, "slices"
|
||||||
|
, "piece"
|
||||||
|
, "pieces"
|
||||||
|
, "can"
|
||||||
|
, "cans"
|
||||||
|
, "package"
|
||||||
|
, "packages"
|
||||||
|
, "bunch"
|
||||||
|
, "bunches"
|
||||||
|
, "sprig"
|
||||||
|
, "sprigs"
|
||||||
|
, "leaf"
|
||||||
|
, "leaves"
|
||||||
|
, "item"
|
||||||
|
, "items"
|
||||||
|
, "inch"
|
||||||
|
, "inches"
|
||||||
|
]
|
||||||
|
|
||||||
|
{- | Parse a schema.org ingredient string into a StepIngredient.
|
||||||
|
Handles formats like:
|
||||||
|
"3/4 cup extra-virgin olive oil"
|
||||||
|
"2 eggs"
|
||||||
|
"Salt and black pepper"
|
||||||
|
"1 large lemon"
|
||||||
|
"1 pound cherry tomatoes, halved"
|
||||||
|
-}
|
||||||
|
parseSchemaOrgIngredient :: Text -> StepItem
|
||||||
|
parseSchemaOrgIngredient t =
|
||||||
|
let cleaned = T.strip t
|
||||||
|
-- Strip preparation notes after the first comma
|
||||||
|
mainPart = fst (T.breakOn "," cleaned)
|
||||||
|
toks = T.words mainPart
|
||||||
|
(parsedQty, remainingWords) = extractQuantity toks
|
||||||
|
name = T.strip $ T.intercalate " " remainingWords
|
||||||
|
in StepIngredient (Ingredient name parsedQty Nothing)
|
||||||
|
where
|
||||||
|
-- Extract quantity and optional unit from the front of the word list.
|
||||||
|
extractQuantity :: [Text] -> (Maybe Quantity, [Text])
|
||||||
|
extractQuantity (w : ws) =
|
||||||
|
case parseHumanRational w of
|
||||||
|
Just amt ->
|
||||||
|
-- Try to consume a unit word next
|
||||||
|
case ws of
|
||||||
|
(u : rest)
|
||||||
|
| T.toLower u `elem` ingredientUnits ->
|
||||||
|
(Just (Quantity amt (Just u) False), rest)
|
||||||
|
_ ->
|
||||||
|
(Just (Quantity amt Nothing False), ws)
|
||||||
|
Nothing -> (Nothing, w : ws) -- didn't start with a number
|
||||||
|
extractQuantity [] = (Nothing, [])
|
||||||
|
|
||||||
|
{- | Parse a human-readable quantity (number or fraction) from text.
|
||||||
|
Handles "3/4", "2", "1,1/2", "0.5".
|
||||||
|
-}
|
||||||
|
parseHumanRational :: Text -> Maybe Rational
|
||||||
|
parseHumanRational t =
|
||||||
|
case T.splitOn "," cleaned of
|
||||||
|
-- Compound: "1,1/2" → 1 + 1/2 = 3/2
|
||||||
|
[a, b] -> (+) <$> parseSimpleRational a <*> parseSimpleRational b
|
||||||
|
-- Simple: "1/2" or "3"
|
||||||
|
[a] -> parseSimpleRational a
|
||||||
|
_ -> Nothing
|
||||||
|
where
|
||||||
|
cleaned = T.strip t
|
||||||
|
|
||||||
|
parseSimpleRational :: Text -> Maybe Rational
|
||||||
|
parseSimpleRational t =
|
||||||
|
case T.splitOn "/" t of
|
||||||
|
-- Fraction: "1/2"
|
||||||
|
[num, den] ->
|
||||||
|
case (readInt (num), readInt (den)) of
|
||||||
|
(Just n, Just d) | d /= 0 -> Just (toRational n / toRational d)
|
||||||
|
_ -> Nothing
|
||||||
|
-- Whole number or decimal: "3", "0.5", or range like "14-18"
|
||||||
|
[a] ->
|
||||||
|
case T.splitOn "-" a of
|
||||||
|
[first, _rest] -> parseNumeric (T.strip first)
|
||||||
|
_ -> parseNumeric a
|
||||||
|
_ -> Nothing
|
||||||
|
where
|
||||||
|
readInt :: Text -> Maybe Int
|
||||||
|
readInt x = case reads (T.unpack x) of [(n, "")] -> Just n; _ -> Nothing
|
||||||
|
|
||||||
|
parseNumeric :: Text -> Maybe Rational
|
||||||
|
parseNumeric t =
|
||||||
|
case reads (T.unpack t) of
|
||||||
|
[(n, "")] -> Just (toRational (n :: Int))
|
||||||
|
_ -> case reads (T.unpack t) of
|
||||||
|
[(d, "")] -> Just (toRational (d :: Double))
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
buildIngredientSection :: [Text] -> Section
|
buildIngredientSection :: [Text] -> Section
|
||||||
buildIngredientSection ings
|
buildIngredientSection ings
|
||||||
| null ings = Section Nothing (SecStep (Step []) :| [])
|
| null ings = Section Nothing (SecStep (Step []) :| [])
|
||||||
| otherwise =
|
| otherwise =
|
||||||
let items = concatMap (\t -> [StepText ", ", StepText t]) ings
|
let
|
||||||
step = Step items
|
-- Parse each ingredient string into a StepIngredient
|
||||||
in Section (Just "Ingredients") (SecStep step :| [])
|
parsed = map parseSchemaOrgIngredient ings
|
||||||
|
-- Interleave with comma separators for nice formatting
|
||||||
|
items = concatMap (\i -> [StepText ", ", i]) parsed
|
||||||
|
step = Step (drop 2 items) -- drop leading ", "
|
||||||
|
in
|
||||||
|
Section (Just "Ingredients") (SecStep step :| [])
|
||||||
|
|
||||||
buildMethodSection :: [SchemaOrgHowToStep] -> Maybe Section
|
buildMethodSection :: [SchemaOrgHowToStep] -> Maybe Section
|
||||||
buildMethodSection [] = Nothing
|
buildMethodSection [] = Nothing
|
||||||
|
|||||||
Reference in New Issue
Block a user