web-page-0.1.0: Web/Page/Render.hs
-- |
-- Module: Web.Page.Render
-- Copyright: (c) 2014 Ertugrul Soeylemez
-- License: BSD3
-- Maintainer: Ertugrul Soeylemez <ertesx@gmx.de>
--
-- This module provides the functionality to render web pages. This is
-- the process to construct and then render a web page:
--
-- 1. Use a writer monad to construct a 'Widget', which represents a
-- web page component and is usually constructed from multiple
-- smaller components,
--
-- 2. use one of the rendering functions to render a widget to a
-- 'Page', which represents a rendered web page, but still abstracts
-- over the exact set of documents (everything inline or a set of
-- separate documents for markup, script and style),
--
-- 3. turn the page into a set of documents that you can deliver to the
-- client, for example by using 'inlinePage'.
--
-- The motivation for rendering to separate documents is that most web
-- pages consist of dynamic markup, but the stylesheet and script is
-- mostly static. The way 'Page' works you can have widgets with
-- batteries included and still render to separate documents to utilise
-- the client's cache better.
--
-- Helper functions for doing that are defined in this module, but
-- framework-specific support is necessary to make this work.
module Web.Page.Render
( -- * Rendering pages
Page(..),
renderWidget,
-- * Single document
inlinePage
)
where
import qualified Clay
import qualified Data.ByteString.Lazy as Bl
import qualified Data.Map.Strict as M
import qualified Data.Text.Lazy as Tl
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
import qualified Text.PrettyPrint.Leijen.Text as Pp
import Data.Foldable (foldMap)
import Data.Monoid
import Data.Text (Text)
import Language.Javascript.JMacro (renderJs)
import Text.Blaze.Html
import Text.Blaze.Html.Renderer.Utf8
import Web.Page.Widget
-- | Rendered pages. This type supports rendering to multiple documents
-- like an HTML document, as explained above.
--
-- If you're running a low-traffic site and don't want to afford the
-- complexity, then you can just include the stylesheets and scripts
-- inline by using the 'inlinePage' function. Except for external
-- script and style URLs this will give you a self-contained document
-- that you can deliver to the client.
--
-- The 'pageHtml' field is the function that takes the markup for the
-- script and the style respectively and returns a lazy bytestring that
-- you can send as @text/html@ to the client. The other two fields are
-- the rendered script and stylesheet. The markup is UTF-8-encoded,
-- which you should indicate in the @content-type@ header, if you
-- deliver via HTTP.
data Page =
Page {
pageHtml :: Html -> Html -> Bl.ByteString, -- ^ Markup document.
pageScript :: Tl.Text, -- ^ Page script.
pageStyle :: Tl.Text -- ^ Page stylesheet.
}
-- | Render the given page to a single self-contained document,
-- including the script and stylesheet inline.
inlinePage :: Page -> Bl.ByteString
inlinePage p = pageHtml p scInc stInc
where
Page { pageScript = sc,
pageStyle = st
} = p
scInc | Tl.null sc = mempty
| otherwise = H.script (toHtml sc)
stInc | Tl.null st = mempty
| otherwise = H.style (toHtml st)
-- | This is the most general rendering function for widgets.
renderWidget ::
(Ord k)
=> [k] -- ^ Sections to render.
-> ([Text] -> Text) -- ^ Title renderer.
-> Widget k Text -- ^ Widget to render.
-> Page
renderWidget ss renderTitle w =
Page { pageHtml = \scInc stInc -> renderHtml (html scInc stInc),
pageScript = Pp.displayT . Pp.renderOneLine . renderJs . _wScript $ w,
pageStyle = Clay.renderWith Clay.compact [] . _wStyle $ w }
where
html :: Html -> Html -> Html
html scInc stInc =
H.docType <>
H.html (H.head headH <>
H.body bodyH)
where
bodyH =
foldMap section ss <>
foldMap scLink (_wScriptLinks w) <>
scInc
headH =
H.title (toHtml title) <>
_wHead w <>
foldMap stLink (_wStyleLinks w) <>
stInc
section =
maybe mempty id .
(`M.lookup` _wSections w)
scLink url = H.script mempty ! A.src (toValue url)
stLink url =
H.link ! A.rel "stylesheet"
! A.href (toValue url)
title = renderTitle . maybe [] id . getLast . _wTitle $ w