├── .gitignore └── Ditaa.hs /.gitignore: -------------------------------------------------------------------------------- 1 | # OS junk 2 | .DS_Store 3 | Thumbs.db 4 | 5 | # Haskell junk 6 | *.hi 7 | *.o 8 | 9 | -------------------------------------------------------------------------------- /Ditaa.hs: -------------------------------------------------------------------------------- 1 | module Ditaa (plugin) where 2 | 3 | -- This plugin allows you to include a [ditaa diagram](http://ditaa.sourceforge.net) 4 | -- in a page like this: 5 | -- 6 | -- ~~~ {.ditaa} 7 | -- +--------+ +-------+ +-------+ 8 | -- | | --+ ditaa +--> | | 9 | -- | Text | +-------+ |diagram| 10 | -- |Document| |!magic!| | | 11 | -- | {d}| | | | | 12 | -- +---+----+ +-------+ +-------+ 13 | -- : ^ 14 | -- | Lots of work | 15 | -- +-------------------------+ 16 | -- ~~~ 17 | -- 18 | -- The "java" executable must be in the path, and "ditaaXXX.jar" must be in the 19 | -- current directory (which will typically be your wiki's root directory). 20 | -- 21 | -- You can get the latest .jar from [here](http://ditaa.sourceforge.net/#download) 22 | -- 23 | -- The generated PNG file will be saved in the static `img` directory, using a unique 24 | -- name generated from a hash of the contents. 25 | 26 | import Network.Gitit.Interface 27 | 28 | import Control.Exception 29 | import Control.Monad 30 | import Control.Monad.Trans (liftIO) 31 | 32 | import Data.Char 33 | import Data.List 34 | import Data.Maybe 35 | 36 | import System.Directory 37 | import System.FilePath 38 | import System.IO 39 | import System.Process 40 | import System.Exit 41 | 42 | -- utf8-string package 43 | import Data.ByteString.Lazy.UTF8 (fromString) 44 | -- SHA package 45 | import Data.Digest.Pure.SHA 46 | 47 | plugin :: Plugin 48 | plugin = mkPageTransformM transformBlock 49 | 50 | transformBlock :: Block -> PluginM Block 51 | transformBlock (CodeBlock (_, classes, namevals) contents) | "ditaa" `elem` classes = do 52 | cfg <- askConfig 53 | let outfile = "img" "ditaa" uniqueName contents <.> "png" 54 | 55 | liftIO $ do 56 | createDirectoryIfMissing True (staticDir cfg takeDirectory outfile) 57 | renderDitaa (staticDir cfg outfile) contents 58 | (maybe False fromYesNo $ lookup "separation" namevals) -- I don't like the separation feature, turn off by default 59 | (maybe False fromYesNo $ lookup "shadows" namevals) -- Ditto for shadows. They mess up my ASCII heaps! 60 | 61 | return $ Para [Image [] ("/" ++ outfile, "")] 62 | transformBlock x = return x 63 | 64 | renderDitaa :: FilePath -> String -> Bool -> Bool -> IO () 65 | renderDitaa outfile contents separation shadows = unlessM (doesFileExist outfile) $ do 66 | -- 0) Establish a temporary file name to store the data into before running ditaa 67 | tmp_dir <- getTemporaryDirectory 68 | withTempFile tmp_dir "ditaa.txt" $ \temp_file temp_file_h -> do 69 | -- 1) Setup input file by writing contents to it: 70 | hPutStrLn temp_file_h contents 71 | hClose temp_file_h 72 | 73 | -- 2) Run ditaa to turn into an equivalently named .png: 74 | ditaa_jar <- getCurrentDirectory >>= findDitaaJar 75 | let options = ["-jar", ditaa_jar, 76 | "-e", "utf8", -- UTF8 input (I think!) 77 | "-o" -- Overwrite existing file if present (shouldn't be necessary) 78 | ] ++ 79 | ["-E" | not separation] ++ 80 | ["-S" | not shadows] ++ 81 | [temp_file] 82 | (ec, _, stderr_out) <- readProcessWithExitCode "java" options "" 83 | if ec == ExitSuccess 84 | then copyFile (replaceExtension temp_file ".png") outfile 85 | else error $ "Error running ditaa: " ++ stderr_out 86 | 87 | -- | Find the ditaaXXX.jar file in the given directory 88 | findDitaaJar :: FilePath -> IO FilePath 89 | findDitaaJar dir = fmap (fromMaybe (error $ "Could not locate the ditaa .jar file in the directory " ++ dir) . maybeHead . 90 | filter ("ditaa" `isInfixOf`) . filter (".jar" `isSuffixOf`)) $ getDirectoryContents dir 91 | 92 | fromYesNo :: String -> Bool 93 | fromYesNo val = null val || (map toLower val) `elem` ["yes","true"] 94 | 95 | withTempFile :: FilePath -- ^ Temporary directory to create the file in 96 | -> String -- ^ File name template: see 'openTempFile' 97 | -> (FilePath -> Handle -> IO a) -> IO a 98 | withTempFile tmpDir template action = 99 | bracket (openTempFile tmpDir template) 100 | (\(name, handle) -> hClose handle >> removeFile name) 101 | (uncurry action) 102 | 103 | maybeHead :: [a] -> Maybe a 104 | maybeHead = foldr ((Just .) . const) Nothing 105 | 106 | unlessM :: Monad m => m Bool -> m () -> m () 107 | unlessM mb mact = mb >>= \b -> unless b mact 108 | 109 | -- | Generate a unique filename given the file's contents. 110 | uniqueName :: String -> String 111 | uniqueName = showDigest . sha1 . fromString 112 | 113 | 114 | main :: IO () 115 | main = renderDitaa "Ditaa.png" "+--+\n|Hi|\n+--+" True False --------------------------------------------------------------------------------