packages feed

graphwiz-1.0.0: src/Text/Dot/Render.hs

module Text.Dot.Render
  ( graphWithT
  , graphT
  , graphWith
  , graph
  , digraphWithT
  , digraphT
  , digraphWith
  , digraph
  , strictGraphWithT
  , strictGraphT
  , strictGraphWith
  , strictGraph
  , strictDigraphWithT
  , strictDigraphT
  , strictDigraphWith
  , strictDigraph
  ) where

import "this" Prelude

import Data.HashMap.Strict qualified as M
import Data.List.NonEmpty  qualified as NE
import Text.Builder        (Builder)
import Text.Builder        qualified as TB
import Text.Printf

import Text.Dot.Monad
import Text.Dot.Types


--------------------------------------------------------------------------------
-- Render functions

-- | Renders a given graph.
--
-- Given a t'DotT' expression that builds a graph, this function evaluates it
-- and builds an undirected non-strict graph. It returns the result in the
-- underlying monad, as a 'Builder'. The callback takes the graph's identifier
-- as argument.
--
-- The result of the graph building expression itself is ignored.
graphWithT :: Monad m => (Entity -> DotT m a) -> m Builder
graphWithT = render "graph" "--"

-- | Renders a given graph.
--
-- Like 'graphWithT', but the expression doesn't take the identifier as agument.
graphT :: Monad m => DotT m a -> m Builder
graphT = render "graph" "--" . const

-- | Renders a given graph.
--
-- Like 'graphWithT', but in the 'Dot' monad.
graphWith :: (Entity -> Dot a) -> Builder
graphWith = runIdentity . render "graph" "--"

-- | Renders a given graph.
--
-- Like 'graphT', but in the 'Dot' monad.
graph :: Dot a -> Builder
graph = runIdentity . render "graph" "--" . const

-- | Renders a given graph.
--
-- Given a t'DotT' expression that builds a graph, this function evaluates it
-- and builds a directed non-strict graph. It returns the result in the
-- underlying monad, as a 'Builder'. The callback takes the graph's identifier
-- as argument.
--
-- The result of the graph building expression itself is ignored.
digraphWithT :: Monad m => (Entity -> DotT m a) -> m Builder
digraphWithT = render "digraph" "->"

-- | Renders a given graph.
--
-- Like 'digraphWithT', but the expression doesn't take the entity as agument.
digraphT :: Monad m => DotT m a -> m Builder
digraphT = render "digraph" "->" . const

-- | Renders a given graph.
--
-- Like 'digraphWithT', but in the 'Dot' monad.
digraphWith :: (Entity -> Dot a) -> Builder
digraphWith = runIdentity . render "digraph" "->"

-- | Renders a given graph.
--
-- Like 'digraphT', but in the 'Dot' monad.
digraph :: Dot a -> Builder
digraph = runIdentity . render "digraph" "->" . const

-- | Renders a given graph.
--
-- Given a t'DotT' expression that builds a graph, this function evaluates it
-- and builds an undirected strict graph. It returns the result in the
-- underlying monad, as a 'Builder'. The callback takes the graph's identifier
-- as argument.
--
-- The result of the graph building expression itself is ignored.
strictGraphWithT :: Monad m => (Entity -> DotT m a) -> m Builder
strictGraphWithT = render "strict graph" "--"

-- | Renders a given graph.
--
-- Like 'strictGraphWithT', but the expression doesn't take the entity as agument.
strictGraphT :: Monad m => DotT m a -> m Builder
strictGraphT = render "strict graph" "--" . const

-- | Renders a given graph.
--
-- Like 'strictGraphWithT', but in the 'Dot' monad.
strictGraphWith :: (Entity -> Dot a) -> Builder
strictGraphWith = runIdentity . render "strict graph" "--"

-- | Renders a given graph.
--
-- Like 'strictGraphT', but in the 'Dot' monad.
strictGraph :: Dot a -> Builder
strictGraph = runIdentity . render "strict graph" "--" . const

-- | Renders a given graph.
--
-- Given a t'DotT' expression that builds a graph, this function evaluates it
-- and builds a directed strict graph. It returns the result in the underlying
-- monad, as a 'Builder'. The callback takes the graph's identifier as argument.
--
-- The result of the graph building expression itself is ignored.
strictDigraphWithT :: Monad m => (Entity -> DotT m a) -> m Builder
strictDigraphWithT = render "strict digraph" "->"

-- | Renders a given graph.
--
-- Like 'strictDigraphWithT', but the expression doesn't take the entity as agument.
strictDigraphT :: Monad m => DotT m a -> m Builder
strictDigraphT = render "strict digraph" "->" . const

-- | Renders a given graph.
--
-- Like 'strictDigraphWithT', but in the 'Dot' monad.
strictDigraphWith :: (Entity -> Dot a) -> Builder
strictDigraphWith = runIdentity . render "strict digraph" "->"

-- | Renders a given graph.
--
-- Like 'strictDigraphT', but in the 'Dot' monad.
strictDigraph :: Dot a -> Builder
strictDigraph = runIdentity . render "strict digraph" "->" . const


--------------------------------------------------------------------------------
-- Internal helpers

render :: Monad m => Builder -> Builder -> (Entity -> DotT m a) -> m Builder
render gtype arrow f = do
  let root = Entity Subgraph (-1)
  allGraph <- run root (f root)
  pure $ TB.intercalate "\n" $ visit allGraph gtype arrow root

indent :: [Builder] -> [Builder]
indent = map ("  " <>)

visit :: DotGraph -> Builder -> Builder -> Entity -> [Builder]
visit DotGraph {..} gtype arrow = visitGraph
  where
    magnitude = ceiling (logBase 10 (fromIntegral _entityIndex :: Double)) :: Int
    intFormat = mconcat ["%0", show magnitude, "d"]

    renderIndex (Entity t i) =
      let prefix = case t of
            Subgraph -> "subgraph"
            Cluster  -> "cluster"
            Node     -> "node"
            Edge     -> error "Text.Dot.Render.visit: tried to render an edge id"
          suffix = TB.string $ printf intFormat i
      in prefix <> suffix

    visitAttribute (name, value) =
      mconcat [TB.text name, "=\"", TB.text value, "\""]

    visitAttributes =
      map visitAttribute . M.toList

    visitEntity e =
      let attrs = fromMaybe mempty $ M.lookup e _entityAttributes
      in  indent $ case getType e of
                     Node     -> visitNode     e attrs
                     Edge     -> visitEdge     e attrs (_edgeInfo     M.! e)
                     Cluster  -> visitSubgraph e attrs (_subgraphInfo M.! e)
                     Subgraph -> visitSubgraph e attrs (_subgraphInfo M.! e)

    visitEntities =
      concatMap visitEntity . reverse

    visitNode e attrs =
      pure $ mconcat
        [ renderIndex e
        , " ["
        , TB.intercalate "," $ visitAttributes attrs
        , "]"
        ]

    visitEdge _ attrs (EdgeInfo o1 o2 p1 p2) =
      pure $ mconcat
        [ renderIndex p1
        , " "
        , arrow
        , " "
        , renderIndex p2
        , " ["
        , TB.intercalate "," $ visitAttributes $ attrs
          <> M.fromList [("ltail", TB.run $ renderIndex o1) | getType o1 == Cluster]
          <> M.fromList [("lhead", TB.run $ renderIndex o2) | getType o2 == Cluster]
        , "]"
        ]

    visitGraph e =
      visitInner gtype e (fromMaybe mempty $ M.lookup e _entityAttributes) (NE.head _contextStack)

    visitSubgraph e =
      visitInner ("subgraph " <> renderIndex e) e

    visitInner etype _ attrs entities = concat
      [ [etype <> " {"]
      , indent $ map (<> ";") $ visitAttributes attrs
      , visitEntities entities
      , ["}"]
      ]