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 feed icon
  • $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 "