diff --git a/src/Roux/SchemaOrg.hs b/src/Roux/SchemaOrg.hs index b7a3cb9..a823928 100644 --- a/src/Roux/SchemaOrg.hs +++ b/src/Roux/SchemaOrg.hs @@ -295,13 +295,145 @@ buildSections r = (s : ss) -> s :| ss [] -> 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 ings | null ings = Section Nothing (SecStep (Step []) :| []) | otherwise = - let items = concatMap (\t -> [StepText ", ", StepText t]) ings - step = Step items - in Section (Just "Ingredients") (SecStep step :| []) + let + -- Parse each ingredient string into a StepIngredient + 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 [] = Nothing