css-simple-0.1.0.0: src/Css/Internal.hs
{-|
Module : Css.Internal
Description : Semi-public functions for the css-simple library
Copyright : (c) Alexey Seledkov, 2022
License : GPL-3
Maintainer : qyutou@gmail.com
Stability : experimental
Portability : portable
-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
module Css.Internal
( -- * Types
Rule(..)
, Css(..)
-- * Rendering
, Config(..)
, pretty
, compact
, renderRule
, renderRuleWith
, renderRuleWith'
, renderCss
, renderCssWith
, putCss
, putCssWith
, renderCssToFile
, renderCssToFileWith
-- * eDSL
, (?)
, rule
, (|>)
, declaration
-- * Utility
, getRules
) where
import Control.Monad.Writer (MonadWriter, Writer, listen, pass,
runWriter, tell)
import Data.String (IsString (fromString))
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Text.Builder (Builder, run, text)
-- * Types
-- | AST-like CSS Rule representation
data Rule
-- | Rule: selector, inner rules/declarations, next
= Rule !Text !Rule !Rule
-- | Declaration: property, value, next
| Declaration !Text !Text !Rule
-- | Leaf: text
-- This is used to allow creating the declarations without using of
-- functions
| Leaf !Text
-- | Empty
| Empty
-- | Semigroup instance for CSS Rule
instance Semigroup Rule where
Rule selector inner next <> a =
Rule selector inner (next <> a)
Declaration property value next <> a =
Declaration property value (next <> a)
_ <> a = a
-- | Monoid instance for CSS Rule
instance Monoid Rule where
mempty = Empty
-- | Show instance for the CSS Rule
instance Show Rule where
show = T.unpack . renderRule
-- | Css monad - newtype wrapper around Control.Monad.Writer.Writer monad
newtype Css a = Css
{ runCss :: Writer Rule a }
deriving (Functor, Applicative, Monad)
-- | MonadWriter instance for the Css monad
instance MonadWriter Rule Css where
tell = Css . tell
listen = Css . listen . runCss
pass = Css . pass . runCss
-- | Semigroup instance for the Css monad
instance Semigroup (Css ()) where
css <> a = tell $ getRules css <> getRules a
-- | Monoid instance for the Css monad
instance Monoid (Css ()) where
mempty = tell Empty
-- | IsString instance used to allow the creation of declarations
-- NOTE: This is only for creating the Declarations, it doesn't do anything
-- else.
instance IsString (Css ()) where
fromString :: String -> Css ()
fromString = tell . Leaf . T.pack
{-# INLINE fromString #-}
-- | IsString instance used to create the rules
instance (a ~ Css (), b ~ Css ()) => IsString (a -> b) where
fromString :: String -> Css () -> Css ()
fromString strSelector css = case getRules css of
Leaf txt -> declaration (T.pack strSelector) txt
inner -> rule (T.pack strSelector) (tell inner)
{-# INLINE fromString #-}
-- | Show instance for the Css monad
instance Show (Css ()) where
show = T.unpack . renderCss
-- * eDSL
-- | Create new rule
--
-- ==== __Examples__
--
-- >>> rule "body" (background "#000000") <> rule ".wrapper" (width "90vw" <> maxWidth "72rem")
-- body {
-- background: #000000;
-- }
-- .wrapper {
-- width: 90vw;
-- max-width: 72rem;
-- }
rule
:: Text -- ^ Selector
-> Css () -- ^ Inner Css
-> Css () -- ^ Return in Css Monad
rule selector inner =
tell $ Rule selector (getRules inner) mempty
{-# INLINE rule #-}
-- | Infix version of 'rule'
(?)
:: Text -- ^ Selector
-> Css () -- ^ Inner Css
-> Css () -- ^ Return in Css Monad
(?) = rule
{-# INLINE (?) #-}
-- | Create new declaration
--
-- ==== __Examples__
--
-- >>> declaration "width" "90vw"
-- width: 90vw;
declaration
:: Text -- ^ Property
-> Text -- ^ Value
-> Css () -- ^ Return in Css Monad
declaration property value =
tell $ Declaration property value mempty
{-# INLINE declaration #-}
-- | Infix version of 'declaration'
(|>)
:: Text -- ^ Property
-> Text -- ^ Value
-> Css () -- ^ Return in Css Monad
(|>) = declaration
{-# INLINE (|>) #-}
-- * Rendering
-- | Rendering configuration
data Config = Config
{ _newline :: !Text -- ^ Newline symbol
, _indent :: !Text -- ^ Indentation
, _spacing :: !Text -- ^ Small spacing
}
-- | Pretty render configuration
pretty :: Config
pretty = Config "\n" " " " "
-- | Compact render configuration
compact :: Config
compact = Config "" "" ""
-- | Used to render the Css Rules with pretty config
renderRule
:: Rule -- ^ Rule to render
-> Text -- ^ Return as a Text
renderRule = (`renderRuleWith` pretty)
-- | Render the Css Rules with certain configuration
renderRuleWith
:: Rule -- ^ Rule to render
-> Config -- ^ Rendering configuration
-> Text -- ^ Return as a Text
renderRuleWith rule config =
run $ renderRuleWith' rule config ""
-- | Helper function for render rules
-- Actually this is the main rendering functions, while the rest of the
-- functions are something like front-end
renderRuleWith'
:: Rule -- ^ Rule to render
-> Config -- ^ Rendering configuration
-> Builder -- ^ Prefix
-> Builder -- ^ Return as a Builder
renderRuleWith' (Rule selector inner Empty) config prefix =
prefix <> text selector <> (text . _spacing) config <> "{"
<> (text . _newline) config
<> renderRuleWith' inner config (prefix <> (text . _indent) config)
<> (text . _newline) config <> prefix <> "}"
renderRuleWith' (Rule selector inner next) config prefix =
prefix <> text selector <> (text . _spacing) config <> "{"
<> (text . _newline) config
<> renderRuleWith' inner config (prefix <> (text . _indent) config)
<> (text . _newline) config <> prefix <> "}" <> (text . _newline) config
<> renderRuleWith' next config prefix
renderRuleWith' (Declaration property value Empty) config prefix =
prefix <> text property <> ":" <> (text . _spacing) config <> text value
renderRuleWith' (Declaration property value next) config prefix =
prefix <> text property <> ":" <> (text . _spacing) config <> text value
<> ";" <> (text . _newline) config
<> renderRuleWith' next config prefix
renderRuleWith' _ _ _ = ""
-- | Render the Css Monad with pretty config as Text
renderCss
:: Css () -- ^ Css to render
-> Text -- ^ Return as a Text
renderCss = renderRule . getRules
-- | Render the Css Monad and print it to stdout
putCss
:: Css () -- ^ Css to render
-> IO () -- ^ Print to stdoup
putCss = TIO.putStrLn . renderRule . getRules
-- | Render the Css Monad and save it to the filePath
renderCssToFile
:: Css () -- ^ Css to render
-> FilePath -- ^ Path/to/file
-> IO () -- ^ Save the rendered Css to the file path
renderCssToFile css filePath =
TIO.writeFile filePath . renderRule . getRules $ css
-- | Render the Css Monad with certain config as Text
renderCssWith
:: Css () -- ^ Css to render
-> Config -- ^ Configuration
-> Text -- ^ Return as a Text
renderCssWith = renderRuleWith . getRules
-- | Render the CSS with certain configuration and print it to stdout
putCssWith
:: Css () -- ^ Css to render
-> Config -- ^ Configuration
-> IO () -- ^ Print to stdout
putCssWith css config = TIO.putStrLn (renderRuleWith (getRules css) config)
-- | Render the Css Monad with certain config and save it to the filepath
renderCssToFileWith
:: Css () -- ^ Css to render
-> Config -- ^ Configuration
-> FilePath -- ^ Path/to/file
-> IO () -- ^ Save the Css to the file
renderCssToFileWith css config filePath =
TIO.writeFile filePath (renderRuleWith (getRules css) config)
-- * Utility
-- | Utility function to extract the Css Rule from Css Monad
getRules :: Css () -> Rule
getRules = snd . runWriter . runCss
{-# INLINE getRules #-}