packages feed

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

module Text.Dot.Render
  ( graphT
  , graph
  , digraphT
  , digraph
  , strictGraphT
  , strictGraph
  , strictDigraphT
  , strictDigraph
  ) where

import "this" Prelude

import Control.Lens
import Data.HashMap.Strict qualified as M
import Data.HashSet        qualified as S
import Data.List.NonEmpty  qualified as NE
import Text.Printf
import TextBuilder         (TextBuilder)
import TextBuilder         qualified as TB

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 'TextBuilder'.
--
-- The result of the graph building expression itself is ignored.
graphT :: Monad m => DotT m a -> m TextBuilder
graphT = render "graph" "--"

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

-- | 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 'TextBuilder'.
--
-- The result of the graph building expression itself is ignored.
digraphT :: Monad m => DotT m a -> m TextBuilder
digraphT = render "digraph" "->"

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

-- | 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 'TextBuilder'.
--
-- The result of the graph building expression itself is ignored.
strictGraphT :: Monad m => DotT m a -> m TextBuilder
strictGraphT = render "strict graph" "--"

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

-- | 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 'TextBuilder'.
--
-- The result of the graph building expression itself is ignored.
strictDigraphT :: Monad m => DotT m a -> m TextBuilder
strictDigraphT = render "strict digraph" "->"

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


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

render :: Monad m => TextBuilder -> TextBuilder -> DotT m a -> m TextBuilder
render gtype arrow action = do
  allGraph <- run action
  pure $ TB.intercalate "\n" $ visit allGraph gtype arrow

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

visit :: DotGraph -> TextBuilder -> TextBuilder -> [TextBuilder]
visit DotGraph {..} gtype arrow = evalState visitGraph initialRenderState
  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

    retrieveEdgeNode = \case
      KnownNode e -> e
      UnknownNode _ -> error "Text.Dot.Render.visit: found unresolved edge name"

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

    visitAttributes =
      map visitAttribute . M.toList

    visitEntity e = do
      let attrs = fromMaybe mempty $ M.lookup e _entityAttributes
      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 =
      fmap concat . traverse visitEntity . reverse

    visitNode e attrs = do
      knownNodes %= S.insert e
      pure $ pure $ mconcat
        [ renderIndex e
        , " ["
        , TB.intercalate "," $ visitAttributes attrs
        , "]"
        ]

    visitEdge _ attrs (EdgeInfo o1 o2 p1 p2) = do
      let
        e1 = retrieveEdgeNode o1
        e2 = retrieveEdgeNode o2
        result = pure $ mconcat
          [ renderIndex (fromMaybe e1 p1)
          , " "
          , arrow
          , " "
          , renderIndex (fromMaybe e2 p2)
          , " ["
          , TB.intercalate "," $ visitAttributes $ attrs
            <> M.fromList [("ltail", TB.toText $ renderIndex e1) | _ <- toList p1]
            <> M.fromList [("lhead", TB.toText $ renderIndex e2) | _ <- toList p2]
          , "]"
          ]
      knownCache <- use knownNodes
      let allKnown = and do
            Just n <- [Just e1, Just e2, p1, p2]
            pure $ S.member n knownCache
      if allKnown
      then
        pure result
      else do
        delayedEdges <>:= result
        pure []

    visitGraph = visitInner
      gtype
      (uses delayedEdges reverse)
      (fromMaybe mempty $ M.lookup rootGraph _entityAttributes)
      (NE.head _contextStack)

    visitSubgraph e = visitInner
      ("subgraph " <> renderIndex e)
      (pure [])

    visitInner etype edgeAction attrs entities = do
      innerEntities <- visitEntities entities
      extraEdges <- edgeAction
      pure $ concat
        [ [etype <> " {"]
        , indent $ map (<> ";") $ visitAttributes attrs
        , indent innerEntities
        , indent extraEdges
        , ["}"]
        ]