{-# LANGUAGE CPP #-}
module Network.Gitit.ContentTransformer
(
runPageTransformer
, runFileTransformer
, showRawPage
, showFileAsText
, showPage
, exportPage
, showHighlightedSource
, showFile
, preview
, applyPreCommitPlugins
, cacheHtml
, cachedHtml
, rawContents
, textResponse
, mimeFileResponse
, mimeResponse
, exportPandoc
, applyWikiTemplate
, pageToWikiPandoc
, pageToPandoc
, pandocToHtml
, highlightSource
, applyPageTransforms
, wikiDivify
, addPageTitleToPandoc
, addMathSupport
, addScripts
, getFileName
, getPageName
, getLayout
, getParams
, getCacheable
, inlinesToURL
, inlinesToString
)
where
import qualified Control.Exception as E
import Control.Monad.State
import Control.Monad.Reader (ask)
import Data.Foldable (traverse_)
import Data.Char (toLower)
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)
import Network.Gitit.Server
import Network.Gitit.State
import Network.Gitit.Types
import Network.Gitit.Util (getPageTypeDefaultExtensions)
import Network.HTTP (urlDecode)
import Network.URI (isUnescapedInURI)
import Network.URL (encString)
import System.FilePath
import qualified Text.Pandoc.Builder as B
import Text.Pandoc.Extensions (getDefaultExtensions)
import Text.HTML.SanitizeXSS (sanitizeBalance)
import Skylighting hiding (Context)
import Text.Pandoc hiding (MathML, WebTeX, MathJax)
import Text.XHtml hiding ( (</>), dir, method, password, rev )
import Text.XHtml.Strict (stringToHtmlString)
#if MIN_VERSION_blaze_html(0,5,0)
import Text.Blaze.Html.Renderer.String as Blaze ( renderHtml )
#else
import Text.Blaze.Renderer.String as Blaze ( renderHtml )
#endif
import qualified Data.Text as T
import qualified Data.ByteString as S (concat)
import qualified Data.ByteString.Char8 as SC (unpack)
import qualified Data.ByteString.Lazy as L (toChunks, fromChunks)
import qualified Data.FileStore as FS
import qualified Text.Pandoc as Pandoc
import Text.URI (parseURI, URI(..), uriQueryItems)
runPageTransformer :: ToMessage a
=> ContentTransformer a
-> GititServerPart a
runPageTransformer xform = withData $ \params -> do
page <- getPage
cfg <- getConfig
evalStateT xform Context{ ctxFile = pathForPage page (defaultExtension cfg)
, ctxLayout = defaultPageLayout{
pgPageName = page
, pgTitle = page
, pgPrintable = pPrintable params
, pgMessages = pMessages params
, pgRevision = pRevision params
, pgLinkToFeed = useFeed cfg }
, ctxCacheable = True
, ctxTOC = tableOfContents cfg
, ctxBirdTracks = showLHSBirdTracks cfg
, ctxCategories = []
, ctxMeta = [] }
runFileTransformer :: ToMessage a
=> ContentTransformer a
-> GititServerPart a
runFileTransformer xform = withData $ \params -> do
page <- getPage
cfg <- getConfig
evalStateT xform Context{ ctxFile = id page
, ctxLayout = defaultPageLayout{
pgPageName = page
, pgTitle = page
, pgPrintable = pPrintable params
, pgMessages = pMessages params
, pgRevision = pRevision params
, pgLinkToFeed = useFeed cfg }
, ctxCacheable = True
, ctxTOC = tableOfContents cfg
, ctxBirdTracks = showLHSBirdTracks cfg
, ctxCategories = []
, ctxMeta = [] }
showRawPage :: Handler
showRawPage = runPageTransformer rawTextResponse
showFileAsText :: Handler
showFileAsText = runFileTransformer rawTextResponse
showPage :: Handler
showPage = runPageTransformer htmlViaPandoc
exportPage :: Handler
exportPage = runPageTransformer exportViaPandoc
showHighlightedSource :: Handler
showHighlightedSource = runFileTransformer highlightRawSource
showFile :: Handler
showFile = runFileTransformer (rawContents >>= mimeFileResponse)
preview :: Handler
preview = runPageTransformer $
liftM (filter (/= '\r') . pRaw) getParams >>=
contentsToPage >>=
pageToWikiPandoc >>=
pandocToHtml >>=
return . toResponse . renderHtmlFragment
applyPreCommitPlugins :: String -> GititServerPart String
applyPreCommitPlugins = runPageTransformer . applyPreCommitTransforms
rawTextResponse :: ContentTransformer Response
rawTextResponse = rawContents >>= textResponse
exportViaPandoc :: ContentTransformer Response
exportViaPandoc = rawContents >>=
maybe mzero return >>=
contentsToPage >>=
pageToWikiPandoc >>=
exportPandoc
htmlViaPandoc :: ContentTransformer Response
htmlViaPandoc = cachedHtml `mplus`
(rawContents >>=
maybe mzero return >>=
contentsToPage >>=
handleRedirects >>=
either return
(pageToWikiPandoc >=>
addMathSupport >=>
pandocToHtml >=>
wikiDivify >=>
applyWikiTemplate >=>
cacheHtml))
highlightRawSource :: ContentTransformer Response
highlightRawSource =
cachedHtml `mplus`
(updateLayout (\l -> l { pgTabs = [ViewTab,HistoryTab] }) >>
rawContents >>=
highlightSource >>=
applyWikiTemplate >>=
cacheHtml)
cacheHtml :: Response -> ContentTransformer Response
cacheHtml resp' = do
params <- getParams
file <- getFileName
cacheable <- getCacheable
cfg <- lift getConfig
when (useCache cfg && cacheable && isNothing (pRevision params) && not (pPrintable params)) $
lift $ cacheContents file $ S.concat $ L.toChunks $ rsBody resp'
return resp'
cachedHtml :: ContentTransformer Response
cachedHtml = do
file <- getFileName
params <- getParams
cfg <- lift getConfig
if useCache cfg && not (pPrintable params) && isNothing (pRevision params)
then do mbCached <- lift $ lookupCache file
let emptyResponse = setContentType "text/html; charset=utf-8" . toResponse $ ()
maybe mzero (\(_modtime, contents) -> lift . ok $ emptyResponse{rsBody = L.fromChunks [contents]}) mbCached
else mzero
rawContents :: ContentTransformer (Maybe String)
rawContents = do
params <- getParams
file <- getFileName
fs <- lift getFileStore
let rev = pRevision params
liftIO $ E.catch (liftM Just $ FS.retrieve fs file rev)
(\e -> if e == FS.NotFound then return Nothing else E.throwIO e)
textResponse :: Maybe String -> ContentTransformer Response
textResponse Nothing = mzero
textResponse (Just c) = mimeResponse c "text/plain; charset=utf-8"
mimeFileResponse :: Maybe String -> ContentTransformer Response
mimeFileResponse Nothing = error "Unable to retrieve file contents."
mimeFileResponse (Just c) =
mimeResponse c =<< lift . getMimeTypeForExtension . takeExtension =<< getFileName
mimeResponse :: Monad m
=> String
-> String
-> m Response
mimeResponse c mimeType =
return . setContentType mimeType . toResponse $ c
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)
applyWikiTemplate :: Html -> ContentTransformer Response
applyWikiTemplate c = do
Context { ctxLayout = layout } <- get
lift $ formattedPage layout c
pageToWikiPandoc :: Page -> ContentTransformer Pandoc
pageToWikiPandoc page' =
pageToWikiPandoc' page' >>= addPageTitleToPandoc (pageTitle page')
pageToWikiPandoc' :: Page -> ContentTransformer Pandoc
pageToWikiPandoc' = applyPreParseTransforms >=>
pageToPandoc >=> applyPageTransforms
pageToPandoc :: Page -> ContentTransformer Pandoc
pageToPandoc page' = do
modifyContext $ \ctx -> ctx{ ctxTOC = pageTOC page'
, ctxCategories = pageCategories page'
, ctxMeta = pageMeta page' }
either (liftIO . E.throwIO) return $ readerFor (pageFormat page') (pageLHS page') (pageText page')
handleRedirects :: Page -> ContentTransformer (Either Response Page)
handleRedirects page = case lookup "redirect" (pageMeta page) of
Nothing -> isn'tRedirect
Just destination -> isRedirect destination
where
addMessage message = modifyContext $ \context -> context
{ ctxLayout = (ctxLayout context)
{ pgMessages = pgMessages (ctxLayout context) ++ [message]
}
}
redirectedFrom source = do
(url, html) <- processSource source
return $ concat
[ "Redirected from <a href=\""
, url
, "?redirect=no\" title=\"Go to original page\">"
, html
, "</a>"
]
doubleRedirect source destination = do
(url, html) <- processSource source
(url', html') <- processDestination destination
return $ concat
[ "This page normally redirects to <a href=\""
, url'
, "\" title=\"Continue to destination\">"
, html'
, "</a>, but as you were already redirected from <a href=\""
, url
, "?redirect=no\" title=\"Go to original page\">"
, html
, "</a>"
, ", this was stopped to prevent a double-redirect."
]
cancelledRedirect destination = do
(url', html') <- processDestination destination
return $ concat
[ "This page redirects to <a href=\""
, url'
, "\" title=\"Continue to destination\">"
, html'
, "</a>."
]
processSource source = do
base' <- getWikiBase
let url = stringToHtmlString $ base' ++ urlForPage source
let html = stringToHtmlString source
return (url, html)
processDestination destination = do
base' <- getWikiBase
let (page', fragment) = break (== '#') destination
let url = stringToHtmlString $ concat
[ base'
, urlForPage page'
, fragment
]
let html = stringToHtmlString page'
return (url, html)
getSource = do
cfg <- lift getConfig
base' <- getWikiBase
request <- askRq
return $ do
uri <- getHeader "referer" request >>= parseURI . SC.unpack
let params = uriQueryItems uri
redirect' <- lookup "redirect" params
guard $ redirect' == "yes"
path' <- stripPrefix (base' ++ "/") (uriPath uri)
let path'' = if null path' then frontPage cfg else urlDecode path'
guard $ isPage path''
return path''
withBody = setContentType "text/html; charset=utf-8" . toResponse
isn'tRedirect = do
getSource >>= traverse_ (redirectedFrom >=> addMessage)
return (Right page)
isRedirect destination = do
params <- getParams
case maybe (pRedirect params) (\_ -> Just False) (pRevision params) of
Nothing -> do
source <- getSource
case source of
Just source' -> do
doubleRedirect source' destination >>= addMessage
return (Right page)
Nothing -> fmap Left $ do
base' <- getWikiBase
let url' = concat
[ base'
, urlForPage (pageName page)
, "?redirect=yes"
]
lift $ seeOther url' $ withBody $ concat
[ "<!doctype html><html><head><title>307 Redirect"
, "</title></head><body><p>You are being <a href=\""
, stringToHtmlString url'
, "\">redirected</a>.</body></p></html>"
]
Just True -> fmap Left $ do
(url', html') <- processDestination destination
lift $ ok $ withBody $ concat
[ "<!doctype html><html><head><title>Redirecting to "
, html'
, "</title><meta http-equiv=\"refresh\" contents=\"0; url="
, url'
, "\" /><script type=\"text/javascript\">window.location=\""
, url'
, "\"</script></head><body><p>Redirecting to <a href=\""
, url'
, "\">"
, html'
, "</a>...</p></body></html>"
]
Just False -> do
cancelledRedirect destination >>= addMessage
return (Right page)
contentsToPage :: String -> ContentTransformer Page
contentsToPage s = do
cfg <- lift getConfig
pn <- getPageName
return $ stringToPage cfg pn s
pandocToHtml :: Pandoc -> ContentTransformer Html
pandocToHtml pandocContents = do
toc <- liftM ctxTOC get
bird <- liftM ctxBirdTracks get
cfg <- lift getConfig
let tpl = "$if(toc)$<div id=\"TOC\">\n$toc$\n</div>\n$endif$\n$body$"
return $ primHtml $ T.unpack .
(if xssSanitize cfg then sanitizeBalance else id) $
either E.throw id . runPure $ writeHtml5String def{
writerTemplate = Just tpl
, writerHTMLMathMethod =
case mathMethod cfg of
MathML -> Pandoc.MathML
WebTeX u -> Pandoc.WebTeX u
MathJax u -> Pandoc.MathJax u
RawTeX -> Pandoc.PlainMath
, writerTableOfContents = toc
, writerHighlightStyle = Just pygments
, writerExtensions = if bird
then enableExtension Ext_literate_haskell
$ writerExtensions def
else writerExtensions def
, writerEmailObfuscation = ReferenceObfuscation
} pandocContents
highlightSource :: Maybe String -> ContentTransformer Html
highlightSource Nothing = mzero
highlightSource (Just source) = do
file <- getFileName
let formatOpts = defaultFormatOpts { numberLines = True, lineAnchors = True }
case syntaxesByFilename defaultSyntaxMap file of
[] -> mzero
(l:_) -> case tokenize TokenizerConfig{
syntaxMap = defaultSyntaxMap
, traceOutput = False} l
$ T.pack $ filter (/='\r') source of
Left e -> fail (show e)
Right r -> return $ primHtml $ Blaze.renderHtml
$ formatHtmlBlock formatOpts r
getPageTransforms :: ContentTransformer [Pandoc -> PluginM Pandoc]
getPageTransforms = liftM (mapMaybe pageTransform) $ queryGititState plugins
where pageTransform (PageTransform x) = Just x
pageTransform _ = Nothing
getPreParseTransforms :: ContentTransformer [String -> PluginM String]
getPreParseTransforms = liftM (mapMaybe preParseTransform) $
queryGititState plugins
where preParseTransform (PreParseTransform x) = Just x
preParseTransform _ = Nothing
getPreCommitTransforms :: ContentTransformer [String -> PluginM String]
getPreCommitTransforms = liftM (mapMaybe preCommitTransform) $
queryGititState plugins
where preCommitTransform (PreCommitTransform x) = Just x
preCommitTransform _ = Nothing
applyTransform :: a -> (a -> PluginM a) -> ContentTransformer a
applyTransform inp transform = do
context <- get
conf <- lift getConfig
user <- lift getLoggedInUser
fs <- lift getFileStore
req <- lift askRq
let pluginData = PluginData{ pluginConfig = conf
, pluginUser = user
, pluginRequest = req
, pluginFileStore = fs }
(result', context') <- liftIO $ runPluginM (transform inp) pluginData context
put context'
return result'
applyPageTransforms :: Pandoc -> ContentTransformer Pandoc
applyPageTransforms c = do
xforms <- getPageTransforms
foldM applyTransform c (wikiLinksTransform : xforms)
applyPreParseTransforms :: Page -> ContentTransformer Page
applyPreParseTransforms page' = getPreParseTransforms >>= foldM applyTransform (pageText page') >>=
(\t -> return page'{ pageText = t })
applyPreCommitTransforms :: String -> ContentTransformer String
applyPreCommitTransforms c = getPreCommitTransforms >>= foldM applyTransform c
wikiDivify :: Html -> ContentTransformer Html
wikiDivify c = do
categories <- liftM ctxCategories get
base' <- lift getWikiBase
let categoryLink ctg = li (anchor ! [href $ base' ++ "/_category/" ++ ctg] << ctg)
let htmlCategories = if null categories
then noHtml
else thediv ! [identifier "categoryList"] << ulist << map categoryLink categories
return $ thediv ! [identifier "wikipage"] << [c, htmlCategories]
addPageTitleToPandoc :: String -> Pandoc -> ContentTransformer Pandoc
addPageTitleToPandoc title' (Pandoc _ blocks) = do
updateLayout $ \layout -> layout{ pgTitle = title' }
return $ if null title'
then Pandoc nullMeta blocks
else Pandoc (B.setMeta "title" (B.str title') nullMeta) blocks
addMathSupport :: a -> ContentTransformer a
addMathSupport c = do
conf <- lift getConfig
updateLayout $ \l ->
case mathMethod conf of
MathML -> addScripts l ["MathMLinHTML.js"]
WebTeX _ -> l
MathJax u -> addScripts l [u]
RawTeX -> l
return c
addScripts :: PageLayout -> [String] -> PageLayout
addScripts layout scriptPaths =
layout{ pgScripts = scriptPaths ++ pgScripts layout }
getParams :: ContentTransformer Params
getParams = lift (withData return)
getFileName :: ContentTransformer FilePath
getFileName = liftM ctxFile get
getPageName :: ContentTransformer String
getPageName = liftM (pgPageName . ctxLayout) get
getLayout :: ContentTransformer PageLayout
getLayout = liftM ctxLayout get
getCacheable :: ContentTransformer Bool
getCacheable = liftM ctxCacheable get
updateLayout :: (PageLayout -> PageLayout) -> ContentTransformer ()
updateLayout f = do
ctx <- get
let l = ctxLayout ctx
put ctx { ctxLayout = f l }
readerFor :: PageType -> Bool -> String -> Either PandocError Pandoc
readerFor pt lhs =
let defExts = getDefaultExtensions $ map toLower $ show pt
defPS = def{ readerExtensions = defExts
<> extensionsFromList [Ext_emoji]
<> getPageTypeDefaultExtensions pt lhs
<> readerExtensions def }
in runPure . (case pt of
RST -> readRST defPS
Markdown -> readMarkdown defPS
CommonMark -> readCommonMark defPS
LaTeX -> readLaTeX defPS
HTML -> readHtml defPS
Textile -> readTextile defPS
Org -> readOrg defPS
DocBook -> readDocBook defPS
MediaWiki -> readMediaWiki defPS) . T.pack
wikiLinksTransform :: Pandoc -> PluginM Pandoc
wikiLinksTransform pandoc
= do cfg <- liftM pluginConfig ask
return (bottomUp (convertWikiLinks cfg) pandoc)
convertWikiLinks :: Config -> Inline -> Inline
convertWikiLinks cfg (Link attr ref ("", "")) | useAbsoluteUrls cfg =
Link attr ref ("/" </> baseUrl cfg </> inlinesToURL ref, "Go to wiki page")
convertWikiLinks _cfg (Link attr ref ("", "")) =
Link attr ref (inlinesToURL ref, "Go to wiki page")
convertWikiLinks _cfg x = x
inlinesToURL :: [Inline] -> String
inlinesToURL = encString False isUnescapedInURI . inlinesToString
inlinesToString :: [Inline] -> String
inlinesToString = concatMap go
where go x = case x of
Str s -> s
Emph xs -> concatMap go xs
Strong xs -> concatMap go xs
Strikeout xs -> concatMap go xs
Superscript xs -> concatMap go xs
Subscript xs -> concatMap go xs
SmallCaps xs -> concatMap go xs
Quoted DoubleQuote xs -> '"' : (concatMap go xs ++ "\"")
Quoted SingleQuote xs -> '\'' : (concatMap go xs ++ "'")
Cite _ xs -> concatMap go xs
Code _ s -> s
Space -> " "
SoftBreak -> " "
LineBreak -> " "
Math DisplayMath s -> "$$" ++ s ++ "$$"
Math InlineMath s -> "$" ++ s ++ "$"
RawInline (Format "tex") s -> s
RawInline _ _ -> ""
Link _ xs _ -> concatMap go xs
Image _ xs _ -> concatMap go xs
Note _ -> ""
Span _ xs -> concatMap go xs