{-| Module : Text.DocL Copyright : 2015 Anders Jellinggaard License : MIT Maintainer : anders.jel@gmail.com Stability : experimental Haskell mini-language to create HTML documents with a mixture of markup and plots based on the library. See the for an introduction. Notes: * The generated documents relies on to serve and friends. * This library should not be used on a public facing server with input supplied by the user. The library has not been properly vetted for security issues. -} module Text.DocL ( Doc, -- * Markup text, header, markdown, html, -- * Output render, renderToFile, -- * Plotting Column, col, col', plot, plot', rawPlot, -- * Utilities linspace, -- * Re-exports from "Text.DocL.Javascript" (/:), javascript ) where import Data.Foldable import Data.HashMap.Strict (insert, unionWith) import Data.Monoid import Data.Sequence (Seq, singleton) import Data.String (IsString (..)) import Text.Blaze.Html.Renderer.Utf8 (renderHtml) import Text.Blaze.Html5 ((!)) import Text.DocL.Javascript import qualified Data.ByteString.Lazy as B import qualified System.IO as IO import qualified Text.Blaze.Html5 as H import qualified Text.Blaze.Html5.Attributes as A import qualified Text.Markdown as Markdown data Node = Chart [Prop] | Html H.Html -- | Base type representing a document. This type allows composition via the -- 'Monoid' instance. newtype Doc = Doc (Seq Node) deriving Monoid -- | Represent a column of a dataset where each row has type /a/. See 'col' for -- details. data Column a = Column { _header :: String , _extract :: a -> Obj } -- | Polymorphic version of 'col'. This allows, for instance, 'Int' and 'String' -- values to be used in plots. It is up to the caller to ensure that the values -- make sense to . col' :: ToObj b => String -> (a -> b) -> Column a col' h f = Column h (toObj . f) -- | Data sets are supplied to 'plot' as a collection of elements of type /a/. -- This function sets up a 'Column', which knows how to extract one 'Double' -- value from each value of type /a/ in the collection. -- -- The first argument to 'col' is the header of the column, which is used in the -- legend of the plot. -- -- The simplest plot that can be created consist of one column for the /x/ -- values and one column for the /y/ values. -- -- @ -- -- Plot x² vs x. -- plot [1..10] (col "x" id) [col "x²" $ \\x -> x*x] -- @ col :: String -> (a -> Double) -> Column a col = col' -- | > plot ds x ys -- -- Plots the data in /ds/ using the column /x/ for the values on the /x/-axis -- and with one line on the plot for each column in /ys/. See also 'col'. plot :: Foldable f => f a -> Column a -> [Column a] -> Doc plot d x ys = plot' d x ys [] -- | Same as 'plot', but takes a final argument which is merged with the -- configuration object supplied to . This allows the -- caller to customize the plot. -- -- @ -- -- Plot x² vs x with the points hidden. -- plot' [1..10] (col "x" id) [col "x²" $ \\x -> x*x] $ -- ["point" //: ["show" //: False]] -- @ -- -- See for the many properties that can be used -- here. plot' :: Foldable f => f a -> Column a -> [Column a] -> [Prop] -> Doc plot' d x ys options = rawPlot $ obj ++ options where obj = [ "data" /: [ "rows" /: (toObj (_header x : map _header ys) : map f (toList d)) , "x" /: _header x ] ] f p = toObj $ map (`_extract` p) (x:ys) -- | This function sends an object with the supplied properties directly to -- . This is the do-it-yourself option which exposes all -- of . The object receives a -- property, -- targeting a @\@ tag placed appropriately. rawPlot :: [Prop] -> Doc rawPlot = Doc . singleton . Chart -- | Creates a 'Doc' representing raw html. html :: H.Html -> Doc html = Doc . singleton . Html -- | Creates a 'Doc' representing plain text. The string will be properly -- escaped. text :: String -> Doc text = html . H.p . H.toMarkup -- | Creates a 'Doc' representing @\@ header tag. The string will be -- properly escaped. header :: String -> Doc header = html . H.h1 . H.toMarkup -- | Converts a string in -- to a 'Doc'. markdown :: String -> Doc markdown = html . Markdown.markdown Markdown.def . fromString -- | Render a document to a lazy 'B.ByteString'. render :: Doc -> B.ByteString render (Doc doc) = renderHtml html where dns = "https://cdnjs.cloudflare.com/ajax/libs/" html = H.docTypeHtml $ do H.head $ do H.link ! A.rel "stylesheet" ! A.href (dns <> "c3/0.4.9/c3.min.css") H.script ! A.charset "utf-8" ! A.src (dns <> "d3/3.5.3/d3.min.js") $ mempty H.script ! A.charset "utf-8" ! A.src (dns <> "c3/0.4.9/c3.min.js") $ mempty H.body $ foldMap f (zip [(0::Int)..] $ toList doc) f (_, (Html t)) = t f (i, (Chart props)) = divTag <> scriptTag where name = "plot" <> show i divTag = H.div ! A.id (fromString name) $ mempty obj = props ++ ["bindto" /: ("#" ++ name)] js = encode $ toObj obj scriptTag = H.script ! A.type_ "text/javascript" $ H.preEscapedToMarkup $ "\nc3.generate(" <> js <> ");\n" -- | If the file exists, it will be overwritten. renderToFile :: FilePath -> Doc -> IO () renderToFile p = B.writeFile p . render -- | /'linspace' x y n/ generates /n/ evenly spaced values from /x/ to /y/. linspace :: Fractional a => a -> a -> Int -> [a] linspace x y n = [x + (fromIntegral i)*d | i <- [0 .. n - 1]] where d = (y - x) / (fromIntegral n - 1)