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:
+28
-3
@@ -1,7 +1,8 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
|
||||
{- | WAI application for the Roux recipe server.
|
||||
|
||||
Handles HTTP requests directly via wai — no framework. Currently serves
|
||||
a single index page listing all discovered recipes.
|
||||
Handles HTTP requests directly via wai — no framework.
|
||||
-}
|
||||
module Roux.Server (
|
||||
app,
|
||||
@@ -9,6 +10,9 @@ module Roux.Server (
|
||||
|
||||
import Control.Monad (when)
|
||||
import Data.ByteString.Lazy (ByteString)
|
||||
import Data.Char (toLower)
|
||||
import Data.List (find)
|
||||
import Data.Text (Text)
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Wai as Wai
|
||||
|
||||
@@ -31,7 +35,6 @@ app recipeDir = do
|
||||
when (errs > 0) $
|
||||
putStrLn $
|
||||
"[roux] " <> show errs <> " had parse errors"
|
||||
-- Return the application; recipes are captured in the closure.
|
||||
pure (handleRequest recipes)
|
||||
|
||||
-- | Route an incoming request to the appropriate handler.
|
||||
@@ -39,8 +42,30 @@ handleRequest :: [Idx.RecipeInfo] -> Wai.Application
|
||||
handleRequest recipes request respond =
|
||||
case Wai.pathInfo request of
|
||||
[] -> respond (htmlResponse (Html.indexPage recipes))
|
||||
["recipes", rawPath] ->
|
||||
case lookupRecipe (urlDecodePath rawPath) recipes of
|
||||
Just info -> respond (htmlResponse (Html.recipePage info))
|
||||
Nothing -> respond notFound
|
||||
_ -> respond notFound
|
||||
|
||||
-- | Look up a recipe by filename (case-insensitive, .cook extension optional).
|
||||
lookupRecipe :: FilePath -> [Idx.RecipeInfo] -> Maybe Idx.RecipeInfo
|
||||
lookupRecipe target recipes =
|
||||
let normalised = map toLower (stripCookExt target)
|
||||
in find (\(Idx.RecipeInfo fp _ _) -> map toLower (stripCookExt fp) == normalised) recipes
|
||||
|
||||
-- | Strip the .cook extension if present.
|
||||
stripCookExt :: FilePath -> FilePath
|
||||
stripCookExt fp
|
||||
| ".cook" `isSuffixOf` fp = take (length fp - 5) fp
|
||||
| otherwise = fp
|
||||
where
|
||||
isSuffixOf suffix s = suffix == drop (length s - length suffix) s
|
||||
|
||||
-- | Decode a percent-encoded URL path segment to a FilePath.
|
||||
urlDecodePath :: Text -> FilePath
|
||||
urlDecodePath = Html.urlDecode
|
||||
|
||||
-- | Build a 200 HTML response.
|
||||
htmlResponse :: ByteString -> Wai.Response
|
||||
htmlResponse body =
|
||||
|
||||
Reference in New Issue
Block a user