diff --git README.markdown README.markdown
index d49e0cc..c5a4ada 100644
--- README.markdown
+++ README.markdown
@@ -7,9 +7,10 @@ files are stored in a [git], [darcs], or [mercurial] repository
and may be modified either by using the VCS's command-line tools or
through the wiki's web interface. By default, pandoc's extended version
of markdown is used as a markup language, but reStructuredText, LaTeX, HTML,
-DocBook, or Emacs Org-mode markup can also be used. Gitit can
-be configured to display TeX math (using [texmath]) and
-highlighted source code (using [highlighting-kate]).
+DocBook, or Emacs Org-mode markup can also be used. Pages can be exported in a
+number of different formats, including LaTeX, RTF, OpenOffice ODT, and
+MediaWiki markup. Gitit can be configured to display TeX math (using
+[texmath]) and highlighted source code (using [highlighting-kate]).
Other features include
@@ -410,7 +411,7 @@ Caching
By default, gitit does not cache content. If your wiki receives a lot of
traffic or contains pages that are slow to render, you may want to activate
caching. To do this, set the configuration option `use-cache` to `yes`.
-By default, rendered pages, and highlighted source files
+By default, rendered pages, highlighted source files, and exported PDFs
will be cached in the `cache` directory. (Another directory can be
specified by setting the `cache-dir` configuration option.)
diff --git data/default.conf data/default.conf
index cd528f9..567bf8f 100644
--- data/default.conf
+++ data/default.conf
@@ -266,9 +266,16 @@ feed-days: 14
feed-refresh-time: 60
# number of minutes to cache feeds before refreshing
+pdf-export: no
+# if yes, PDF will appear in export options. PDF will be created using
+# pdflatex, which must be installed and in the path. Note that PDF
+# exports create significant additional server load.
+
pandoc-user-data:
# if a directory is specified, this will be searched for pandoc
-# customizations. If no directory is
+# customizations. These can include a templates/ directory for custom
+# templates for various export formats, an S5 directory for custom
+# S5 styles, and a reference.odt for ODT exports. If no directory is
# specified, $HOME/.pandoc will be searched. See pandoc's README for
# more information.
diff --git data/templates/pagetools.st data/templates/pagetools.st
index a5178f4..2d01dfa 100644
--- data/templates/pagetools.st
+++ data/templates/pagetools.st
@@ -9,5 +9,6 @@
Atom feed
$endif$
+ $exportbox$
diff --git gitit.cabal gitit.cabal
index 3d0d695..361415d 100644
--- gitit.cabal
+++ gitit.cabal
@@ -115,6 +115,7 @@ Library
Network.Gitit.Authentication.Github,
Network.Gitit.Util, Network.Gitit.Server
Network.Gitit.Cache, Network.Gitit.State,
+ Network.Gitit.Export,
Network.Gitit.Handlers,
Network.Gitit.Plugins, Network.Gitit.Rpxnow,
Network.Gitit.Page, Network.Gitit.Feed,
diff --git src/Network/Gitit.hs src/Network/Gitit.hs
index 3ad25f8..032cc9d 100644
--- src/Network/Gitit.hs
+++ src/Network/Gitit.hs
@@ -199,6 +199,7 @@ wikiHandlers =
authenticate ForModify (unlessNoDelete deletePage showPage) ]
, dir "_preview" preview
, guardIndex >> indexPage
+ , guardCommand "export" >> exportPage
, method POST >> guardCommand "cancel" >> showPage
, method POST >> guardCommand "update" >>
authenticate ForModify (unlessNoEdit updatePage showPage)
diff --git src/Network/Gitit/Cache.hs src/Network/Gitit/Cache.hs
index 3334d07..91b6c0a 100644
--- src/Network/Gitit/Cache.hs
+++ src/Network/Gitit/Cache.hs
@@ -41,13 +41,23 @@ import Control.Monad.Trans (liftIO)
import Text.Pandoc.UTF8 (encodePath)
-- | Expire a cached file, identified by its filename in the filestore.
+-- If there is an associated exported PDF, expire it too.
-- Returns () after deleting a file from the cache, fails if no cached file.
expireCachedFile :: String -> GititServerPart ()
expireCachedFile file = do
cfg <- getConfig
let target = encodePath $ cacheDir cfg > file
exists <- liftIO $ doesFileExist target
- when exists $ liftIO $ liftIO $ removeFile target
+ when exists $ liftIO $ do
+ liftIO $ removeFile target
+ expireCachedPDF target (defaultExtension cfg)
+
+expireCachedPDF :: String -> String -> IO ()
+expireCachedPDF file ext =
+ when (takeExtension file == "." ++ ext) $ do
+ let pdfname = file ++ ".export.pdf"
+ exists <- doesFileExist pdfname
+ when exists $ removeFile pdfname
lookupCache :: String -> GititServerPart (Maybe (UTCTime, B.ByteString))
lookupCache file = do
@@ -74,3 +84,4 @@ cacheContents file contents = do
liftIO $ do
createDirectoryIfMissing True targetDir
B.writeFile target contents
+ expireCachedPDF target (defaultExtension cfg)
diff --git src/Network/Gitit/Config.hs src/Network/Gitit/Config.hs
index d39d8cf..1bfbc47 100644
--- src/Network/Gitit/Config.hs.orig 2001-09-09 01:46:40 UTC
+++ src/Network/Gitit/Config.hs
@@ -176,6 +176,7 @@ extractConfig cfgmap = do
cfWikiTitle <- get "DEFAULT" "wiki-title"
cfFeedDays <- get "DEFAULT" "feed-days" >>= readNumber
cfFeedRefreshTime <- get "DEFAULT" "feed-refresh-time" >>= readNumber
+ cfPDFExport <- get "DEFAULT" "pdf-export" >>= readBool
cfPandocUserData <- get "DEFAULT" "pandoc-user-data"
cfXssSanitize <- get "DEFAULT" "xss-sanitize" >>= readBool
cfRecentActivityDays <- get "DEFAULT" "recent-activity-days" >>= readNumber
@@ -279,6 +280,7 @@ extractConfig cfgmap = do
, wikiTitle = cfWikiTitle
, feedDays = cfFeedDays
, feedRefreshTime = cfFeedRefreshTime
+ , pdfExport = cfPDFExport
, pandocUserData = if null cfPandocUserData
then Nothing
else Just cfPandocUserData
diff --git src/Network/Gitit/ContentTransformer.hs src/Network/Gitit/ContentTransformer.hs
index 12e450a..fa82604 100644
--- src/Network/Gitit/ContentTransformer.hs
+++ src/Network/Gitit/ContentTransformer.hs
@@ -31,6 +31,7 @@ module Network.Gitit.ContentTransformer
, showRawPage
, showFileAsText
, showPage
+ , exportPage
, showHighlightedSource
, showFile
, preview
@@ -44,6 +45,7 @@ module Network.Gitit.ContentTransformer
, textResponse
, mimeFileResponse
, mimeResponse
+ , exportPandoc
, applyWikiTemplate
-- * Content-type transformation combinators
, pageToWikiPandoc
@@ -77,6 +79,7 @@ import Data.List (stripPrefix)
import Data.Maybe (isNothing, mapMaybe)
import Data.Semigroup ((<>))
import Network.Gitit.Cache (lookupCache, cacheContents)
+import Network.Gitit.Export (exportFormats)
import Network.Gitit.Framework hiding (uriPath)
import Network.Gitit.Layout
import Network.Gitit.Page (stringToPage)
@@ -183,6 +186,10 @@ showFileAsText = runFileTransformer rawTextResponse
showPage :: Handler
showPage = runPageTransformer htmlViaPandoc
+-- | Responds with page exported into selected format.
+exportPage :: Handler
+exportPage = runPageTransformer exportViaPandoc
+
-- | Responds with highlighted source code.
showHighlightedSource :: Handler
showHighlightedSource = runFileTransformer highlightRawSource
@@ -213,6 +220,15 @@ applyPreCommitPlugins = runPageTransformer . applyPreCommitTransforms
rawTextResponse :: ContentTransformer Response
rawTextResponse = rawContents >>= textResponse
+-- | Responds with a wiki page in the format specified
+-- by the @format@ parameter.
+exportViaPandoc :: ContentTransformer Response
+exportViaPandoc = rawContents >>=
+ maybe mzero return >>=
+ contentsToPage >>=
+ pageToWikiPandoc >>=
+ exportPandoc
+
-- | Responds with a wiki page. Uses the cache when
-- possible and caches the rendered page when appropriate.
htmlViaPandoc :: ContentTransformer Response
@@ -306,6 +322,17 @@ mimeResponse :: Monad m
mimeResponse c mimeType =
return . setContentType mimeType . toResponse $ c
+-- | Converts Pandoc to response using format specified in parameters.
+exportPandoc :: Pandoc -> ContentTransformer Response
+exportPandoc doc = do
+ params <- getParams
+ page <- getPageName
+ cfg <- lift getConfig
+ let format = pFormat params
+ case lookup format (exportFormats cfg) of
+ Nothing -> error $ "Unknown export format: " ++ format
+ Just writer -> lift (writer page doc)
+
-- | Adds the sidebar, page tabs, and other elements of the wiki page
-- layout to the raw content.
applyWikiTemplate :: Html -> ContentTransformer Response
diff --git src/Network/Gitit/Export.hs src/Network/Gitit/Export.hs
new file mode 100644
index 0000000..0842a8c
--- /dev/null
+++ src/Network/Gitit/Export.hs
@@ -0,0 +1,307 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-
+Copyright (C) 2009 John MacFarlane
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+-}
+
+{- Functions for exporting wiki pages in various formats.
+-}
+
+module Network.Gitit.Export ( exportFormats ) where
+import Control.Exception (throwIO)
+import Text.Pandoc hiding (HTMLMathMethod(..), getDataFileName)
+import qualified Text.Pandoc as Pandoc
+import Text.Pandoc.PDF (makePDF)
+import Text.Pandoc.SelfContained as SelfContained
+import qualified Text.Pandoc.UTF8 as UTF8
+import qualified Data.Map as M
+import Network.Gitit.Server
+import Network.Gitit.Framework (pathForPage)
+import Network.Gitit.State (getConfig)
+import Network.Gitit.Types
+import Network.Gitit.Cache (cacheContents, lookupCache)
+import Text.DocTemplates as DT
+import Control.Monad.Trans (liftIO)
+import Control.Monad (unless)
+import Text.XHtml (noHtml)
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Lazy as L
+import System.FilePath ((>), takeDirectory)
+import System.Environment (setEnv)
+import System.Directory (doesFileExist)
+import Text.HTML.SanitizeXSS
+import Data.ByteString.Lazy (fromStrict)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Encoding (encodeUtf8)
+import Data.List (isPrefixOf)
+import Skylighting (styleToCss, pygments)
+import System.IO.Temp (withSystemTempDirectory)
+import Paths_gitit (getDataFileName)
+
+defaultRespOptions :: WriterOptions
+defaultRespOptions = def { writerHighlightStyle = Just pygments }
+
+respondX :: String -> String -> String
+ -> (WriterOptions -> Pandoc -> PandocIO L.ByteString)
+ -> WriterOptions -> String -> Pandoc -> Handler
+respondX templ mimetype ext fn opts page doc = do
+ cfg <- getConfig
+ doc' <- if ext `elem` ["odt","pdf","beamer","epub","docx","rtf"]
+ then fixURLs page doc
+ else return doc
+ doc'' <- liftIO $ runIO $ do
+ setUserDataDir $ pandocUserData cfg
+ compiledTemplate <- compileDefaultTemplate (T.pack templ)
+ fn opts{ writerTemplate = Just compiledTemplate } doc'
+ either (liftIO . throwIO)
+ (ok . setContentType mimetype .
+ (if null ext then id else setFilename (page ++ "." ++ ext)) .
+ toResponseBS B.empty)
+ doc''
+
+respondS :: String -> String -> String -> (WriterOptions -> Pandoc -> PandocIO Text)
+ -> WriterOptions -> String -> Pandoc -> Handler
+respondS templ mimetype ext fn =
+ respondX templ mimetype ext (\o d -> fromStrict . encodeUtf8 <$> fn o d)
+
+respondSlides :: String -> (WriterOptions -> Pandoc -> PandocIO Text) -> String -> Pandoc -> Handler
+respondSlides templ fn page doc = do
+ cfg <- getConfig
+ let math = case mathMethod cfg of
+ MathML -> Pandoc.MathML
+ WebTeX u -> Pandoc.WebTeX $ T.pack u
+ _ -> Pandoc.PlainMath
+ let opts' = defaultRespOptions { writerIncremental = True
+ , writerHTMLMathMethod = math}
+ -- We sanitize the body only, to protect against XSS attacks.
+ -- (Sanitizing the whole HTML page would strip out javascript
+ -- needed for the slides.) We then pass the body into the
+ -- slide template using the 'body' variable.
+ Pandoc meta blocks <- fixURLs page doc
+ docOrError <- liftIO $ runIO $ do
+ setUserDataDir $ pandocUserData cfg
+ body' <- writeHtml5String opts' (Pandoc meta blocks) -- just body
+ let body'' = T.unpack
+ $ (if xssSanitize cfg then sanitizeBalance else id)
+ $ body'
+ let setVariable key val (DT.Context ctx) =
+ DT.Context $ M.insert (T.pack key) (toVal (T.pack val)) ctx
+ variables' <- if mathMethod cfg == MathML
+ then do
+ s <- readDataFile "MathMLinHTML.js"
+ return $ setVariable "mathml-script"
+ (UTF8.toString s) mempty
+ else return mempty
+ compiledTemplate <- compileDefaultTemplate (T.pack templ)
+ dzcore <- if templ == "dzslides"
+ then do
+ dztempl <- readDataFile $ "dzslides" > "template.html"
+ return $ unlines
+ $ dropWhile (not . isPrefixOf "