Source

hpaco / hpaco-lib / Text / HPaco / Writers / Javascript.hs

{-#LANGUAGE TemplateHaskell #-}
module Text.HPaco.Writers.Javascript
    ( writeJavascript
    , defJsWriterOptions
    , WrapMode (..)
    )
where

import Control.Monad.RWS
import Data.FileEmbed
import Data.List (intersperse)
import Data.Maybe
import Data.Typeable
import Text.HPaco.AST.AST
import Text.HPaco.AST.Expression
import Text.HPaco.AST.Statement
import Text.HPaco.Writer
import Text.HPaco.Writers.Internal.WrapMode
import Text.HPaco.Writers.Internal.CodeWriter
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Map as M

defJsWriterOptions =
    defaultWriterOptions { woWriteFunc = "_write" }

data JavascriptWriterState =
    JavascriptWriterState
        { jwsIndent :: Int
        , jwsAST :: AST
        }

defaultJavascriptWriterState =
    JavascriptWriterState
        { jwsIndent = 0
        , jwsAST = defAST
        }

type PWS = RWS WriterOptions String JavascriptWriterState

instance CodeWriterState JavascriptWriterState where
    cwsGetIndent = jwsIndent
    cwsSetIndent f s = s { jwsIndent = f }
    -- stubbing these out for now...
    cwsGetFilters s = []
    cwsSetFilters f = id

writeJavascript :: WriterOptions -> Writer
writeJavascript opts ast =
    let (s, w) = execRWS (writeAST ast) opts defaultJavascriptWriterState { jwsAST = ast}
    in w

writeAST :: AST -> PWS ()
writeAST ast = do
    writeHeader
    writeDefs $ astDefs ast
    writeStatement $ astRootStatement ast
    writeFooter

writeDefs = mapM_ writeDef

writeDef (identifier, body) = do
    writeIndented $ "var _macro_" ++ identifier ++ " = function() {"
    withIndent $ writeStatement body
    writeIndented "};"

writePreamble :: PWS ()
writePreamble = do
    let src = BS8.unpack $(embedFile "snippets/js/preamble.js")
    write src
    endl

writeHeader :: PWS ()
writeHeader = do
    templateName <- woTemplateName `liftM` ask
    wrapMode <- woWrapMode `liftM` ask

    case wrapMode of
        WrapFunction -> do
            let funcName =
                    if null templateName
                        then "runTemplate"
                        else "runTemplate_" ++ templateName
            writeIndented $ "function " ++ funcName ++ "(context) {"
            pushIndent
        otherwise -> return ()
    writeIndented "(function(){"
    pushIndent
    includePreamble <- asks woIncludePreamble
    when includePreamble writePreamble


writeFooter :: PWS ()
writeFooter = do
    wrapMode <- woWrapMode `liftM` ask

    popIndent
    writeIndented "}).apply(context);"

    case wrapMode of
        WrapFunction -> do
            popIndent
            writeIndented "}"
        otherwise -> return ()

writeStatement :: Statement -> PWS ()
writeStatement stmt =
    case stmt of
        StatementSequence ss -> mapM_ writeStatement ss
        PrintStatement expr -> do
            wfunc <- woWriteFunc `liftM` ask
            writeIndent
            write $ wfunc ++ "(_f("
            writeExpression expr
            write "));"
            endl
        NullStatement -> return ()
        IfStatement { } -> writeIf stmt
        LetStatement identifier expr stmt -> writeLet identifier expr stmt
        ForStatement Nothing identifier expr stmt -> writeFor identifier expr stmt
        ForStatement (Just iter) identifier expr stmt -> writeForExt iter identifier expr stmt
        SwitchStatement masterExpr branches -> writeSwitch masterExpr branches
        CallStatement identifier ->
            writeIndented $ "_macro_" ++ identifier ++ ".apply(this);"
        SourcePositionStatement fn ln -> do
            c <- asks woSourcePositionComments
            when c $ do
                writeIndent
                write "/* "
                write fn
                write ":"
                write $ show ln
                write " */"
                endl

writeIf :: Statement -> PWS ()
writeIf (IfStatement cond true false) = do
    writeIndent
    write "if ("
    writeExpression cond
    write ") {"
    endl
    withIndent $ writeStatement true
    writeIndented "}"
    unless (false == NullStatement) $ do
        writeIndented "else {"
        withIndent $ writeStatement false
        writeIndented "}"

writeLet :: String -> Expression -> Statement -> PWS ()
writeLet identifier expr stmt =
    writeWithScope identifier (writeExpression expr) (writeStatement stmt)


writeFor :: String -> Expression -> Statement -> PWS ()
writeFor identifier expr stmt =
    writeFor_ identifier expr $
            withIndent $
            writeWithScope identifier (write "_iteree[_index]") (writeStatement stmt)

writeForExt :: String -> String -> Expression -> Statement -> PWS ()
writeForExt ident identifier expr stmt =
    writeFor_ identifier expr $
            withIndent $
            writeWithScope ident (write "_index") $
            writeWithScope identifier (write "_iteree[_index]") (writeStatement stmt)

writeFor_ :: String -> Expression -> PWS () -> PWS ()
writeFor_ identifier expr writeInner = do
    writeIndented "(function(){"
    withIndent $ do
        writeIndent
        write "var _iteree = "
        writeExpression expr
        write ";"
        endl
        writeIndented "if (Array.isArray(_iteree)) {"
        withIndent $ do
            writeIndented "for (var _index = 0; _index < _iteree.length; ++_index) {"
            writeInner
            writeIndented "}"
        writeIndented "}"
        writeIndented "else {"
        withIndent $ do
            writeIndented "for (var _index in _iteree) {"
            writeInner
            writeIndented "}"
        writeIndented "}"
    writeIndented "}).apply(this);"

writeWithScope :: String -> PWS () -> PWS () -> PWS ()
writeWithScope identifier rhs inner = do
    if identifier == "."
        then do
            writeIndent
            write "_newscope = "
            rhs
            write ";"
            endl
        else do
            writeIndent
            write "_newscope = {'"
            write identifier
            write "':"
            rhs
            write "};"
            endl
    writeIndented "_scope = _merge(this, _newscope);"
    writeIndented "(function(){"
    withIndent $ do
        writeIndented "var _scope = null; var _newscope = null;"
        inner
    writeIndented "}).apply(_scope);"

writeSwitch :: Expression -> [(Expression, Statement)] -> PWS ()
writeSwitch masterExpr branches = do
    writeIndent
    write "switch ("
    writeExpression masterExpr
    write ") {"
    endl
    withIndent $
        mapM writeSwitchBranch branches
    writeIndented "}"
    where
        writeSwitchBranch :: (Expression, Statement) -> PWS ()
        writeSwitchBranch (expr, stmt) = do
            writeIndent
            write "case "
            writeExpression expr
            write ":"
            endl
            withIndent $ do
                writeStatement stmt
                writeIndented "break;"

writeExpression :: Expression -> PWS ()
writeExpression expr =
    case expr of
        StringLiteral str -> write $ quoteJavascriptString str

        IntLiteral i -> write $ show i
        FloatLiteral i -> write $ show i

        BooleanLiteral b -> write $ if b then "true" else "false"

        ListExpression items -> do
            write "["
            sequence_ $
                intersperse (write ", ") $
                map writeExpression items
            write "]"

        AListExpression items -> do
            write "{"
            sequence_ $
                intersperse (write ", ") $
                map writeElem items
            write "}"
            where
                writeElem (key, value) = do
                writeExpression key
                write " : "
                writeExpression value

        VariableReference vn ->
            if vn == "."
                then write "this"
                else do
                    write "this['"
                    write vn
                    write "']"

        EscapeExpression mode e -> do
            let escapefunc =
                    case mode of
                        EscapeHTML -> "_htmlencode"
                        EscapeURL -> "encodeURI"
            write escapefunc
            write "(_f("
            writeExpression e
            write "))"

        BinaryExpression (Flipped op) left right ->
            writeExpression $ BinaryExpression op right left

        BinaryExpression OpMember left right -> do
            writeExpression left
            write "["
            writeExpression right
            write "]"

        BinaryExpression OpInList left right -> do
            write "_in("
            writeExpression left
            write ", "
            writeExpression right
            write ")"

        BinaryExpression OpBooleanXor left right -> do
            write "(function(a,b){return (a||b) && !(a&&b);})("
            writeExpression left
            write ","
            writeExpression right
            write ")"

        BinaryExpression o left right -> do
            let opstr = case o of
                            OpPlus -> "+"
                            OpMinus -> "-"
                            OpMul -> "*"
                            OpDiv -> "/"
                            OpMod -> "%"
                            OpEquals -> "==="
                            OpLooseEquals -> "=="
                            OpNotEquals -> "!=="
                            OpLooseNotEquals -> "!="
                            OpGreater -> ">"
                            OpLess -> "<"
                            OpNotGreater -> "<="
                            OpNotLess -> ">="
                            OpBooleanAnd -> "&&"
                            OpBooleanOr -> "||"
                            OpConcat -> "+"
            write "("
            wrappedArg o left
            write opstr
            wrappedArg o right
            write ")"
            where
                numericOps = [
                    OpPlus,
                    OpMinus,
                    OpMul,
                    OpDiv,
                    OpMod,
                    OpGreater,
                    OpLess,
                    OpNotGreater,
                    OpNotLess ]
                stringOps = [
                    OpConcat ]
                wrappedArg o i =
                    let wrapWord =
                            if o `elem` numericOps
                                then "Number"
                                else if o `elem` stringOps
                                then "String"
                                else ""
                    in write wrapWord >> write "(" >> writeExpression i >> write ")"
               


        UnaryExpression o e -> do
            let opstr = case o of
                            OpNot -> "!"
            write "("
            write opstr
            write "("
            writeExpression e
            write "))"

        FunctionCallExpression (VariableReference "library") (libnameExpr:_) -> do
            write "(_loadlib("
            writeExpression libnameExpr
            write "))"

        FunctionCallExpression fn args -> do
            write "("
            writeExpression fn
            write "("
            sequence_ . intersperse (write ",") $ map writeExpression args
            write "))"


quoteJavascriptString :: String -> String
quoteJavascriptString str =
    "'" ++ escape str ++ "'"
    where
        escapeChar '\'' = "\\'"
        escapeChar '\n' = "' + \"\\n\" + '"
        escapeChar '\t' = "' + \"\\t\" + '"
        escapeChar '\r' = "' + \"\\r\" + '"
        escapeChar x = [x]
        escape = concatMap escapeChar