packages feed

bookhound-format-0.1.1.0: src/Bookhound/Format/Parsers/Yaml.hs

module Bookhound.Format.Parsers.Yaml (yaml, nil, integer, float, bool, string,
                     list, mapping) where


import Bookhound.Parser              (Parser, andThen, check, exactly,
                                      withError)
import Bookhound.ParserCombinators   (IsMatch (..), maybeWithin, (<#>), (<|>),
                                      (->>-), (|*), (|+), (|++), (|?))
import Bookhound.Parsers.Char        (char, colon, dash, dot, doubleQuote,
                                      hashTag, newLine, question, quote, space,
                                      whiteSpace)
import Bookhound.Parsers.Collections (listOf, mapOf)
import Bookhound.Parsers.Number      (double, hexInt, int, octInt)
import Bookhound.Parsers.String      (blankLine, blankLines, spaces,
                                      spacesOrTabs, tabs, withinDoubleQuotes,
                                      withinQuotes)

import Bookhound.Format.SyntaxTrees.Yaml    (CollectionType (..), YamlExpression (..))

import qualified Bookhound.Parsers.DateTime as Dt

import           Data.List (nub)
import qualified Data.Map  as Map



yaml :: Parser YamlExpression
yaml =  normalize `andThen` yamlWithIndent (-1)




-- TODO: Add support for anchors and aliases

nil :: Parser YamlExpression
nil = withError "Yaml Null"
  $ YamlNull <$ oneOf ["null", "Null", "NULL"]

integer :: Parser YamlExpression
integer = withError "Yaml Integer"
  $ YamlInteger <$> (hexInt <|> octInt <|> int)

float :: Parser YamlExpression
float = withError "Yaml Float"
  $ YamlFloat <$> double

bool :: Parser YamlExpression
bool = withError "Yaml Bool"
  $ YamlBool <$> (True  <$ oneOf ["true", "True", "TRUE"]    <|>
                  False <$ oneOf ["false", "False", "FALSE"])


dateTime :: Parser YamlExpression
dateTime = withError "Yaml DateTime"
  $ YamlDateTime <$> Dt.dateTime

date :: Parser YamlExpression
date = withError "Yaml Date"
  $ YamlDate <$> Dt.date

time :: Parser YamlExpression
time = withError "Yaml Time"
  $ YamlTime <$> Dt.time


string :: Int -> Parser YamlExpression
string indent = withError "Yaml String"
  $ YamlString <$> text indent


sequential :: Parser a -> Int -> Parser [YamlExpression]
sequential sep indent = listParser
  where

  listParser = indentationCheck elemParser indent

  elemParser = do n <- length <$> (space |*)
                  sep *> whiteSpace
                  elm <- yamlWithIndent n
                  pure (n, elm)


list :: Int -> Parser YamlExpression
list indent = withError "Json List"
  $   (YamlList Inline   <$> jsonList)
  <|> (YamlList Standard <$> yamlList)
  where
    yamlList = sequential dash indent
    jsonList = maybeWithin spacesOrTabs $ listOf $ yamlWithIndent (-1)


set :: Int -> Parser YamlExpression
set indent = withError "Yaml Set"
  $ YamlList Standard . nub <$> sequential question indent


mapping :: Int -> Parser YamlExpression
mapping indent = withError "Yaml Mapping"
  $   (YamlMap Inline   <$> jsonMap)
  <|> (YamlMap Standard <$> yamlMap)
  where
    yamlMap = Map.fromList <$> mapParser
    jsonMap = maybeWithin spacesOrTabs $ mapOf colon (text 100) $ yamlWithIndent (-1)

    mapParser = indentationCheck keyValueParser indent

    keyValueParser = do n <- length <$> (space |*)
                        key <- show <$> element indent
                        (spacesOrTabs |?)
                        colon *> whiteSpace
                        value <- yamlWithIndent n
                        pure (n, (key, value))



element :: Int -> Parser YamlExpression
element indent = exactly (dateTime <|> date <|> time <|> float <|>
                          integer <|> bool <|> nil) <|>
                    string indent

container :: Int -> Parser YamlExpression
container indent = list indent <|> mapping indent <|> set indent



yamlWithIndent :: Int -> Parser YamlExpression
yamlWithIndent indent = maybeWithin ((blankLine <|> comment <|> directive <|>
                                      docStart <|> docEnd) |+)
                        yamlValue
  where
    yamlValue = container indent <|> maybeWithin spacesOrTabs (element indent)
    comment = hashTag *> (inverse newLine |+) <* newLine
    directive = is "%" *> (inverse space *> (inverse newLine |+)) <* newLine
    docStart = dash <#> 3
    docEnd = dot <#> 3



text :: Int -> Parser String
text indent = withinDoubleQuotes (quotedParser (inverse doubleQuote |*))                      <|>
              withinQuotes       (quotedParser (inverse quote       |*))                      <|>
              (is "|" *> blankLine *> plainTextParser literalLineParser)                      <|>
              (is ">" *> blankLine *> (spacesOrTabs |?) *> plainTextParser foldingLineParser) <|>
              plainTextParser foldingLineParser
  where

    quotedParser parser = mconcat <$> ((snd <$> foldingLineParser parser) |*)

    plainTextParser styleParser = allowedStart ->>- allowedString ->>-
                                  (indentationCheck (styleParser allowedString) indent |*)

    foldingLineParser parser = do sep <- ("\n" <$ newLine <* blankLines) <|> (" " <$ newLine)
                                  n   <- maybeWithin tabs $ length <$> (space |*)
                                  str <- parser
                                  pure (n, sep <> str)

    literalLineParser parser = do sep <- pure <$> newLine
                                  n   <- length <$> (space |*)
                                  str <- parser
                                  pure (n, sep <> replicate (n - indent) ' ' <> str)

    allowedStart = noneOf $ forbiddenChar <> ['>', '|', ':', '!']

    allowedString = (noneOf forbiddenChar |*)

    forbiddenChar = ['\n', '#', '&', '*', ',', '?', '-', ':', '[', ']', '{', '}']



indentationCheck :: Parser (Int, a) -> Int -> Parser [a]
indentationCheck parser indent = ((snd <$> check "indentation"
                                  (\(n, _) -> n > indent) parser) |+)


normalize :: Parser String
normalize = withError "Normalize Yaml"
  $ (parserActions ->>- normalize) <|> (char |*)
  where

    parserActions = spreadDashes     <|>
                    spreadDashKey    <|>
                    spreadKeyDash    <|>
                    next

    next = pure <$> char

    spreadDashes = ((<>) "- ") . genDashes <$> dashesParser

    genDashes (offset, n) = concatMap (\x -> "- " <> replicate (offset + 2 * x) ' ')
                                      [1 .. n - 1]

    dashesParser = do offset <- length <$> (spaces |?)
                      n <- length <$> ((dash <* spacesOrTabs) |++)
                      pure (offset, n)


    spreadDashKey = (\(offset, key) -> replicate offset ' ' <> "- " <>
                                      replicate (offset + 2) ' ' <> key <> ": ")
                    <$> dashKeyParser

    dashKeyParser = do offset <- length <$> (spaces |?)
                       dash <* spacesOrTabs
                       key <- text 100 <* maybeWithin spacesOrTabs colon
                       pure (offset, key)


    spreadKeyDash = (\(offset, key) -> replicate offset ' ' <> key <> ": " <>
                                      replicate (offset + 2) ' ' <> "- ")
                    <$> keyDashParser

    keyDashParser = do offset <- length <$> (spaces |?)
                       key <- text 100 <* maybeWithin spacesOrTabs colon
                       dash <* spacesOrTabs
                       pure (offset, key)