packages feed

hakyllbars-1.0.0.0: src/Hakyllbars/Source/Lexer.hs

module Hakyllbars.Source.Lexer where

import Data.Functor
import Data.Maybe
import Data.Scientific
import Data.String
import Hakyllbars.Source.Util
import Text.Parsec hiding (runParser, token, tokens, (<?>))
import Prelude hiding (lex)

type Lexer a = Parsec String ParserState a

lex :: SourceName -> String -> Either ParseError [Token]
lex = runParser $ manyTill token eof

token :: Lexer Token
token =
  getLexerMode >>= \case
    --
    BlockMode -> do
      trace "BlockMode"
      blockToken >>= \t -> case t of
        TaggedToken TurnOffToken _ -> t <$ startFenced
        TaggedToken CommentBlockToken _ -> t <$ startFenced
        TaggedToken CloseBlockToken _ -> t <$ startText
        _ -> return t
    --
    TextMode -> do
      trace "TextMode"
      tryOne
        [ do
            void . lookAhead $ tryOne [spaces *> trimmingOpen, open]
            startBlock
            token,
          textToken
        ]
    --
    FencedMode {} -> do
      trace "FencedMode"
      fencedText

startText :: (Show t, Stream s m t) => ParsecT s ParserState m ()
startText = putLexerMode TextMode <?> "starting text"

startBlock :: (Show t, Stream s m t) => ParsecT s ParserState m ()
startBlock = putLexerMode BlockMode <?> "starting block"

startFenced :: (Show t, Stream s m t) => ParsecT s ParserState m ()
startFenced = putLexerMode (FencedMode 1) <?> "starting fenced text"

illegalState :: (Show t, Stream s m t) => ParsecT s ParserState m a
illegalState = p <?> "illegal state"
  where
    p = do
      mode <- getLexerMode
      fail $ "Illegal lexer mode: " ++ show mode

blockToken :: Lexer Token
blockToken =
  tryOne
    [ symbolToken,
      numberToken,
      boolToken,
      endKeyword,
      elseKeyword,
      nameToken,
      stringToken
    ]

textToken :: Lexer Token
textToken = withPosition (TextToken <$> text) <?> "text"

fencedText :: Lexer Token
fencedText = withPosition (TextToken <$> p "")
  where
    p acc =
      tryOne
        [ downFence acc,
          upFence acc,
          fenceText acc
        ]
    -- {{
    downFence acc = do
      trace "downFence"
      x <- open
      downFenceLevel
      p (acc ++ x)
    -- }}
    upFence acc = do
      trace "upFence"
      void . lookAhead $ tryOne [trimmingClose, close]
      upFenceLevel >>= \case
        0 -> do
          startBlock
          return acc
        _ -> do
          x <- tryOne [trimmingClose, close]
          p (acc ++ x)
    -- ...abc...
    fenceText acc = do
      trace "fenceText"
      t <- text
      p (acc ++ t)
    --
    downFenceLevel = do
      n <- succ <$> fenceLevel
      putLexerMode $ FencedMode n
    upFenceLevel = do
      n <- pred <$> fenceLevel
      putLexerMode $ FencedMode n
      return n
    fenceLevel =
      getLexerMode >>= \case
        FencedMode n -> return n
        _ -> illegalState

text :: Lexer String
text = mconcat <$> manyTill p (lookAhead $ try end)
  where
    p =
      tryOne
        [ labeled "TextString" $ many1 (noneOf "-{}\\\n\t "),
          labeled "Just '{'" $ openBrace <* notFollowedBy (try openBrace),
          labeled "Just '}'" $ closeBrace <* notFollowedBy (try closeBrace),
          labeled "Just '-'" $ string "-" <* notFollowedBy (try closeBrace),
          labeled "Just '\\'" $ string "\\" <* notFollowedBy (try escapedText),
          labeled "EscapedText" $ char '\\' *> escapedText,
          labeled "SpaceString" $ many1 space <* notFollowedBy (try trimmingOpen)
        ]
    escapedText =
      tryOne
        [ openBrace,
          closeBrace,
          string "-",
          string "\\"
        ]
    end =
      labeled "text terminator" . tryOne $
        [ spaces *> trimmingOpen,
          trimmingClose,
          open,
          close,
          "" <$ eof
        ]

symbolToken :: Lexer Token
symbolToken =
  withPosition $
    tryOne
      [ spaces
          *> tryOne
            [ mkTrimmingSymbol TurnOffToken,
              mkTrimmingSymbol CommentBlockToken,
              mkTrimmingSymbol AltBlockToken <* spaces,
              mkTrimmingSymbol ChromeBlockToken <* spaces,
              mkTrimmingSymbol ExpressionBlockToken <* spaces
            ],
        mkTrimmingSymbol CloseBlockToken <* spaces,
        --
        mkSymbol TurnOffToken,
        mkSymbol CommentBlockToken,
        mkSymbol AltBlockToken <* spaces,
        mkSymbol ChromeBlockToken <* spaces,
        mkSymbol ExpressionBlockToken <* spaces,
        mkSymbol CloseBlockToken,
        --
        mkSymbol OpenBraceToken <* spaces,
        mkSymbol CloseBraceToken <* spaces,
        mkSymbol OpenParenToken <* spaces,
        mkSymbol CloseParenToken <* spaces,
        mkSymbol OpenBracketToken <* spaces,
        mkSymbol CloseBracketToken <* spaces,
        mkSymbol PipeToken <* spaces,
        mkSymbol ColonToken <* spaces,
        mkSymbol DotToken <* spaces,
        mkSymbol CommaToken <* spaces
      ]
  where
    mkSymbol t = TaggedToken t <$ string (tokenTagValue t) <?> show t
    mkTrimmingSymbol t = TaggedToken t <$ string (trimmingTokenTagValue t) <?> showTrimmingTokenTag t

boolToken :: Lexer Token
boolToken = withPosition (BoolToken <$> value) <?> "BoolToken"
  where
    value =
      True <$ tryOne [keyword "True", keyword "true"]
        <|> False <$ tryOne [keyword "False", keyword "false"]

stringToken :: Lexer Token
stringToken = labeled "StringToken" $ withPosition $ (StringToken <$> stringChars) <* spaces
  where
    stringChars =
      tryOne
        [ between squote squote $ many (noneOf "\\\'\n" <|> escapeChar),
          between dquote dquote $ many (noneOf "\\\"\n" <|> escapeChar)
        ]
    squote = char '\''
    dquote = char '"'
    escapeChar =
      labeled "EscapeChar" $
        char '\\'
          *> tryOne
            [ char '\\',
              char '\'',
              char '"',
              char 'n' $> '\n',
              char 't' $> '\t'
            ]

numberToken :: Lexer Token
numberToken = withPosition (numberToken' <* spaces)
  where
    numberToken' = do
      value <- ints <> option "" doubles
      notFollowedBy badChars
      case (read value :: Scientific) of
        n | isInteger n -> labeled "IntToken" . return . IntToken . fromJust . toBoundedInteger $ n
        n | otherwise -> labeled "DoubleToken" . return . DoubleToken . toRealFloat $ n

    ints = labeled "IntegerDigits" $ justZero <|> ((:) <$> nonZero <*> many digit)
    justZero = labeled "JustZero" $ string "0" <* notFollowedBy nonZero
    nonZero = labeled "NonZeroDigit" $ oneOf ['1' .. '9']
    doubles = labeled "DoubleDigits" $ string "." <> many1 digit
    badChars = labeled "BadNumberChar" $ oneOf "_." <|> alphaNum

nameToken :: Lexer Token
nameToken = withPosition $ (NameToken <$> name <?> "NameToken") <* spaces

endKeyword :: Lexer Token
endKeyword = withPosition do
  keyword (tokenTagValue EndToken)
  return $ TaggedToken EndToken

elseKeyword :: Lexer Token
elseKeyword = withPosition do
  keyword (tokenTagValue ElseToken)
  return $ TaggedToken ElseToken

keyword :: String -> Lexer ()
keyword s = labeled ("Keyword " ++ show s) do
  () <$ string s <* notFollowedBy nameRest <* spaces

name :: Lexer String
name = labeled "NameValue" do
  (:) <$> nameStart <*> many nameRest

nameStart :: Lexer Char
nameStart = oneOf ('_' : ['a' .. 'z'])

nameRest :: Lexer Char
nameRest = tryOne [nameStart, alphaNum]

data Token
  = TaggedToken TokenTag SourcePos
  | BoolToken Bool SourcePos
  | NameToken String SourcePos
  | StringToken String SourcePos
  | IntToken Int SourcePos
  | DoubleToken Double SourcePos
  | TextToken String SourcePos
  deriving (Eq)

instance Show Token where
  show t = case t of
    TaggedToken tt _ -> show tt
    BoolToken b _ -> "Bool " ++ show b
    NameToken s _ -> "Name " ++ show s
    StringToken s _ -> "String " ++ show s
    IntToken n _ -> "Int " ++ show n
    DoubleToken x _ -> "Double " ++ show x
    TextToken s _ -> "Text " ++ show s

getTokenPos :: Token -> SourcePos
getTokenPos = \case
  TaggedToken _ pos -> pos
  BoolToken _ pos -> pos
  NameToken _ pos -> pos
  StringToken _ pos -> pos
  IntToken _ pos -> pos
  DoubleToken _ pos -> pos
  TextToken _ pos -> pos

data TokenTag
  = ExpressionBlockToken
  | CommentBlockToken
  | AltBlockToken
  | ChromeBlockToken
  | CloseBlockToken
  | OpenParenToken
  | CloseParenToken
  | OpenBraceToken
  | CloseBraceToken
  | OpenBracketToken
  | CloseBracketToken
  | PipeToken
  | CommaToken
  | DotToken
  | ColonToken
  | EndToken
  | ElseToken
  | TurnOffToken
  deriving (Eq)

tokenTagName :: TokenTag -> String
tokenTagName = \case
  ExpressionBlockToken -> "ExpressionBlock"
  CommentBlockToken -> "CommentBlock"
  AltBlockToken -> "AltBlock"
  ChromeBlockToken -> "ChromeBlock"
  CloseBlockToken -> "CloseBlock"
  OpenParenToken -> "OpenParen"
  CloseParenToken -> "CloseParen"
  OpenBracketToken -> "OpenBracket"
  CloseBracketToken -> "CloseBracket"
  OpenBraceToken -> "OpenBrace"
  CloseBraceToken -> "CloseBrace"
  PipeToken -> "PipeToken"
  CommaToken -> "CommaToken"
  DotToken -> "DotToken"
  ColonToken -> "ColonToken"
  EndToken -> "EndToken"
  ElseToken -> "ElseToken"
  TurnOffToken -> "TurnOff"

tokenTagValue :: TokenTag -> String
tokenTagValue = \case
  ExpressionBlockToken -> "{{"
  CommentBlockToken -> "{{!"
  AltBlockToken -> "{{#"
  ChromeBlockToken -> "{{@"
  CloseBlockToken -> "}}"
  OpenParenToken -> "("
  CloseParenToken -> ")"
  OpenBraceToken -> "{"
  CloseBraceToken -> "}"
  OpenBracketToken -> "["
  CloseBracketToken -> "]"
  PipeToken -> "|"
  CommaToken -> ","
  DotToken -> "."
  ColonToken -> ":"
  EndToken -> "end"
  ElseToken -> "else"
  TurnOffToken -> "{{*"

tokenTagParser :: TokenTag -> Lexer String
tokenTagParser tag = string (tokenTagValue tag) <?> show tag

trimmingTokenTagValue :: TokenTag -> String
trimmingTokenTagValue = \case
  ExpressionBlockToken -> "{{-"
  CommentBlockToken -> "{{-!"
  AltBlockToken -> "{{-#"
  ChromeBlockToken -> "{{-@"
  CloseBlockToken -> "-}}"
  TurnOffToken -> "{{-*"
  t -> tokenTagValue t

trimmingTokenTagParser :: TokenTag -> Lexer String
trimmingTokenTagParser tag = string (trimmingTokenTagValue tag) <?> showTrimmingTokenTag tag

showTrimmingTokenTag :: TokenTag -> String
showTrimmingTokenTag t = tokenTagName t ++ " " ++ show (trimmingTokenTagValue t)

open :: Lexer String
open = tokenTagParser ExpressionBlockToken

close :: Lexer String
close = tokenTagParser CloseBlockToken

trimmingOpen :: Lexer String
trimmingOpen = trimmingTokenTagParser ExpressionBlockToken

trimmingClose :: Lexer String
trimmingClose = trimmingTokenTagParser CloseBlockToken

openBrace :: Lexer String
openBrace = tokenTagParser OpenBraceToken

closeBrace :: Lexer String
closeBrace = tokenTagParser CloseBraceToken

instance Show TokenTag where
  show t = tokenTagName t ++ " " ++ show (tokenTagValue t)