Recipe detail page with server-side HTML rendering
- Add Roux.Html.recipePage: full recipe detail page with:
- Back navigation link
- Title and metadata (servings, times, difficulty, etc.)
- Ingredients summary list with quantities
- Sections with named headings
- Steps with inline element rendering:
- Ingredients highlighted in pumpkin (with quantity badge)
- Cookware in violet italic
- Timers in azure with ⏱ icon
- Comments in sand italic
- Recipe refs as links
- Line breaks
- Update Roux.Server: route /recipes/FILENAME to recipe page
- Case-insensitive filename lookup, .cook extension optional
- URL decoding for filenames
- Fix ingredient name parsing: restrict name characters to
alphaNum + spaces/hyphens/apostrophes, preventing greedy
consumption across special chars like ) and #
- Split name chars into pMultiNameChar (with spaces, before braces)
and pSingleNameChar (without spaces, fallback)
- Suppress -Wname-shadowing in Html.hs (blaze-html exports clash
with common variable names)
- All 26 tests passing, server smoke-tested with examples
This commit is contained in:
+17
-5
@@ -128,6 +128,10 @@ pLineBreak = do
|
||||
_ <- P.newline
|
||||
return StepBreak
|
||||
|
||||
-- | Characters allowed in recipe reference paths.
|
||||
pPathChar :: Parser Char
|
||||
pPathChar = P.alphaNum P.<|> P.oneOf "./-_"
|
||||
|
||||
-- | Recipe reference: @./path/to/recipe{quantity}
|
||||
pRecipeRef :: Parser StepItem
|
||||
pRecipeRef = do
|
||||
@@ -135,7 +139,7 @@ pRecipeRef = do
|
||||
-- Consume @ and store ./ as part of the path
|
||||
_ <- P.char '.'
|
||||
_ <- P.char '/'
|
||||
path <- P.many (P.noneOf "{(\n\\")
|
||||
path <- P.many pPathChar
|
||||
mqty <- P.optionMaybe (P.between (P.char '{') (P.char '}') pQuantityBody)
|
||||
return (StepRecipeRef (RecipeRef ("./" <> path) (join mqty)))
|
||||
|
||||
@@ -147,6 +151,14 @@ pIngredient = do
|
||||
prep <- P.optionMaybe (P.try pPreparation)
|
||||
return (StepIngredient (Ingredient (T.pack name) qty (fmap T.strip prep)))
|
||||
|
||||
-- | Characters allowed in names that may include spaces (before braces).
|
||||
pMultiNameChar :: Parser Char
|
||||
pMultiNameChar = P.alphaNum P.<|> P.oneOf " .-'"
|
||||
|
||||
-- | Characters allowed in single-word names (no braces).
|
||||
pSingleNameChar :: Parser Char
|
||||
pSingleNameChar = P.alphaNum P.<|> P.oneOf ".-'"
|
||||
|
||||
{- | Parse a name optionally followed by @{quantity}@ braces.
|
||||
|
||||
Used by ingredients. First tries a multi-word name with braces
|
||||
@@ -156,14 +168,14 @@ pNameWithQuantity :: Parser ([Char], Maybe Quantity)
|
||||
pNameWithQuantity =
|
||||
P.try
|
||||
( do
|
||||
name <- P.many (P.noneOf "{(\n\\")
|
||||
name <- P.many pMultiNameChar
|
||||
_ <- P.char '{'
|
||||
qty <- pQuantityBody
|
||||
_ <- P.char '}'
|
||||
return (name, qty)
|
||||
)
|
||||
P.<|> do
|
||||
name <- P.many1 (P.alphaNum P.<|> P.oneOf ".-'")
|
||||
name <- P.many1 pSingleNameChar
|
||||
return (name, Nothing)
|
||||
|
||||
{- | Parse a name optionally followed by empty @{}@ braces.
|
||||
@@ -174,12 +186,12 @@ pNameWithBraces :: Parser [Char]
|
||||
pNameWithBraces =
|
||||
P.try
|
||||
( do
|
||||
name <- P.many (P.noneOf "{(\n\\")
|
||||
name <- P.many pMultiNameChar
|
||||
_ <- P.char '{'
|
||||
_ <- P.char '}'
|
||||
return name
|
||||
)
|
||||
P.<|> P.many1 (P.alphaNum P.<|> P.oneOf ".-'")
|
||||
P.<|> P.many1 pSingleNameChar
|
||||
|
||||
-- | Parse preparation text inside parentheses: @name(diced)
|
||||
pPreparation :: Parser Text
|
||||
|
||||
Reference in New Issue
Block a user