packages feed

marxup-3.1.0.0: MarXupParser.hs

{-# LANGUAGE TupleSections, FlexibleInstances, TransformListComp #-}

module MarXupParser (parseFile, Haskell(..), MarXup(..)) where

import Text.ParserCombinators.Parsek.Position
import Data.Char
import Data.List
import System.IO
import Control.Monad
import GHC.Exts (the,groupWith)
import Config

-- todo: parse haskell comments (so that marxup there is not recognized)

------------------
-- Simple printing combinators, which do not add nor remove line breaks

data Haskell = HaskChunk String | HaskLn SourcePos | Quote [MarXup] | List [Haskell] | Parens [Haskell] | String String deriving (Show)
data MarXup = TextChunk String | Unquote (Maybe (SourcePos,String)) [(SourcePos,Haskell)] | Comment String deriving (Show)

----------------------------------------------
-- Parsing combinators

anyQuoteStrings :: [String]
anyQuoteStrings = concatMap (\(x,y) -> [x,y]) quoteStrings

pTextChunk = TextChunk <$> pChunk' (commentString : antiQuoteStrings ++ anyQuoteStrings) <?> "Text chunk"
pHaskChunk = HaskChunk <$> pChunk' (map box "\n\"[]()" ++ map fst quoteStrings) <?> "Haskell chunk"
    -- we keep track of balancing

pWPos :: Parser SourcePos
pWPos = do
  char '\n'
  getPosition

withPos :: Parser a -> Parser (SourcePos,a)
withPos p = do
  pos <- getPosition
  x <- p
  return (pos,x)

pHaskLn = HaskLn <$> pWPos -- before each newline, tell GHC where we are.

box = (:[])

pString :: Parser Haskell
pString = do
  char '"'
  result <- many (string "\\\"" <|> pChunk ['"'])
  char '"'
  return $ String $ concat result

-- | Parse some Haskell code with markup inside.
pHask :: Parser [Haskell]
pHask = many ((List <$> pArg "[]") <|>
              (Parens <$> pArg "()") <|>
              pTextArg  <|>
              pString <|>
              pHaskChunk <|>
              pHaskLn)

-- | Parse a text argument to an element
pTextArg' :: String -> String -> Parser Haskell
pTextArg' open close = Quote <$> (label "quoted text" $
  string open *>
  (many (pElement <|> pTextChunk <|> pComment))
  <* string close)

pTextArg :: Parser Haskell
pTextArg = choice $ map (uncurry pTextArg') quoteStrings

pArg :: String -> Parser [Haskell]
pArg [open,close] = char open *> pHask <*  char close

isIdentChar :: Char -> Bool
isIdentChar x = isAlphaNum x || (x `elem` "\'_")

pIdent :: Parser String
pIdent = munch1 isIdentChar <?> "identifier"

pArgument :: Parser Haskell
pArgument = (Parens <$> pArg "()" <|> (List <$> pArg "[]") <|> pTextArg <|> pString) <?> "argument"

pId :: Parser Haskell
pId = HaskChunk <$> pIdent

pElement :: Parser MarXup
pElement = 
  label "Haskell element" $ do
    choice $ map string $ antiQuoteStrings
    var <- (Just <$> (withPos pIdent <* string "<-")) <<|> pure Nothing
    val <- ((:) <$> withPos pId <*> manyGreedy (withPos pArgument)) <|>
           (box <$> withPos (Parens <$> pArg "()"))
    return $ Unquote var val

commentString :: String
commentString = "%%"

pComment :: Parser MarXup
pComment = Comment <$> do
  label "Comment" $ do
    string commentString
    munch (/= '\n')
    string "\n"
    return mempty

parseFile :: String -> ([Haskell] -> IO ()) -> IO ()
parseFile fname k = do
  p <- parseFromFile (pHask <* endOfFile) completeResults fname
  case p of
    Left e -> handleErr e
    Right [res] -> k res
    Right _ -> hPutStrLn stderr "Amibiguous input!"

handleErr e =
   sequence_
          [ hPutStrLn stderr (show $ maybePosToPos $ the pos) >>
            hPutStrLn stderr ("  Expected:" ++ (intercalate " or " $ nub what))
           | (exps,_why) <- e, (what,pos) <- exps, then group by pos using groupWith, then reverse ]

----------------------------------------------
-- Parsing helpers

satisfy' :: (String -> Bool) -> Parser Char
satisfy' p = do
  l <- look
  unless (p l) $
    fail "Unexpected leading string"
  anySymbol

munch',munch1' :: (String -> Bool) -> Parser String
munch' p = scan =<< look
 where
  scan (c:cs) | p (c:cs) = (:) <$> anySymbol <*> scan cs
  scan _            = pure []

munch1' p = (:) <$> satisfy' p <*> munch' p

-- | A chunk not containing some strings
pChunk' :: [String] -> Parser String
pChunk' stops = munch1' (\l -> not $ any (`isPrefixOf` l) stops)


-- | A chunk not containing some chars.
pChunk :: [Char] -> Parser String
pChunk stops = munch1 (not . (`elem` stops))


-- Tests
testHask = parse "<interactive>" pHask completeResults "arst « text @z<-fct[x](y) awft"
testHask2 = parse "<interactive>" pHask completeResults "ars(t) « text @z<-fct[x](y) » awft"
testText2 = parse "<interactive>" pTextArg completeResults "« text @fct(x »"
testText3 = parse "<interactive>" pTextArg completeResults "« 1 @x 2 @y 3 @x 4 »"
testElem = parse "<interactive>" pElement completeResults "@x<-fct(x « yop »)[y]"
testChunk = parse "<interactive>" pHaskChunk completeResults "t"
testArg = parse "<interactive>" (pArg "()") completeResults "()"