-- | Formats Haskell source code as HTML with CSS and Mouseover Type Annotations
module Language.Haskell.Liquid.UX.ACSS (
hscolour
, hsannot
, AnnMap (..)
, breakS
, srcModuleName
, Status (..)
, tokeniseWithLoc
) where
import Prelude hiding (error)
import qualified Language.Haskell.Liquid.GHC.API as SrcLoc
import Language.Haskell.HsColour.Anchors
import Language.Haskell.HsColour.Classify as Classify
import Language.Haskell.HsColour.HTML (renderAnchors, escape)
import qualified Language.Haskell.HsColour.CSS as CSS
import Data.Either (partitionEithers)
import Data.Maybe (fromMaybe)
import qualified Data.HashMap.Strict as M
import Data.List (find, isPrefixOf, findIndex, elemIndices, intercalate)
import Data.Char (isSpace)
import Text.Printf
import Language.Haskell.Liquid.GHC.Misc
import Language.Haskell.Liquid.Types.Errors (panic, impossible)
data AnnMap = Ann
{ types :: M.HashMap Loc (String, String) -- ^ Loc -> (Var, Type)
, errors :: [(Loc, Loc, String)] -- ^ List of error intervals
, status :: !Status
, sptypes :: ![(SrcLoc.RealSrcSpan, (String, String)) ]-- ^ Type information with spans
}
data Status = Safe | Unsafe | Error | Crash
deriving (Eq, Ord, Show)
data Annotation = A {
typ :: Maybe String -- ^ type string
, err :: Maybe String -- ^ error string
, lin :: Maybe (Int, Int) -- ^ line number, total width of lines i.e. max (length (show lineNum))
} deriving (Show)
-- | Formats Haskell source code using HTML and mouse-over annotations
hscolour :: Bool -- ^ Whether to include anchors.
-> Bool -- ^ Whether input document is literate haskell or not
-> String -- ^ Haskell source code, Annotations as comments at end
-> String -- ^ Coloured Haskell source code.
hscolour anchor lhs = hsannot anchor Nothing lhs . splitSrcAndAnns
type CommentTransform = Maybe (String -> [(TokenType, String)])
-- | Formats Haskell source code using HTML and mouse-over annotations
hsannot :: Bool -- ^ Whether to include anchors.
-> CommentTransform -- ^ Function to refine comment tokens
-> Bool -- ^ Whether input document is literate haskell or not
-> (String, AnnMap) -- ^ Haskell Source, Annotations
-> String -- ^ Coloured Haskell source code.
hsannot anchor tx False z = hsannot' Nothing anchor tx z
hsannot anchor tx True (s, m) = concatMap chunk $ litSpans $ joinL $ classify $ inlines s
where chunk (Code c, l) = hsannot' (Just l) anchor tx (c, m)
chunk (Lit c , _) = c
litSpans :: [Lit] -> [(Lit, Loc)]
litSpans lits = zip lits $ spans lits
where spans = tokenSpans Nothing . map unL
hsannot' :: Maybe Loc
-> Bool -> CommentTransform -> (String, AnnMap) -> String
hsannot' baseLoc anchor tx =
CSS.pre
. (if anchor then concatMap (renderAnchors renderAnnotToken)
. insertAnnotAnchors
else concatMap renderAnnotToken)
. annotTokenise baseLoc tx
tokeniseWithLoc :: CommentTransform -> String -> [(TokenType, String, Loc)]
tokeniseWithLoc tx str = zipWith (\(x,y) z -> (x, y, z)) toks spans
where
toks = tokeniseWithCommentTransform tx str
spans = tokenSpans Nothing $ map snd toks
-- | annotTokenise is absurdly slow: O(#tokens x #errors)
annotTokenise :: Maybe Loc -> CommentTransform -> (String, AnnMap) -> [(TokenType, String, Annotation)]
annotTokenise baseLoc tx (src, annm) = zipWith (\(x,y) z -> (x,y,z)) toks annots
where
toks = tokeniseWithCommentTransform tx src
spans = tokenSpans baseLoc $ map snd toks
annots = fmap (spanAnnot linWidth annm) spans
linWidth = length $ show $ length $ lines src
spanAnnot :: Int -> AnnMap -> Loc -> Annotation
spanAnnot w (Ann ts es _ _) span = A t e b
where
t = fmap snd (M.lookup span ts)
e = fmap (\_ -> "ERROR") $ find (span `inRange`) [(x,y) | (x,y,_) <- es]
b = spanLine w span
spanLine :: t -> Loc -> Maybe (Int, t)
spanLine w (L (l, c))
| c == 1 = Just (l, w)
| otherwise = Nothing
inRange :: Loc -> (Loc, Loc) -> Bool
inRange (L (l0, c0)) (L (l, c), L (l', c'))
= l <= l0 && c <= c0 && l0 <= l' && c0 < c'
tokeniseWithCommentTransform :: Maybe (String -> [(TokenType, String)]) -> String -> [(TokenType, String)]
tokeniseWithCommentTransform Nothing = tokenise
tokeniseWithCommentTransform (Just f) = concatMap (expand f) . tokenise
where expand f (Comment, s) = f s
expand _ z = [z]
tokenSpans :: Maybe Loc -> [String] -> [Loc]
tokenSpans = scanl plusLoc . fromMaybe (L (1, 1))
plusLoc :: Loc -> String -> Loc
plusLoc (L (l, c)) s
= case '\n' `elemIndices` s of
[] -> L (l, (c + n))
is -> L ((l + length is), (n - maximum is))
where n = length s
renderAnnotToken :: (TokenType, String, Annotation) -> String
renderAnnotToken (x, y, a) = renderLinAnnot (lin a)
$ renderErrAnnot (err a)
$ renderTypAnnot (typ a)
$ CSS.renderToken (x, y)
renderTypAnnot :: (PrintfArg t, PrintfType t) => Maybe String -> t -> t
renderTypAnnot (Just ann) s = printf "%s%s" (escape ann) s
renderTypAnnot Nothing s = s
renderErrAnnot :: (PrintfArg t1, PrintfType t1) => Maybe t -> t1 -> t1
renderErrAnnot (Just _) s = printf "%s" s
renderErrAnnot Nothing s = s
renderLinAnnot :: (Show t, PrintfArg t1, PrintfType t1)
=> Maybe (t, Int) -> t1 -> t1
renderLinAnnot (Just d) s = printf "%s: %s" (lineString d) s
renderLinAnnot Nothing s = s
lineString :: Show t => (t, Int) -> [Char]
lineString (i, w) = (replicate (w - (length is)) ' ') ++ is
where is = show i
{- Example Annotation:
x#agV:Int -> {VV_int:Int | (0 <= VV_int),(x#agV <= VV_int)}
NOWTRYTHIS
-}
insertAnnotAnchors :: [(TokenType, String, a)] -> [Either String (TokenType, String, a)]
insertAnnotAnchors toks
= stitch (zip toks' toks) $ insertAnchors toks'
where toks' = [(x,y) | (x,y,_) <- toks]
stitch :: Eq b => [(b, c)] -> [Either a b] -> [Either a c]
stitch xys ((Left a) : rest)
= (Left a) : stitch xys rest
stitch ((x,y):xys) ((Right x'):rest)
| x == x'
= (Right y) : stitch xys rest
| otherwise
= panic Nothing "stitch"
stitch _ []
= []
stitch _ _
= impossible Nothing "stitch: cannot happen"
splitSrcAndAnns :: String -> (String, AnnMap)
splitSrcAndAnns s =
let ls = lines s in
case findIndex (breakS ==) ls of
Nothing -> (s, Ann M.empty [] Safe mempty)
Just i -> (src, ann)
where (codes, _:mname:annots) = splitAt i ls
ann = annotParse mname $ dropWhile isSpace $ unlines annots
src = unlines codes
srcModuleName :: String -> String
srcModuleName = fromMaybe "Main" . tokenModule . tokenise
tokenModule :: [(TokenType, [Char])] -> Maybe [Char]
tokenModule toks
= do i <- findIndex ((Keyword, "module") ==) toks
let (_, toks') = splitAt (i+2) toks
j <- findIndex ((Space ==) . fst) toks'
let (toks'', _) = splitAt j toks'
return $ concatMap snd toks''
breakS :: [Char]
breakS = "MOUSEOVER ANNOTATIONS"
annotParse :: String -> String -> AnnMap
annotParse mname s = Ann (M.fromList ts) [(x,y,"") | (x,y) <- es] Safe mempty
where
(ts, es) = partitionEithers $ parseLines mname 0 $ lines s
parseLines :: [Char]
-> Int
-> [[Char]]
-> [Either (Loc, ([Char], [Char])) (Loc, Loc)]
parseLines _ _ []
= []
parseLines mname i ("":ls)
= parseLines mname (i+1) ls
parseLines mname i (_:_:l:c:"0":l':c':rest')
= Right (L (line, col), L (line', col')) : parseLines mname (i + 7) rest'
where line = (read l) :: Int
col = (read c) :: Int
line' = (read l') :: Int
col' = (read c') :: Int
parseLines mname i (x:f:l:c:n:rest)
| f /= mname
= parseLines mname (i + 5 + num) rest'
| otherwise
= Left (L (line, col), (x, anns)) : parseLines mname (i + 5 + num) rest'
where line = (read l) :: Int
col = (read c) :: Int
num = (read n) :: Int
anns = intercalate "\n" $ take num rest
rest' = drop num rest
parseLines _ i _
= panic Nothing $ "Error Parsing Annot Input on Line: " ++ show i
instance Show AnnMap where
show (Ann ts es _ _) = "\n\n"
++ (concatMap ppAnnotTyp $ M.toList ts)
++ (concatMap ppAnnotErr [(x,y) | (x,y,_) <- es])
ppAnnotTyp :: (PrintfArg t, PrintfType t1) => (Loc, (t, String)) -> t1
ppAnnotTyp (L (l, c), (x, s)) = printf "%s\n%d\n%d\n%d\n%s\n\n\n" x l c (length $ lines s) s
ppAnnotErr :: PrintfType t => (Loc, Loc) -> t
ppAnnotErr (L (l, c), L (l', c')) = printf " \n%d\n%d\n0\n%d\n%d\n\n\n\n" l c l' c'
---------------------------------------------------------------------------------
---- Code for Dealing With LHS, stolen from Language.Haskell.HsColour.HsColour --
---------------------------------------------------------------------------------
-- | Separating literate files into code\/comment chunks.
data Lit = Code {unL :: String} | Lit {unL :: String} deriving (Show)
-- Re-implementation of 'lines', for better efficiency (but decreased laziness).
-- Also, importantly, accepts non-standard DOS and Mac line ending characters.
-- And retains the trailing '\n' character in each resultant string.
inlines :: String -> [String]
inlines s = lines' s id
where
lines' [] acc = [acc []]
lines' ('\^M':'\n':s) acc = acc ['\n'] : lines' s id -- DOS
lines' ('\n':s) acc = acc ['\n'] : lines' s id -- Unix
lines' (c:s) acc = lines' s (acc . (c:))
-- | The code for classify is largely stolen from Language.Preprocessor.Unlit.
classify :: [String] -> [Lit]
classify [] = []
classify (x:xs) | "\\begin{code}"`isPrefixOf`x
= Lit x: allProg "code" xs
classify (x:xs) | "\\begin{spec}"`isPrefixOf`x
= Lit x: allProg "spec" xs
classify (('>':x):xs) = Code ('>':x) : classify xs
classify (x:xs) = Lit x: classify xs
allProg :: [Char] -> [[Char]] -> [Lit]
allProg name = go
where
end = "\\end{" ++ name ++ "}"
go [] = [] -- Should give an error message,
-- but I have no good position information.
go (x:xs) | end `isPrefixOf `x
= Lit x: classify xs
go (x:xs) = Code x: go xs
-- | Join up chunks of code\/comment that are next to each other.
joinL :: [Lit] -> [Lit]
joinL [] = []
joinL (Code c:Code c2:xs) = joinL (Code (c++c2):xs)
joinL (Lit c :Lit c2 :xs) = joinL (Lit (c++c2):xs)
joinL (any:xs) = any: joinL xs