1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332
|
{-# LANGUAGE FlexibleInstances, OverloadedStrings, TypeSynonymInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{- |
Module : Data.GraphViz.Printing
Description : Helper functions for converting to Dot format.
Copyright : (c) Ivan Lazar Miljenovic
License : 3-Clause BSD-style
Maintainer : Ivan.Miljenovic@gmail.com
This module defines simple helper functions for use with
"Text.PrettyPrint". It also re-exports all the pretty-printing
combinators from that module.
Note that the 'PrintDot' instances for 'Bool', etc. match those
specified for use with Graphviz.
You should only be using this module if you are writing custom node
types for use with "Data.GraphViz.Types". For actual printing of
code, use @'Data.GraphViz.Types.printDotGraph'@ (which produces a
'Text' value).
The Dot language specification specifies that any identifier is in
one of four forms:
* Any string of alphabetic ([a-zA-Z\\200-\\377]) characters,
underscores ('_') or digits ([0-9]), not beginning with a
digit;
* a number [-]?(.[0-9]+ | [0-9]+(.[0-9]*)? );
* any double-quoted string (\"...\") possibly containing
escaped quotes (\\\");
* an HTML string (\<...\>).
(Note that the first restriction is referring to a byte-by-byte
comparison using octal values; when using UTF-8 this corresponds to
all characters @c@ where @ord c >= 128@.)
Due to these restrictions, you should only use 'text' when you are
sure that the 'Text' in question is static and quotes are
definitely needed/unneeded; it is better to use the 'Text'
instance for 'PrintDot'. For more information, see the
specification page:
<http://graphviz.org/doc/info/lang.html>
-}
module Data.GraphViz.Printing
( module Text.PrettyPrint.Leijen.Text.Monadic
, DotCode
, renderDot -- Exported for Data.GraphViz.Types.Internal.Common.printSGID
, PrintDot(..)
, unqtText
, dotText
, printIt
, addQuotes
, unqtEscaped
, printEscaped
, wrap
, commaDel
, printField
, angled
, fslash
, printColorScheme
) where
import Data.GraphViz.Internal.State
import Data.GraphViz.Internal.Util
-- To avoid orphan instances and cyclic imports
import Data.GraphViz.Attributes.ColorScheme
-- Only implicitly import and re-export combinators.
import Data.Text.Lazy (Text)
import qualified Data.Text.Lazy as T
import Text.PrettyPrint.Leijen.Text.Monadic hiding (Pretty (..),
SimpleDoc (..), bool,
displayIO, displayT,
hPutDoc, putDoc,
renderCompact,
renderPretty, string,
width, (<$>))
import qualified Text.PrettyPrint.Leijen.Text.Monadic as PP
import Control.Monad (ap, when)
import Control.Monad.Trans.State
import Data.Char (toLower)
import qualified Data.Set as Set
import Data.Version (Version (..))
import Data.Word (Word16, Word8)
-- -----------------------------------------------------------------------------
-- | A type alias to indicate what is being produced.
type DotCode = State GraphvizState Doc
instance Show DotCode where
showsPrec d = showsPrec d . renderDot
-- | Correctly render Graphviz output.
renderDot :: DotCode -> Text
renderDot = PP.displayT . PP.renderPretty 0.4 80
. (`evalState` initialState)
-- | A class used to correctly print parts of the Graphviz Dot language.
-- Minimal implementation is 'unqtDot'.
class PrintDot a where
-- | The unquoted representation, for use when composing values to
-- produce a larger printing value.
unqtDot :: a -> DotCode
-- | The actual quoted representation; this should be quoted if it
-- contains characters not permitted a plain ID String, a number
-- or it is not an HTML string. Defaults to 'unqtDot'.
toDot :: a -> DotCode
toDot = unqtDot
-- | The correct way of representing a list of this value when
-- printed; not all Dot values require this to be implemented.
-- Defaults to Haskell-like list representation.
unqtListToDot :: [a] -> DotCode
unqtListToDot = list . mapM unqtDot
-- | The quoted form of 'unqtListToDot'; defaults to wrapping double
-- quotes around the result of 'unqtListToDot' (since the default
-- implementation has characters that must be quoted).
listToDot :: [a] -> DotCode
listToDot = dquotes . unqtListToDot
-- | Convert to DotCode; note that this has no indentation, as we can
-- only have one of indentation and (possibly) infinite line lengths.
printIt :: (PrintDot a) => a -> Text
printIt = renderDot . toDot
instance PrintDot Int where
unqtDot = int
instance PrintDot Integer where
unqtDot = text . T.pack . show
instance PrintDot Word8 where
unqtDot = int . fromIntegral
instance PrintDot Word16 where
unqtDot = int . fromIntegral
instance PrintDot Double where
-- If it's an "integral" double, then print as an integer. This
-- seems to match how Graphviz apps use Dot.
unqtDot d = if d == fromIntegral di
then int di
else double d
where
di = round d
toDot d = if any ((==) 'e' . toLower) $ show d
then dquotes ud
else ud
where
ud = unqtDot d
unqtListToDot = hcat . punctuate colon . mapM unqtDot
listToDot [d] = toDot d
listToDot ds = dquotes $ unqtListToDot ds
instance PrintDot Bool where
unqtDot True = text "true"
unqtDot False = text "false"
instance PrintDot Char where
unqtDot = char
toDot = qtChar
unqtListToDot = unqtDot . T.pack
listToDot = toDot . T.pack
-- | Ignores 'versionTags' and assumes 'not . null . versionBranch'
-- (usually you want 'length . versionBranch == 2').
instance PrintDot Version where
unqtDot = hcat . punctuate dot . mapM int . versionBranch
toDot v = bool id dquotes (not . null . drop 2 . versionBranch $ v)
$ unqtDot v
instance PrintDot Text where
unqtDot = unqtString
toDot = qtString
-- | For use with @OverloadedStrings@ to avoid ambiguous type variable errors.
unqtText :: Text -> DotCode
unqtText = unqtDot
-- | For use with @OverloadedStrings@ to avoid ambiguous type variable errors.
dotText :: Text -> DotCode
dotText = toDot
-- | Check to see if this 'Char' needs to be quoted or not.
qtChar :: Char -> DotCode
qtChar c
| restIDString c = char c -- Could be a number as well.
| otherwise = dquotes $ char c
needsQuotes :: Text -> Bool
needsQuotes str
| T.null str = True
| isKeyword str = True
| isIDString str = False
| isNumString str = False
| otherwise = True
addQuotes :: Text -> DotCode -> DotCode
addQuotes = bool id dquotes . needsQuotes
-- | Escape quotes in Strings that need them.
unqtString :: Text -> DotCode
unqtString "" = empty
unqtString str = unqtEscaped [] str -- no quotes? no worries!
-- | Escape quotes and quote Texts that need them (including keywords).
qtString :: Text -> DotCode
qtString = printEscaped []
instance (PrintDot a) => PrintDot [a] where
unqtDot = unqtListToDot
toDot = listToDot
wrap :: DotCode -> DotCode -> DotCode -> DotCode
wrap b a d = b <> d <> a
commaDel :: (PrintDot a, PrintDot b) => a -> b -> DotCode
commaDel a b = unqtDot a <> comma <> unqtDot b
printField :: (PrintDot a) => Text -> a -> DotCode
printField f v = text f <> equals <> toDot v
-- | Escape the specified chars as well as @\"@.
unqtEscaped :: [Char] -> Text -> DotCode
unqtEscaped cs = text . addEscapes cs
-- | Escape the specified chars as well as @\"@ and then wrap the
-- result in quotes.
printEscaped :: [Char] -> Text -> DotCode
printEscaped cs str = addQuotes str' $ text str'
where
str' = addEscapes cs str
-- | Ensure the provided characters are all escaped. Note that we
-- cannot convert to 'DotCode' immediately because 'printEscaped'
-- needs to pass the result from this to 'addQuotes' to determine if
-- it needs to be quoted or not.
addEscapes :: [Char] -> Text -> Text
addEscapes cs = foldr escape T.empty . withNext
where
cs' = Set.fromList $ quote : slash : cs
slash = '\\'
quote = '"'
escape (c,c') str
| c == slash && c' `Set.member` escLetters = c `T.cons` str
| c `Set.member` cs' = slash `T.cons` (c `T.cons` str)
| c == '\n' = slash `T.cons` ('n' `T.cons` str)
| otherwise = c `T.cons` str
-- When a slash precedes one of these characters, don't escape the slash.
escLetters = Set.fromList ['N', 'G', 'E', 'T', 'H', 'L', 'n', 'l', 'r']
-- Need to check subsequent characters when escaping slashes, but
-- don't want to lose the last character when zipping, so append a space.
withNext "" = []
withNext str = T.zip `ap` ((`T.snoc` ' ') . T.tail) $ str
angled :: DotCode -> DotCode
angled = wrap langle rangle
fslash :: DotCode
fslash = char '/'
-- -----------------------------------------------------------------------------
-- These instances are defined here to avoid cyclic imports and orphan instances
instance PrintDot ColorScheme where
unqtDot = printColorScheme True
printColorScheme :: Bool -> ColorScheme -> DotCode
printColorScheme scs cs = do when scs $ setColorScheme cs
case cs of
X11 -> unqtText "X11"
SVG -> unqtText "svg"
Brewer bs -> unqtDot bs
instance PrintDot BrewerScheme where
unqtDot (BScheme n l) = unqtDot n <> unqtDot l
instance PrintDot BrewerName where
unqtDot Accent = unqtText "accent"
unqtDot Blues = unqtText "blues"
unqtDot Brbg = unqtText "brbg"
unqtDot Bugn = unqtText "bugn"
unqtDot Bupu = unqtText "bupu"
unqtDot Dark2 = unqtText "dark2"
unqtDot Gnbu = unqtText "gnbu"
unqtDot Greens = unqtText "greens"
unqtDot Greys = unqtText "greys"
unqtDot Oranges = unqtText "oranges"
unqtDot Orrd = unqtText "orrd"
unqtDot Paired = unqtText "paired"
unqtDot Pastel1 = unqtText "pastel1"
unqtDot Pastel2 = unqtText "pastel2"
unqtDot Piyg = unqtText "piyg"
unqtDot Prgn = unqtText "prgn"
unqtDot Pubu = unqtText "pubu"
unqtDot Pubugn = unqtText "pubugn"
unqtDot Puor = unqtText "puor"
unqtDot Purd = unqtText "purd"
unqtDot Purples = unqtText "purples"
unqtDot Rdbu = unqtText "rdbu"
unqtDot Rdgy = unqtText "rdgy"
unqtDot Rdpu = unqtText "rdpu"
unqtDot Rdylbu = unqtText "rdylbu"
unqtDot Rdylgn = unqtText "rdylgn"
unqtDot Reds = unqtText "reds"
unqtDot Set1 = unqtText "set1"
unqtDot Set2 = unqtText "set2"
unqtDot Set3 = unqtText "set3"
unqtDot Spectral = unqtText "spectral"
unqtDot Ylgn = unqtText "ylgn"
unqtDot Ylgnbu = unqtText "ylgnbu"
unqtDot Ylorbr = unqtText "ylorbr"
unqtDot Ylorrd = unqtText "ylorrd"
|