@@ -1,5 +1,5 @@
11{-# LANGUAGE DeriveDataTypeable, CPP, MultiParamTypeClasses,
2− FlexibleContexts, ScopedTypeVariables #-}
2+ FlexibleContexts, ScopedTypeVariables, GeneralizedNewtypeDeriving #-}
33{-
44Copyright (C) 2006-2014 John MacFarlane <jgm@berkeley.edu>
55
@@ -50,7 +50,11 @@ module Text.Pandoc.Shared (
5050 tabFilter,
5151 -- * Media Handling
5252 MediaBag,
53+ emptyMediaBag,
54+ lookupMedia,
5355 insertMedia,
56+ mediaDirectory,
57+ extractMediaBag,
5458 -- * Date/time
5559 normalizeDate,
5660 -- * Pandoc block and inline list processing
@@ -102,16 +106,18 @@ import Data.Char ( toLower, isLower, isUpper, isAlpha,
102106 isLetter, isDigit, isSpace )
103107import Data.List ( find, isPrefixOf, intercalate )
104108import qualified Data.Map as M
109+import Data.Maybe ( fromMaybe )
105110import Network.URI ( escapeURIString, isURI, nonStrictRelativeTo,
106111 unEscapeString, parseURIReference )
107112import qualified Data.Set as Set
108113import System.Directory
109114import Text.Pandoc.MIME (getMimeType)
110−import System.FilePath ( (</>), takeExtension, dropExtension )
115+import System.FilePath ( (</>), takeExtension, dropExtension, takeDirectory,
116+ splitPath, joinPath )
111117import Data.Generics (Typeable, Data)
112118import qualified Control.Monad.State as S
113119import qualified Control.Exception as E
114−import Control.Monad (msum, unless)
120+import Control.Monad (msum, unless, MonadPlus(..), when)
115121import Text.Pandoc.Pretty (charWidth)
116122import System.Locale (defaultTimeLocale)
117123import Data.Time
@@ -292,15 +298,63 @@ tabFilter tabStop =
292298--- Media handling
293299---
294300
295−-- | A map of media paths to their binary representations.
296−type MediaBag = M.Map String BL.ByteString
301+-- | A container for a collection of binary resources, with names and
302+-- mime types.
303+newtype MediaBag = MediaBag (M.Map String (String, BL.ByteString))
304+ deriving (Monoid)
297305
298−-- | Insert a media item into a `MediaBag`
299−insertMedia :: FilePath
300− -> BL.ByteString
306+instance Show MediaBag where
307+ show bag = "MediaBag " ++ show (mediaDirectory bag)
308+
309+emptyMediaBag :: MediaBag
310+emptyMediaBag = MediaBag M.empty
311+
312+-- | Insert a media item into a 'MediaBag', replacing any existing
313+-- value with the same name.
314+insertMedia :: FilePath -- ^ relative path and canonical name of resource
315+ -> Maybe String -- ^ mime type (Nothing = determine from extension)
316+ -> BL.ByteString -- ^ contents of resource
317+ -> MediaBag
301318 -> MediaBag
319+insertMedia fp mbMime contents (MediaBag mediamap) =
320+ MediaBag (M.insert fp (mime, contents) mediamap)
321+ where mime = fromMaybe "application/octet-stream" (mbMime `mplus` fallback)
322+ fallback = case takeExtension fp of
323+ ".gz" -> getMimeType $ dropExtension fp
324+ _ -> getMimeType fp
325+
326+-- | Lookup a media item in a 'MediaBag', returning mime type and contents.
327+lookupMedia :: FilePath
302328 -> MediaBag
303−insertMedia = M.insert
329+ -> Maybe (String, BL.ByteString)
330+lookupMedia fp (MediaBag mediamap) = M.lookup fp mediamap
331+
332+-- | Get a list of the file paths stored in a 'MediaBag', with
333+-- their corresponding mime types and the lengths in bytes of the contents.
334+mediaDirectory :: MediaBag -> [(String, String, Int)]
335+mediaDirectory (MediaBag mediamap) =
336+ M.foldWithKey (\fp (mime,contents) ->
337+ ((fp, mime, fromIntegral $ BL.length contents):)) [] mediamap
338+
339+-- | Extract contents of MediaBag to a given directory. Print informational
340+-- messages if 'verbose' is true.
341+extractMediaBag :: Bool
342+ -> FilePath
343+ -> MediaBag
344+ -> IO ()
345+extractMediaBag verbose dir (MediaBag mediamap) = do
346+ sequence_ $ M.foldWithKey
347+ (\fp (_ ,contents) ->
348+ ((writeMedia verbose dir (fp, contents)):)) [] mediamap
349+
350+writeMedia :: Bool -> FilePath -> (FilePath, BL.ByteString) -> IO ()
351+writeMedia verbose dir (subpath, bs) = do
352+ -- we join and split to convert a/b/c to a\b\c on Windows;
353+ -- in zip containers all paths use /
354+ let fullpath = dir </> joinPath (splitPath subpath)
355+ createDirectoryIfMissing True $ takeDirectory fullpath
356+ when verbose $ warn $ "extracting " ++ fullpath
357+ BL.writeFile fullpath bs
304358
305359--
306360-- Date/time
@@ -803,13 +857,9 @@ fetchItem sourceURL s
803857fetchItem' :: MediaBag -> Maybe String -> String
804858 -> IO (Either E.SomeException (BS.ByteString, Maybe String))
805859fetchItem' media sourceURL s = do
806− case M.lookup s media of
860+ case lookupMedia s media of
807861 Nothing -> fetchItem sourceURL s
808− Just bs -> do
809− let mime = case takeExtension s of
810− ".gz" -> getMimeType $ dropExtension s
811− x -> getMimeType x
812− return $ Right (BS.concat $ toChunks bs, mime)
862+ Just (mime, bs) -> return $ Right (BS.concat $ toChunks bs, Just mime)
813863
814864-- | Read from a URL and return raw data and maybe mime type.
815865openURL :: String -> IO (Either E.SomeException (BS.ByteString, Maybe String))