packages feed

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 #-}