packages feed

kdl-hs-0.2.0: src/KDL/Parser/Hustle/Parser.hs

{- FOURMOLU_DISABLE -}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

{- | Vendered from https://github.com/fuzzypixelz/hustle -}
module KDL.Parser.Hustle.Parser where

import           KDL.Parser.Hustle.Internal
import           KDL.Parser.Hustle.Types

import           Control.Monad                  ( void )
import           Data.Char                      ( chr
                                                , isHexDigit
                                                , isOctDigit
                                                , isDigit
                                                , isSpace
                                                )
import qualified Data.Map.Strict               as Map
import           Data.Maybe                     ( catMaybes
                                                , fromMaybe
                                                , mapMaybe
                                                , maybeToList
                                                )
import           Data.Scientific                ( Scientific )
import           Data.Text                      ( Text )
import qualified Data.Text                     as T
import           Text.Megaparsec                ( (<?>)
                                                , (<|>)
                                                , MonadParsec(eof, label, try)
                                                , anySingle
                                                , between
                                                , choice
                                                , many
                                                , manyTill
                                                , noneOf
                                                , optional
                                                , satisfy
                                                , some
                                                )
import           Text.Megaparsec.Char           ( char
                                                , char'
                                                , crlf
                                                , newline
                                                , string
                                                )

-- WHITESPACE

escline :: Parser ()
escline =
  char '\\' >> many ws >> (try linebreak <|> lineComment) <?> "Escape Line"

linespace :: Parser ()
linespace = try linebreak <|> try ws <|> try lineComment <?> "Line Space"

linebreak :: Parser ()
linebreak = label "Newline" $ do
  choice $ void crlf : map
    void
    [ char '\r' <?> "carriage return"
    , char '\n' <?> "newline"
    , char '\x85' <?> "next line"
    , char '\f' <?> "form feed"
    , char '\x2028' <?> "line seperator"
    , char '\x2029' <?> "paragraph seperator"
    ]

ws :: Parser ()
ws = bom <|> hspacechar <|> blockComment <?> "Whitespace"

bom :: Parser ()
bom = void (char '\xFEFF') <?> "BOM"

hspacechar :: Parser ()
hspacechar = label "Unicode Space" $ do
  choice $ map
    void
    [ char '\x0009' <?> "character tabulation"
    , char '\x0020' <?> "space"
    , char '\x00A0' <?> "bo-break space"
    , char '\x1680' <?> "ogham space mark"
    , char '\x2000' <?> "en quad"
    , char '\x2001' <?> "em quad"
    , char '\x2002' <?> "en space"
    , char '\x2003' <?> "em space"
    , char '\x2004' <?> "three-per-em space"
    , char '\x2005' <?> "four-per-em space"
    , char '\x2006' <?> "six-per-em space"
    , char '\x2007' <?> "figure space"
    , char '\x2008' <?> "punctuation space"
    , char '\x2009' <?> "thin space"
    , char '\x200A' <?> "hair space"
    , char '\x202F' <?> "narrow no-break space"
    , char '\x205F' <?> "medium mathmatical space"
    , char '\x3000' <?> "ideographic space"
    ]

-- STRINGS

anystring :: Parser Text
anystring = try unquotedstring <|> try rawstring <|> quotedstring

unquotedstring :: Parser Text
unquotedstring = label "Unquoted String" $ do
  c0 <- satisfy $ \c -> isValidChar c && not (isDigit c) && c /= '"'
  rest <- many $ satisfy isValidChar
  -- TODO: Forbid true, false, null, inf, -inf, nan, or "looks like a number"
  pure $ T.pack (c0 : rest)
  where
    isValidChar c = not (isSpace c) && c `notElem` ("[]{}()\\/#\";=" :: [Char])

quotedstring :: Parser Text
quotedstring = label "Quoted String" $ do
  T.concat <$> (char '"' *> manyTill character (char '"'))

rawstring :: Parser Text
rawstring = label "Raw String" $ do
  void (char 'r')
  hs <- T.pack <$> many (char '#')
  void (char '"')
  s <- manyTill anySingle (string (T.cons '"' hs))
  return (T.pack s)

character :: Parser Text
character = void (char '\\') *> escape <|> nonescape

{-
  As per the Haskell 2010 Language Report,
  (https://www.haskell.org/onlinereport/haskell2010/haskellch2.html#x7-200002.6)
  The '\/' Solidus escape isn't defined, so if we try to parse it directly the
  compiler throws a lexical error. Hence this terribleness.
-}
escape :: Parser Text
escape =
  do
    e <- choice
      [ '\x08' <$ char 'b'
      , '\x09' <$ char 't'
      , '\x0A' <$ char 'n'
      , '\x0C' <$ char 'f'
      , '\x0D' <$ char 'r'
      , '\x22' <$ char '\"'
      , '\x2F' <$ char '/'
      , '\x5C' <$ char '\\'
      ]
    return (T.singleton e)
  <|> uescape

uescape :: Parser Text
uescape = do
  void (string "u{")
  u <- fromInteger <$> number 16 isHexDigit
  if u >= 0x10ffff
    then fail "Exceeded Unicode code point limit."
    else do
      void (char '}')
      let c = chr u
      return (T.singleton c)

nonescape :: Parser Text
nonescape = do
  c <- noneOf ("\\\"" :: [Char])
  return (T.singleton c)

-- NUMBERS

binary :: Parser Integer
binary = signed $ char '0' >> char' 'b' >> number 2 isBinDigit

octal :: Parser Integer
octal = signed $ char '0' >> char 'o' >> number 8 isOctDigit

hexadecimal :: Parser Integer
hexadecimal = signed $ char '0' >> char 'x' >> number 16 isHexDigit

integer :: Parser Integer
integer = signed decimal_

scientific :: Parser Scientific
scientific = signed scientific_

-- CONTENT

name :: Parser Identifier
name = Identifier <$> (try anystring <|> identifier)

identifier :: Parser Text
identifier = label "Identifier" $ do
  i  <- satisfy iichar
  is <- many (satisfy ichar)
  let result = T.pack (i : is)
  case result of
    "true"  -> fail "keyword true in identifier"
    "false" -> fail "keyword false in identifier"
    "null"  -> fail "keyword null in identifier"
    _       -> return result
 where
  ichar c =
    (c > '\x20')
      && (c <= '\x10FFFF')
      && (c `notElem` ("\\/(){}<>;[]=,\"" :: [Char]))
      && not (match linespace (T.singleton c))
  iichar c = ichar c && c `notElem` ['0' .. '9']

nullvalue :: Parser Text
nullvalue = string "#null"

bool :: Parser Bool
bool = True <$ string "#true" <|> False <$ string "#false"

property :: Parser (Identifier, Value)
property = label "Property" $ do
  propKey <- name
  void (char '=')
  propValue <- value
  return (propKey, propValue)

value :: Parser Value
value = label "Value" $ do
  valueAnn <- optional typeAnnotation
  valueExp <- choice
    [ IntegerValue <$> try binary <?> "Binary"
    , IntegerValue <$> try octal <?> "Octal"
    , IntegerValue <$> try hexadecimal <?> "Hexadecimal"
    , SciValue <$> try scientific <?> "Decimal"
    , IntegerValue <$> try integer <?> "Integer"
    , BooleanValue <$> try bool <?> "Boolean"
    , NullValue <$ try nullvalue <?> "Null"
    , StringValue <$> anystring <?> "String"
    ]
  return Value { .. }

typeAnnotation :: Parser Identifier
typeAnnotation = label "Type Annotation" $ do
  void (char '(')
  i <- name
  void (char ')')
  return i

-- NODES

nodes :: Parser [Node]
nodes = between (many linespace) (many linespace) (fromMaybe [] <$> body)
 where
  body = optional $ do
    n  <- maybeToList <$> node
    ns <- fromMaybe [] <$> optional nodes
    return (n ++ ns)

node :: Parser (Maybe Node)
node = label "Node" $ do
  discard      <- optional comment
  nodeAnn      <- optional typeAnnotation
  nodeName     <- name
  nodeContent  <- catMaybes <$> content
  nodeChildren <- fromMaybe [] <$> optional children
  _            <- many nodespace
  _            <- terminator
  let nodeArgs  = mapMaybe isArg nodeContent
      nodeProps = Map.fromList $ mapMaybe isProp nodeContent
  case discard of
    Just _  -> return Nothing
    Nothing -> return $ Just Node { .. }
 where
  isArg c = case c of
    NodeValue v -> Just v
    _           -> Nothing
  isProp c = case c of
    NodeProperty p -> Just p
    _              -> Nothing

content :: Parser [Maybe Content]
content = many . try $ do
  void (some nodespace)
  discard <- optional $ comment <* many nodespace
  c       <- choice [NodeProperty <$> try property, NodeValue <$> try value]
  case discard of
    Just _  -> return Nothing
    Nothing -> return $ Just c

children :: Parser [Node]
children = label "Node Child" . try $ do
  void (many nodespace)
  discard <- optional comment
  void (char '{')
  ns <- nodes
  void (char '}')
  void (many ws)
  case discard of
    Just _  -> return []
    Nothing -> return ns

comment :: Parser ()
comment = label "/-Comment" . try $ do
  void (string "/-")
  void $ many nodespace

nodespace :: Parser ()
nodespace = label "Node Space" $ do
  try (many ws *> escline <* many ws) <|> try (void $ some ws)

terminator :: Parser ()
terminator = label "Node Terminator" $ do
  choice [try (void (char ';')), try (void newline), try lineComment, eof]

document :: Parser Document
document = do
  docNodes <- nodes
  void eof
  return Document { .. }