{-# LANGUAGE OverloadedStrings #-} module HieDb.Html ( Color (..) , Span (..) , generate ) where import Control.Monad (forM_) import Data.Function (on) import Data.IntMap.Strict (IntMap) import qualified Data.IntMap.Strict as IM import Data.List (foldl', sortBy) import Data.Text (Text) import qualified Data.Text as T import Lucid import HieDb.Compat generate :: FilePath -> ModuleName -> [Text] -> [Span] -> IO () generate fp mn ts sps = renderToFile fp $ doctypehtml_ $ do head_ $ title_ $ toHtml $ moduleNameString mn body_ $ forM_ (layout ts sps) generateLine' where generateLine' :: (Int, Text, [LineSpan]) -> Html () generateLine' (i, t, lsps) = pre_ [style_ "margin:0em;font-size:large"] $ do span_ [style_ "background-color:lightcyan;padding-right:1em"] $ padLineNumber i go 1 t lsps go :: Int -> Text -> [LineSpan] -> Html () go _ t [] = toHtml t go col t lsps@(lsp : lsps') | col < lspStartColumn lsp = do let (t1, t2) = T.splitAt (lspStartColumn lsp - col) t toHtml t1 go (lspStartColumn lsp) t2 lsps | otherwise = do let l = lspEndColumn lsp - lspStartColumn lsp + 1 (t1, t2) = T.splitAt l t span_ [lineSpanAttribute lsp] $ toHtml t1 go (lspEndColumn lsp + 1) t2 lsps' padLineNumber :: Int -> Html () padLineNumber n = let s = show n in go s $ length s where go s l | l >= 6 = toHtml s | otherwise = go (' ' : s) (l + 1) data Color = Reachable | Unreachable deriving (Show, Read, Eq, Ord) data Span = Span { spStartLine :: !Int , spStartColumn :: !Int , spEndLine :: !Int , spEndColumn :: !Int , spColor :: !Color } deriving (Show, Read, Eq, Ord) data LineSpan = LineSpan { lspLine :: !Int , lspStartColumn :: !Int , lspEndColumn :: !Int , lspColor :: !Color } deriving (Show, Read, Eq, Ord) lineSpanAttribute :: LineSpan -> Attribute lineSpanAttribute lsp = let color = case lspColor lsp of Reachable -> "lightgreen" Unreachable -> "yellow" in style_ $ "background-color:" <> color lineSpans :: (Int -> Int) -> Span -> [LineSpan] lineSpans cols sp | spStartLine sp == spEndLine sp = return LineSpan { lspLine = spStartLine sp , lspStartColumn = spStartColumn sp , lspEndColumn = spEndColumn sp , lspColor = spColor sp } | otherwise = let lsp1 = LineSpan { lspLine = spStartLine sp , lspStartColumn = spStartColumn sp , lspEndColumn = cols $ spStartLine sp , lspColor = spColor sp } lsp i = LineSpan { lspLine = i , lspStartColumn = 1 , lspEndColumn = cols i , lspColor = spColor sp } lsp2 = LineSpan { lspLine = spEndLine sp , lspStartColumn = 1 , lspEndColumn = spEndColumn sp , lspColor = spColor sp } in lsp1 : [lsp i | i <- [spStartLine sp + 1 .. spEndLine sp - 1]] ++ [lsp2] layout :: [Text] -> [Span] -> [(Int, Text, [LineSpan])] layout ts ss = let m1 = IM.fromList [(i, (t, T.length t, [])) | (i, t) <- zip [1..] ts] m2 = foldl' f m1 ss :: IntMap (Text, Int, [LineSpan]) in [(i, t, lsps) | (i, (t, lsps)) <- IM.toList $ j <$> m2] where f :: IntMap (Text, Int, [LineSpan]) -> Span -> IntMap (Text, Int, [LineSpan]) f m = foldl' g m . lineSpans lookup' where lookup' i = case IM.lookup i m of Nothing -> 0 Just (_, l, _) -> l g :: IntMap (Text, Int, [LineSpan]) -> LineSpan -> IntMap (Text, Int, [LineSpan]) g m lsp = IM.adjust (h lsp) (lspLine lsp) m h :: LineSpan -> (Text, Int, [LineSpan]) -> (Text, Int, [LineSpan]) h lsp (t, l, lsps) = (t, l, lsp : lsps) j :: (Text, Int, [LineSpan]) -> (Text, [LineSpan]) j (t, _, lsps) = (t, sortBy (compare `on` lspStartColumn) lsps)