module Diagrams.Haddock
(
DiagramURL(..)
, displayDiagramURL
, parseDiagramURL
, parseKeyValPair
, maybeParseDiagramURL
, parseDiagramURLs
, displayDiagramURLs
, getDiagramNames
, coalesceComments
, CodeBlock(..)
, codeBlockCode, codeBlockIdents, codeBlockBindings
, makeCodeBlock
, collectBindings
, extractCodeBlocks
, parseCodeBlocks
, transitiveClosure
, compileDiagram
, compileDiagrams
, processHaddockDiagrams
, processHaddockDiagrams'
, showParseFailure
, CollectErrors(..)
, failWith
, runCE
) where
import Control.Arrow (first, (&&&), (***))
import Control.Lens (makeLenses, orOf, view, (%%~),
(%~), (&), (.~), (^.), _2, _Right)
import Control.Monad.Writer
import qualified Data.ByteString.Base64.Lazy as BS64
import qualified Data.ByteString.Lazy as BS
import qualified Data.ByteString.Lazy.Char8 as BS8
import Data.Char (isSpace)
import Data.Either (lefts, rights)
import Data.Function (on)
import Data.Generics.Uniplate.Data (universeBi)
import Data.List (groupBy, intercalate, isPrefixOf,
partition)
import Data.List.Split (dropBlanks, dropDelims, split,
whenElt)
import qualified Data.Map as M
import Data.Maybe (catMaybes, mapMaybe)
import qualified Data.Set as S
import qualified Data.Text.Lazy as T
import qualified Data.Text.Lazy.Encoding as T
import qualified Graphics.Svg as G
import Language.Haskell.Exts hiding (loc)
import qualified Language.Haskell.Exts as HSE
import Language.Preprocessor.Cpphs
import System.Console.ANSI (setCursorColumn)
import System.Directory (copyFile,
createDirectoryIfMissing,
doesFileExist)
import System.FilePath (dropExtension, normalise,
splitDirectories, (<.>), (</>))
import qualified System.IO as IO
import qualified System.IO.Cautious as Cautiously
import qualified System.IO.Strict as Strict
import Text.Parsec
import qualified Text.Parsec as P
import Text.Parsec.String
import Diagrams.Backend.SVG (Options (..), SVG (..))
import qualified Diagrams.Builder as DB
import Diagrams.Prelude (V2, zero)
import Diagrams.TwoD.Size (mkSizeSpec2D)
showParseFailure :: SrcLoc -> String -> String
showParseFailure loc err = unlines [ prettyPrint loc, err ]
newtype CollectErrors a = CE { unCE :: Writer [String] a }
deriving (Functor, Applicative, Monad, MonadWriter [String])
failWith :: String -> CollectErrors (Maybe a)
failWith err = tell [err] >> return Nothing
runCE :: CollectErrors a -> (a, [String])
runCE = runWriter . unCE
data DiagramURL = DiagramURL
{ _diagramURL :: String
, _diagramName :: String
, _diagramOpts :: M.Map String String
}
deriving (Show, Eq)
makeLenses ''DiagramURL
displayDiagramURL :: DiagramURL -> String
displayDiagramURL d = "<<" ++ d ^. diagramURL ++ "#" ++ opts ++ ">>"
where
opts = intercalate "&"
. map displayOpt
. (("diagram", d ^. diagramName) :)
. M.assocs
$ d ^. diagramOpts
displayOpt (k,v) = k ++ "=" ++ v
parseDiagramURL :: Parser DiagramURL
parseDiagramURL =
DiagramURL
<$> (string "<<" *> many (noneOf "#>"))
<*> (char '#' *> string "diagram=" *> many1 (noneOf "&>"))
<*> ((M.fromList <$> many parseKeyValPair) <* string ">>")
parseKeyValPair :: Parser (String,String)
parseKeyValPair =
char '&' *>
((,) <$> (many1 (noneOf "&>=") <* char '=') <*> many1 (noneOf "&>="))
maybeParseDiagramURL :: Parser (Either Char DiagramURL)
maybeParseDiagramURL =
Right <$> try parseDiagramURL
<|> Left <$> anyChar
parseDiagramURLs :: Parser [Either String DiagramURL]
parseDiagramURLs = condenseLefts <$> many maybeParseDiagramURL
where
condenseLefts :: [Either a b] -> [Either [a] b]
condenseLefts [] = []
condenseLefts (Right a : xs) = Right a : condenseLefts xs
condenseLefts xs = Left (lefts ls) : condenseLefts xs'
where (ls,xs') = span isLeft xs
isLeft (Left {}) = True
isLeft _ = False
displayDiagramURLs :: [Either String DiagramURL] -> String
displayDiagramURLs = concatMap (either id displayDiagramURL)
getDiagramNames :: Comment -> S.Set String
getDiagramNames (Comment _ _ s) =
case P.parse parseDiagramURLs "" s of
Left _ -> error "This case can never happen; see prop_parseDiagramURLs_succeeds"
Right urls -> S.fromList . map (view diagramName) . rights $ urls
coalesceComments :: [Comment] -> [(String, Int)]
coalesceComments
= map (unlines . map getComment &&& commentLine . head)
. map (map fst)
. concatMap (groupBy ((==) `on` snd))
. map (zipWith (\i c -> (c, commentLine c i)) [1..])
. concatMap (\xs -> if isMultiLine (head xs) then map (:[]) xs else [xs])
. groupBy ((==) `on` isMultiLine)
where
isMultiLine (Comment b _ _) = b
getComment (Comment _ _ c) = c
commentLine (Comment _ s _) = srcSpanStartLine s
data CodeBlock
= CodeBlock
{ _codeBlockCode :: String
, _codeBlockIdents :: S.Set String
, _codeBlockBindings :: S.Set String
}
deriving (Show, Eq)
makeLenses ''CodeBlock
makeCodeBlock :: FilePath -> (String,Int) -> CollectErrors (Maybe CodeBlock)
makeCodeBlock file (s,l) =
case HSE.parseFileContentsWithMode parseMode s of
ParseOk m -> return . Just $ CodeBlock s
(collectIdents m)
(collectBindings m)
ParseFailed loc err -> failWith . unlines $
[ file ++ ": " ++ show l ++ ":\nWarning: could not parse code block:" ]
++
showBlock s
++
[ "Error was:" ]
++
(indent 2 . lines $ showParseFailure loc err)
where
parseMode = defaultParseMode
{ fixities = Nothing
, baseLanguage = Haskell2010
, extensions = [EnableExtension MultiParamTypeClasses]
}
indent n = map (replicate n ' ' ++)
showBlock b
| length ls > 5 = indent 2 (take 4 ls ++ ["..."])
| otherwise = indent 2 ls
where ls = lines b
collectBindings :: Module l -> S.Set String
collectBindings (Module _ _ _ _ decls) = S.fromList $ mapMaybe getBinding decls
collectBindings _ = S.empty
getBinding :: Decl l -> Maybe String
getBinding (FunBind _ []) = Nothing
getBinding (FunBind _ (Match _ nm _ _ _ : _)) = Just $ getName nm
getBinding (PatBind _ (PVar _ nm) _ _) = Just $ getName nm
getBinding _ = Nothing
getName :: Name l -> String
getName (HSE.Ident _ s) = s
getName (Symbol _ s) = s
getQName :: QName l -> Maybe String
getQName (Qual _ _ n) = Just $ getName n
getQName (UnQual _ n) = Just $ getName n
getQName _ = Nothing
collectIdents :: Module SrcSpanInfo -> S.Set String
collectIdents m = S.fromList . catMaybes $
[ getQName n
| (Var _ n :: Exp SrcSpanInfo) <- universeBi m
]
extractCodeBlocks :: FilePath -> (String,Int) -> CollectErrors [CodeBlock]
extractCodeBlocks file (s,l)
= fmap catMaybes
. mapM (makeCodeBlock file . (unlines***head) . unzip . (map.first) (drop 2 . dropWhile isSpace))
. (split . dropBlanks . dropDelims $ whenElt (not . isBird . fst))
. flip zip [l ..]
. lines
$ s
where
isBird = ((||) <$> (">"==) <*> ("> " `isPrefixOf`)) . dropWhile isSpace
parseCodeBlocks :: FilePath -> String -> CollectErrors (Maybe ([CodeBlock], S.Set String))
parseCodeBlocks file src =
case HSE.parseFileContentsWithComments parseMode src of
ParseFailed loc err -> failWith $ showParseFailure loc err
ParseOk (_, cs) -> do
blocks <- fmap concat
. mapM (extractCodeBlocks file)
. coalesceComments
$ cs
let diaNames = S.unions . map getDiagramNames $ cs
return . Just $ (blocks, diaNames)
where
parseMode = defaultParseMode
{ fixities = Nothing
, parseFilename = file
, baseLanguage = Haskell2010
, extensions = [EnableExtension MultiParamTypeClasses]
}
transitiveClosure :: String -> [CodeBlock] -> [CodeBlock]
transitiveClosure ident = tc [ident]
where
tc _ [] = []
tc [] _ = []
tc (i:is) blocks =
let (ins,outs) = partition (\cb -> i `S.member` (cb ^. codeBlockBindings)) blocks
in ins ++ tc (is ++ concatMap (S.toList . view codeBlockIdents) ins) outs
compileDiagram :: Bool
-> Bool
-> FilePath
-> FilePath
-> FilePath
-> S.Set String
-> [CodeBlock]
-> DiagramURL
-> WriterT [String] IO (DiagramURL, Bool)
compileDiagram quiet dataURIs cacheDir outputDir file ds code url
| (url ^. diagramName) `S.notMember` ds = return (url, False)
| otherwise = do
let outFile = outputDir </>
(munge file ++ "_" ++ (url ^. diagramName)) <.> "svg"
munge = intercalate "_" . splitDirectories . normalise . dropExtension
w, h :: Maybe Double
w = read <$> M.lookup "width" (url ^. diagramOpts)
h = read <$> M.lookup "height" (url ^. diagramOpts)
oldURL = (url, False)
newURL content = (url & diagramURL .~ content, content /= url^.diagramURL)
neededCode = transitiveClosure (url ^. diagramName) code
errHeader = file ++ ": " ++ (url ^. diagramName) ++ ":\n"
res <- liftIO $ do
createDirectoryIfMissing True cacheDir
when (not dataURIs) $ createDirectoryIfMissing True outputDir
logStr $ "[ ] " ++ (url ^. diagramName)
IO.hFlush IO.stdout
let
bopts :: DB.BuildOpts SVG V2 Double
bopts = DB.mkBuildOpts SVG zero (SVGOptions (mkSizeSpec2D w h) Nothing "" [] False)
& DB.snippets .~ map (view codeBlockCode) neededCode
& DB.imports .~ [ "Diagrams.Backend.SVG" ]
& DB.diaExpr .~ (url ^. diagramName)
& DB.decideRegen .~ (DB.hashedRegenerate (\_ opts -> opts) cacheDir)
DB.buildDiagram bopts
case res of
DB.ParseErr err -> do
tell [errHeader ++ "Parse error: " ++ err]
logResult "!"
return oldURL
DB.InterpErr ierr -> do
tell [errHeader ++ "Interpreter error: " ++ DB.ppInterpError ierr]
logResult "!"
return oldURL
DB.Skipped hash -> do
let cached = mkCached (DB.hashToHexStr hash)
when (not dataURIs) $ liftIO $ copyFile cached outFile
logResult "."
if dataURIs
then do
svgBS <- liftIO $ BS.readFile cached
return (newURL (mkDataURI svgBS))
else return (newURL outFile)
DB.OK hash svg -> do
let cached = mkCached (DB.hashToHexStr hash)
svgBS = G.renderBS svg
liftIO $ BS.writeFile cached svgBS
url' <- if dataURIs
then return (newURL (mkDataURI svgBS))
else liftIO (copyFile cached outFile >> return (newURL outFile))
logResult "X"
return url'
where
mkCached base = cacheDir </> base <.> "svg"
mkDataURI svg = "data:image/svg+xml;base64," ++ BS8.unpack (BS64.encode svg)
logStr, logResult :: MonadIO m => String -> m ()
logStr = liftIO . when (not quiet) . putStr
logResult s = liftIO . when (not quiet) $ do
setCursorColumn 1
putStrLn s
compileDiagrams :: Bool
-> Bool
-> FilePath
-> FilePath
-> FilePath
-> S.Set String
-> [CodeBlock]
-> [Either String DiagramURL]
-> WriterT [String] IO ([Either String DiagramURL], Bool)
compileDiagrams quiet dataURIs cacheDir outputDir file ds cs urls = do
urls' <- urls & (traverse . _Right)
%%~ compileDiagram quiet dataURIs cacheDir outputDir file ds cs
let changed = orOf (traverse . _Right . _2) urls'
return (urls' & (traverse . _Right) %~ fst, changed)
processHaddockDiagrams
:: Bool
-> Bool
-> FilePath
-> FilePath
-> FilePath
-> IO [String]
processHaddockDiagrams = processHaddockDiagrams' opts
where
opts = defaultCpphsOptions
{ boolopts = defaultBoolOptions { hashline = False } }
processHaddockDiagrams'
:: CpphsOptions
-> Bool
-> Bool
-> FilePath
-> FilePath
-> FilePath
-> IO [String]
processHaddockDiagrams' opts quiet dataURIs cacheDir outputDir file = do
e <- doesFileExist file
case e of
False -> return ["Error: " ++ file ++ " not found."]
True -> do
h <- IO.openFile file IO.ReadMode
IO.hSetEncoding h IO.utf8
src <- Strict.hGetContents h
r <- go src
case r of
(Nothing, msgs) -> return msgs
(Just (cs, ds), msgs) ->
case P.parse parseDiagramURLs "" src of
Left _ ->
error "This case can never happen; see prop_parseDiagramURLs_succeeds"
Right urls -> do
((urls', changed), msgs2) <- runWriterT $
compileDiagrams quiet dataURIs cacheDir outputDir file ds cs urls
let src' = displayDiagramURLs urls'
when changed $ Cautiously.writeFileL file (T.encodeUtf8 . T.pack $ src')
return (msgs ++ msgs2)
where
go src | needsCPP src = runCpp src >>= return . runCE . parseCodeBlocks file
| otherwise = return $ runCE (parseCodeBlocks file src)
needsCPP src = case readExtensions src of
Just (_, es) | EnableExtension CPP `elem` es -> True
_ -> False
runCpp s = runCpphs opts file s