{-|
Module      : $Header$
Description : Functions for parsing mustache templates
Copyright   : (c) Justus Adam, 2015
License     : LGPL-3
Maintainer  : development@justusadam.com
Stability   : experimental
Portability : POSIX
-}
{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections     #-}
{-# LANGUAGE UnicodeSyntax     #-}
module Text.Mustache.Parser
  (
  -- * Generic parsing functions

    parse, parseWithConf

  -- * Configurations

  , MustacheConf, emptyConf, defaultConf

  -- * Parser

  , MustacheParser

  -- ** Components

  , genParseTag, parseSection, parseTag, parseVariable, parsePartial, parseText
  , parseInvertedSection, parseDelimiterChange, parseUnescapedVar, parseEnd

  -- * Mustache Constants

  , sectionBegin, sectionEnd, invertedSectionBegin, unescape2, unescape1
  , delimiterChange, nestingSeparator

  ) where


import           Control.Monad
import           Data.Char           (isAlphaNum, isSpace)
import           Data.Foldable       (fold)
import           Data.Functor        ((<$>))
import           Data.List           (nub)
import           Data.Monoid         (mempty, (<>))
import           Data.Text           as T
import           Prelude             as Prel
import           Text.Mustache.Types
import           Text.Parsec         as P hiding (parse)


data MustacheConf = MustacheConf
  { delimiters  (String, String)
  }

-- | @#@
sectionBegin  String
sectionBegin = "#"
-- | @/@
sectionEnd  String
sectionEnd = "/"
-- | @>@
partialBegin  String
partialBegin = ">"
-- | @^@
invertedSectionBegin  String
invertedSectionBegin = "^"
-- | @{@ and @}@
unescape2  (String, String)
unescape2 = ("{", "}")
-- | @&@
unescape1  String
unescape1 = "&"
-- | @=@
delimiterChange  String
delimiterChange = "="
-- | @.@
nestingSeparator  String
nestingSeparator = "."
-- | Cannot be a letter, number or the nesting separation Character @.@
isAllowedDelimiterCharacter  Char  Bool
isAllowedDelimiterCharacter =
  not . Prel.or . sequence
    [ isSpace, isAlphaNum, flip elem nestingSeparator ]
allowedDelimiterCharacter  MustacheParser Char
allowedDelimiterCharacter =
  satisfy isAllowedDelimiterCharacter


-- | Empty configuration
emptyConf  MustacheConf
emptyConf = MustacheConf ("", "")


-- | Default configuration (delimiters = ("{{", "}}"))
defaultConf  MustacheConf
defaultConf = emptyConf { delimiters = ("{{", "}}") }


type MustacheParser = Parsec Text MustacheConf
type MNodeParser = MustacheParser (MustacheNode Text)


{-|
  Runs the parser for a mustache template, returning the syntax tree.
-}
parse  FilePath  Text  Either ParseError MustacheAST
parse = parseWithConf defaultConf


parseWithConf  MustacheConf  FilePath  Text  Either ParseError MustacheAST
parseWithConf = P.runParser (parseText Nothing)


parseText  Maybe [Text]  MustacheParser MustacheAST
parseText
  tagName = do
    (MustacheConf { delimiters = ( start, _ )})  getState
    let endOfText = try (void $ string start) <|> try eof
    content  pack <$> manyTill anyChar (lookAhead endOfText)
    others  parseTag tagName
    return $ if T.null content
              then others
              else MustacheText content : others



parseTag  Maybe [Text]  MustacheParser MustacheAST
parseTag tagName = choice
  [ parseEnd tagName >> return []
  , parseSection >>= continue
  , parseInvertedSection >>= continue
  , parseUnescapedVar >>= continue
  , parseDelimiterChange >> parseText tagName
  , parsePartial >>= continue
  , parseVariable >>= continue
  , eof >> maybe (return []) (parserFail . ("Unclosed section " <>) . unpack . fold) tagName
  ]
  where
    continue val = (val :) <$> parseText tagName

parseSection  MNodeParser
parseSection = do
  sectionName  genParseTag sectionBegin mempty
  MustacheSection sectionName <$> parseText (return sectionName)


parsePartial  MNodeParser
parsePartial = do
  (MustacheConf { delimiters = ( start, end )}) <- getState
  let pStart = start <> partialBegin
      pEnd = end
  void $ try $ string pStart
  spaces
  MustachePartial <$>
    anyChar `manyTill` try (skipMany space >> string pEnd)


parseDelimiterChange  MustacheParser ()
parseDelimiterChange = do
  (MustacheConf { delimiters = ( start, end )}) <- getState
  void $ try $ string (start <> delimiterChange)
  delim1  allowedDelimiterCharacter `manyTill` space
  spaces
  delim2  allowedDelimiterCharacter `manyTill` try (string $ delimiterChange <> end)
  oldState  getState
  putState $ oldState { delimiters = (delim1, delim2) }


parseInvertedSection  MNodeParser
parseInvertedSection = do
  sectionName  genParseTag invertedSectionBegin mempty
  MustacheInvertedSection sectionName <$> parseText (return sectionName)


parseUnescapedVar  MNodeParser
parseUnescapedVar = MustacheVariable False <$>
  (try (uncurry genParseTag unescape2) <|> genParseTag unescape1 mempty)


parseVariable  MNodeParser
parseVariable = MustacheVariable True <$> genParseTag mempty mempty


parseEnd  Maybe [Text] -> MustacheParser ()
parseEnd tagName = do
  tag  genParseTag sectionEnd mempty
  unless (isSameSection tag) $
    parserFail $
      maybe
        (unexpectedSection tag)
        (`unexpectedClosingSequence` tag)
        tagName
  where
    isSameSection = maybe (const True) (==) tagName


genParseTag  String  String  MustacheParser [Text]
genParseTag smod emod = do
  (MustacheConf { delimiters = ( start, end ) }) <- getState

  let nStart = start <> smod
      nEnd = emod <> end
      disallowed = nub $ nestingSeparator <> start <> end

      parseOne :: MustacheParser [Text]
      parseOne = do
        spaces

        one  noneOf disallowed
          `manyTill` lookAhead
            (try (spaces >> void (string nEnd))
            <|> try (void $ string nestingSeparator))

        others  (string nestingSeparator >> parseOne)
                  <|> (const mempty <$> (spaces >> string nEnd))
        return $ pack one : others

  void $ try $ string nStart
  parseOne


-- ERRORS

sectionToString  [Text]  String
sectionToString = unpack . intercalate "."

unexpectedSection  [Text]  String
unexpectedSection s = "No such section '" <> sectionToString s <> "'"
unexpectedClosingSequence  [Text]  [Text]  String
unexpectedClosingSequence tag1 tag2 =
  "Expected closing sequence for section '"
  <> sectionToString tag1
  <> "' got '"
  <> sectionToString tag2
  <> "'"