hiccup-0.2: BSParse.hs
{-# OPTIONS_GHC -fbang-patterns #-}
module BSParse (parseArgs,runParse,getInterp,TclWord(..),dropWhite) where
import qualified Data.ByteString.Char8 as B
import Control.Monad
import Data.Char
import Test.HUnit -- IGNORE
data TclWord = Word B.ByteString | Subcommand [TclWord] | NoSub B.ByteString deriving (Show,Eq)
dispatch str = do h <- safeHead str
case h of
'{' -> nested str
'[' -> parseSub str
'"' -> parseStr str
_ -> getword str
parseArgs = multi (dispatch . dropWhite)
runParse = multi (mainparse . dropWhite)
safeHead s = guard (not (B.null s)) >> return (B.head s)
getInterp str = do
loc <- B.findIndex (\x -> x == '$' || x == '[') str
let locval = B.index str loc
if escaped loc str
then do (p,v,r) <- getInterp (B.drop (loc+1) str)
return (B.append (B.take (loc-1) str) (B.cons locval p), v, r)
else let (pre,aft) = B.splitAt loc str in
case B.index str loc of
'$' -> do (s, rest) <- getword aft
return (pre, s,rest)
'[' -> do (s, rest) <- parseSub aft
return (pre, s, rest)
mainparse str = do h <- safeHead str
case h of
';' -> return ([], B.tail str)
'\n' -> return ([], B.tail str)
'#' -> eatcomment str
_ -> parseArgs str
multi p s = do (w,r) <- p s
case multi p r of
Nothing -> return ([w],r)
Just (wx,r2) -> return $! (w:wx,r2)
parseSub s = do guard (B.head s == '[')
(p,r) <- parseArgs (B.tail s)
loc <- B.elemIndex ']' r
let (pre,aft) = B.splitAt loc r
return (Subcommand p, B.tail aft)
eatcomment = return . (,) [] . B.tail . B.dropWhile (/= '\n')
dropWhite = B.dropWhile (\x -> x == ' ' || x == '\t')
wordChar ' ' = False
wordChar c = let ci = ord c in
(ord 'a' <= ci && ci <= ord 'z') || (ord 'A' <= ci && ci <= ord 'Z') ||
(ord '0' <= ci && ci <= ord '9') || (c `B.elem` (B.pack "+-*=/:^$%!^&<>"))
getword s = if B.null w then fail "can't parse word" else return (Word w,n)
where (w,n) = B.span wordChar s
parseStr s = do loc <- B.elemIndex '"' str
let (w,r) = B.splitAt loc str
if escaped loc str then do (Word w1, v) <- parseStr r
let nw = B.snoc (B.take (B.length w - 1) w) '"'
return (ueword (B.append nw w1), v)
else return (ueword w, B.tail r)
where str = B.tail s
ueword = Word . strSub (B.pack "\\t", B.singleton '\t') . strSub (B.pack "\\n",B.singleton '\n')
strSub (from,to) s = B.concat $ reverse $ breakUp s nls
where nls = reverse $ B.findSubstrings from s
breakUp x [] = [x]
breakUp x (i:xs) = let (a,b) = B.splitAt i x in (B.drop (B.length from) b):to:breakUp a xs
escaped v s = escaped' v
where escaped' !i = if (i <= 0) then False else (B.index s (i-1) == '\\') && not (escaped' (i-1))
nested s = do ind <- match 0 0
let (w,r) = B.splitAt ind s
return (NoSub (B.tail w), (B.tail r))
where match !c !i
| B.length s <= i = fail $ "Couldn't match bracket" ++ show s
| otherwise =
case B.index s i of
'}' -> if c == 1 then return i else match (c-1) (i+1)
'{' -> match (c+1) (i+1)
_ -> match c (i+1)
-- # TESTS # --
testNested = "Fail nested" ~: Nothing ~=? nested (bp " { the end")
testNested2 = "Pass nested" ~: Just (NoSub (bp " { }"), B.empty) ~=? nested (bp "{ { }}")
testNested3 = "Fail nested" ~: Nothing ~=? nested (bp " { { }")
testEscaped = (escaped 1 (B.pack "\\\"")) ~? "pre-slashed quote should be escaped"
testEscaped2 = TestCase $ assertBool "non-slashed quote not escaped" (not (escaped 1 (B.pack " \"")))
testEscaped3 = TestCase $ assertBool "pre-slashed quote should be escaped" (escaped 2 (B.pack " \\\""))
testEscaped4 = TestCase $ assertBool "non-slashed quote not escaped" (not (escaped 2 (B.pack " \"")))
bp = B.pack
mklit = Word . bp
mkwd = Word . bp
testParseStr = "Escaped works" ~: Just (mklit "Oh \"yeah\" baby.", B.empty) ~=? parseStr (bp "\"Oh \\\"yeah\\\" baby.\"")
testParseStrLeft = "Parse Str with leftover" ~: Just (mklit "Hey there.", bp " 44") ~=? parseStr (bp "\"Hey there.\" 44")
testGetInterp = "Escaped $ works" ~: Nothing ~=? getInterp (bp "a \\$variable")
testGetInterp2 = "unescaped $ works" ~: Just (bp "a ", mkwd "$variable", bp "") ~=? getInterp (bp "a $variable")
testGetInterp3 = "Escaped [" ~: Nothing ~=? getInterp (bp "a \\[sub] thing.")
testGetInterp4 = "Escaped []" ~: Nothing ~=? getInterp (bp "a \\[sub\\] thing.")
testGetInterp5 = "Escaped [] crazy" ~:
Just (bp "a ",Subcommand [mkwd "sub",mklit "quail [puts 1]"], bp " thing.") ~=? getInterp (bp "a [sub \"quail [puts 1]\"] thing.")
testGetInterp6 = "unescaped $ works" ~: Just (bp "a $", mkwd "$variable", bp "") ~=? getInterp (bp "a \\$$variable")
nestedTests= TestList [testNested, testNested2, testNested3]
getInterpTests = TestList [ testGetInterp, testGetInterp2, testGetInterp3, testGetInterp4, testGetInterp5, testGetInterp6 ]
tests = TestList [ nestedTests, testEscaped, testEscaped2, testEscaped3, testEscaped4, testParseStr, testParseStrLeft,
getInterpTests ]
runUnit = runTestTT tests
-- # ENDTESTS # --