{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# OPTIONS_GHC -fno-warn-orphans #-} ----------------------------------------------------------------------------- -- | -- Module : Diagrams.Builder.Opts -- Copyright : (c) 2013 diagrams-lib team (see LICENSE) -- License : BSD-style (see LICENSE) -- Maintainer : diagrams-discuss@googlegroups.com -- -- Options for dynamic creation of diagrams. -- ----------------------------------------------------------------------------- module Diagrams.Builder.Opts ( -- * Options Hash , BuildOpts(..), mkBuildOpts, backendOpts, snippets, pragmas, imports, decideRegen, diaExpr, postProcess -- * Rebuilding , alwaysRegenerate, hashedRegenerate, hashToHexStr ) where import Control.Lens (Lens', generateSignatures, lensRules, makeLensesWith, (&), (.~)) import System.Directory (getDirectoryContents) import System.FilePath (takeBaseName) import Text.Printf import Diagrams.Prelude (Diagram, Options) -- | Synonym for more perspicuous types. -- -- We use @Int@ values for hashes because that's what the @Hashable@ -- package uses. Assuming diagram hashes are uniformly distributed, -- on a 64-bit system one needs to build on the order of billions of -- diagrams before the probability of a hash collision exceeds 1/2, -- and for anything up to tens of millions of diagrams the -- probability of a collision is under 0.1%. On 32-bit systems -- those become tens of thousands and thousands, respectively. type Hash = Int -- | Options to control the behavior of @buildDiagram@. Create one -- with 'mkBuildOpts' followed by using the provided lenses to -- override more fields; for example, -- -- @ -- mkBuildOpts SVG zeroV (Options ...) -- & imports .~ [\"Foo.Bar\", \"Baz.Quux\"] -- & diaExpr .~ \"square 6 # fc green\" -- @ data BuildOpts b v = BuildOpts { backendToken :: b -- ^ Backend token , vectorToken :: v -- ^ Dummy vector argument to fix the vector space type , _backendOpts :: Options b v , _snippets :: [String] , _pragmas :: [String] , _imports :: [String] , _decideRegen :: Hash -> IO (Maybe (Options b v -> Options b v)) , _diaExpr :: String , _postProcess :: Diagram b v -> Diagram b v } makeLensesWith (lensRules & generateSignatures .~ False) ''BuildOpts -- | Create a @BuildOpts@ record with default options: -- -- * no snippets -- -- * no pragmas -- -- * no imports -- -- * always regenerate -- -- * the diagram expression @circle 1@ -- -- * no postprocessing mkBuildOpts :: b -> v -> Options b v -> BuildOpts b v mkBuildOpts b v opts = BuildOpts b v opts [] [] [] alwaysRegenerate "circle 1" id -- | Backend-specific options to use. backendOpts :: Lens' (BuildOpts b v) (Options b v) -- | Source code snippets. Each should be a syntactically valid -- Haskell module. They will be combined intelligently, /i.e./ -- not just pasted together textually but combining pragmas, -- imports, /etc./ separately. snippets :: Lens' (BuildOpts b v) [String] -- | Extra @LANGUAGE@ pragmas to use (@NoMonomorphismRestriction@ -- is automatically enabled.) pragmas :: Lens' (BuildOpts b v) [String] -- | Additional module imports (note that "Diagrams.Prelude" is -- automatically imported). imports :: Lens' (BuildOpts b v) [String] -- | A function to decide whether a particular diagram needs to be -- regenerated. It will be passed a hash of the final assembled -- source for the diagram (but with the module name set to @Main@ -- instead of something auto-generated, so that hashing the source -- will produce consistent results across runs), plus any options, -- local imports, and other things which could affect the result of -- rendering. It can return some information (such as a hash of the -- source) via the @x@ result, which will be passed through to the -- result of 'buildDiagram'. More importantly, it decides whether -- the diagram should be built: a result of 'Just' means the diagram -- /should/ be built; 'Nothing' means it should not. In the case -- that it should be built, it returns a function for updating the -- rendering options. This could be used, /e.g./, to request a -- filename based on a hash of the source. -- -- Two standard decision functions are provided for -- convenience: 'alwaysRegenerate' returns no extra information -- and always decides to regenerate the diagram; -- 'hashedRegenerate' creates a hash of the diagram source and -- looks for a file with that name in a given directory. decideRegen :: Lens' (BuildOpts b v) (Hash -> IO (Maybe (Options b v -> Options b v))) -- | The diagram expression to interpret. All the given import sand -- snippets will be in scope, with the given LANGUAGE pragmas -- enabled. The expression may have either of the types @Diagram b -- v@ or @IO (Diagram b v)@. diaExpr :: Lens' (BuildOpts b v) String -- | A function to apply to the interpreted diagram prior to -- rendering. For example, you might wish to apply @pad 1.1 -- . centerXY@. This is preferred over directly modifying the -- string expression to be interpreted, since it gives better -- typechecking, and works no matter whether the expression -- represents a diagram or an IO action. postProcess :: Lens' (BuildOpts b v) (Diagram b v -> Diagram b v) -- | Convenience function suitable to be given as the final argument -- to 'buildDiagram'. It implements the simple policy of always -- rebuilding every diagram. alwaysRegenerate :: Hash -> IO (Maybe (a -> a)) alwaysRegenerate _ = return (Just id) -- | Convenience function suitable to be given as the final argument -- to 'buildDiagram'. It works by converting the hash value to a -- zero-padded hexadecimal string and looking in the specified -- directory for any file whose base name is equal to the hash. If -- there is such a file, it specifies that the diagram should not be -- rebuilt. Otherwise, it specifies that the diagram should be -- rebuilt, and uses the provided function to update the rendering -- options based on the generated hash string. (Most likely, one -- would want to set the requested output file to the hash followed -- by some extension.) hashedRegenerate :: (String -> a -> a) -- ^ A function for computing an update to rendering options, -- given a new base filename computed from a hash of the -- diagram source. -> FilePath -- ^ The directory in which to look for generated files -> Hash -- ^ The hash -> IO (Maybe (a -> a)) hashedRegenerate upd d hash = do let fileBase = hashToHexStr hash files <- getDirectoryContents d case any ((fileBase==) . takeBaseName) files of True -> return Nothing False -> return $ Just (upd fileBase) hashToHexStr :: Hash -> String hashToHexStr n = printf "%016x" n' where n' :: Integer n' = fromIntegral n - fromIntegral (minBound :: Int)