2017-08-08 11:01:05 -07:00
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
2015-03-23 11:35:44 -07:00
|
|
|
|
{-
|
2017-10-26 22:57:13 -07:00
|
|
|
|
Copyright (C) 2015-2017 John MacFarlane <jgm@berkeley.edu>
|
2015-03-23 11:35:44 -07:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
-}
|
|
|
|
|
|
|
|
|
|
{- |
|
|
|
|
|
Module : Text.Pandoc.Writers.CommonMark
|
2017-10-26 22:57:13 -07:00
|
|
|
|
Copyright : Copyright (C) 2015-2017 John MacFarlane
|
2015-03-23 11:35:44 -07:00
|
|
|
|
License : GNU GPL, version 2 or above
|
|
|
|
|
|
|
|
|
|
Maintainer : John MacFarlane <jgm@berkeley.edu>
|
|
|
|
|
Stability : alpha
|
|
|
|
|
Portability : portable
|
|
|
|
|
|
|
|
|
|
Conversion of 'Pandoc' documents to CommonMark.
|
|
|
|
|
|
|
|
|
|
CommonMark: <http://commonmark.org>
|
|
|
|
|
-}
|
|
|
|
|
module Text.Pandoc.Writers.CommonMark (writeCommonMark) where
|
|
|
|
|
|
2017-08-07 23:10:17 -07:00
|
|
|
|
import CMarkGFM
|
2017-06-16 23:29:37 +02:00
|
|
|
|
import Control.Monad.State.Strict (State, get, modify, runState)
|
2017-03-04 13:03:41 +01:00
|
|
|
|
import Data.Foldable (foldrM)
|
2017-08-08 11:01:05 -07:00
|
|
|
|
import Data.List (transpose)
|
2017-10-27 20:28:29 -07:00
|
|
|
|
import Data.Monoid (Any (..), (<>))
|
2017-06-10 23:39:49 +02:00
|
|
|
|
import Data.Text (Text)
|
2017-03-04 13:03:41 +01:00
|
|
|
|
import qualified Data.Text as T
|
|
|
|
|
import Text.Pandoc.Class (PandocMonad)
|
2015-03-23 11:35:44 -07:00
|
|
|
|
import Text.Pandoc.Definition
|
2017-03-04 13:03:41 +01:00
|
|
|
|
import Text.Pandoc.Options
|
2017-08-08 11:01:05 -07:00
|
|
|
|
import Text.Pandoc.Shared (isTightList, linesToPara, substitute)
|
2015-03-23 11:35:44 -07:00
|
|
|
|
import Text.Pandoc.Templates (renderTemplate')
|
2017-10-27 20:28:29 -07:00
|
|
|
|
import Text.Pandoc.Walk (query, walk, walkM)
|
2017-03-04 13:03:41 +01:00
|
|
|
|
import Text.Pandoc.Writers.HTML (writeHtml5String)
|
|
|
|
|
import Text.Pandoc.Writers.Shared
|
2015-03-23 11:35:44 -07:00
|
|
|
|
|
|
|
|
|
-- | Convert Pandoc to CommonMark.
|
2017-06-10 23:39:49 +02:00
|
|
|
|
writeCommonMark :: PandocMonad m => WriterOptions -> Pandoc -> m Text
|
2016-11-26 08:46:28 -05:00
|
|
|
|
writeCommonMark opts (Pandoc meta blocks) = do
|
|
|
|
|
let (blocks', notes) = runState (walkM processNotes blocks) []
|
|
|
|
|
notes' = if null notes
|
|
|
|
|
then []
|
|
|
|
|
else [OrderedList (1, Decimal, Period) $ reverse notes]
|
2017-08-08 13:17:29 -07:00
|
|
|
|
main <- blocksToCommonMark opts (blocks' ++ notes')
|
2016-11-26 08:46:28 -05:00
|
|
|
|
metadata <- metaToJSON opts
|
|
|
|
|
(blocksToCommonMark opts)
|
|
|
|
|
(inlinesToCommonMark opts)
|
|
|
|
|
meta
|
|
|
|
|
let context = defField "body" main $ metadata
|
2017-06-20 22:43:48 +02:00
|
|
|
|
case writerTemplate opts of
|
|
|
|
|
Nothing -> return main
|
|
|
|
|
Just tpl -> renderTemplate' tpl context
|
2015-03-23 11:35:44 -07:00
|
|
|
|
|
2017-08-08 11:01:05 -07:00
|
|
|
|
softBreakToSpace :: Inline -> Inline
|
|
|
|
|
softBreakToSpace SoftBreak = Space
|
2017-10-27 20:28:29 -07:00
|
|
|
|
softBreakToSpace x = x
|
2017-08-08 11:01:05 -07:00
|
|
|
|
|
2015-03-23 11:35:44 -07:00
|
|
|
|
processNotes :: Inline -> State [[Block]] Inline
|
|
|
|
|
processNotes (Note bs) = do
|
|
|
|
|
modify (bs :)
|
|
|
|
|
notes <- get
|
|
|
|
|
return $ Str $ "[" ++ show (length notes) ++ "]"
|
|
|
|
|
processNotes x = return x
|
|
|
|
|
|
|
|
|
|
node :: NodeType -> [Node] -> Node
|
|
|
|
|
node = Node Nothing
|
|
|
|
|
|
2017-06-10 23:39:49 +02:00
|
|
|
|
blocksToCommonMark :: PandocMonad m => WriterOptions -> [Block] -> m Text
|
2016-11-26 08:46:28 -05:00
|
|
|
|
blocksToCommonMark opts bs = do
|
|
|
|
|
let cmarkOpts = [optHardBreaks | isEnabled Ext_hard_line_breaks opts]
|
|
|
|
|
colwidth = if writerWrapText opts == WrapAuto
|
|
|
|
|
then Just $ writerColumns opts
|
|
|
|
|
else Nothing
|
2017-08-08 09:14:13 -07:00
|
|
|
|
nodes <- blocksToNodes opts bs
|
2017-08-08 14:00:13 -07:00
|
|
|
|
return $ T.stripEnd $
|
2016-11-26 08:46:28 -05:00
|
|
|
|
nodeToCommonmark cmarkOpts colwidth $
|
|
|
|
|
node DOCUMENT nodes
|
|
|
|
|
|
2017-06-10 23:39:49 +02:00
|
|
|
|
inlinesToCommonMark :: PandocMonad m => WriterOptions -> [Inline] -> m Text
|
2015-03-23 11:35:44 -07:00
|
|
|
|
inlinesToCommonMark opts ils = return $
|
2017-08-08 09:14:13 -07:00
|
|
|
|
nodeToCommonmark cmarkOpts colwidth $
|
|
|
|
|
node PARAGRAPH (inlinesToNodes opts ils)
|
2015-03-23 11:35:44 -07:00
|
|
|
|
where cmarkOpts = [optHardBreaks | isEnabled Ext_hard_line_breaks opts]
|
2015-12-11 15:58:11 -08:00
|
|
|
|
colwidth = if writerWrapText opts == WrapAuto
|
2015-07-14 22:51:23 -07:00
|
|
|
|
then Just $ writerColumns opts
|
|
|
|
|
else Nothing
|
2015-03-23 11:35:44 -07:00
|
|
|
|
|
2017-08-08 09:14:13 -07:00
|
|
|
|
blocksToNodes :: PandocMonad m => WriterOptions -> [Block] -> m [Node]
|
|
|
|
|
blocksToNodes opts = foldrM (blockToNodes opts) []
|
2016-11-26 08:46:28 -05:00
|
|
|
|
|
2017-08-08 09:14:13 -07:00
|
|
|
|
blockToNodes :: PandocMonad m => WriterOptions -> Block -> [Node] -> m [Node]
|
|
|
|
|
blockToNodes opts (Plain xs) ns =
|
|
|
|
|
return (node PARAGRAPH (inlinesToNodes opts xs) : ns)
|
|
|
|
|
blockToNodes opts (Para xs) ns =
|
|
|
|
|
return (node PARAGRAPH (inlinesToNodes opts xs) : ns)
|
|
|
|
|
blockToNodes opts (LineBlock lns) ns = blockToNodes opts (linesToPara lns) ns
|
|
|
|
|
blockToNodes _ (CodeBlock (_,classes,_) xs) ns = return $
|
2016-11-26 08:46:28 -05:00
|
|
|
|
(node (CODE_BLOCK (T.pack (unwords classes)) (T.pack xs)) [] : ns)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
blockToNodes _ (RawBlock fmt xs) ns
|
2016-11-26 08:46:28 -05:00
|
|
|
|
| fmt == Format "html" = return (node (HTML_BLOCK (T.pack xs)) [] : ns)
|
|
|
|
|
| otherwise = return (node (CUSTOM_BLOCK (T.pack xs) (T.empty)) [] : ns)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
blockToNodes opts (BlockQuote bs) ns = do
|
|
|
|
|
nodes <- blocksToNodes opts bs
|
2016-11-26 08:46:28 -05:00
|
|
|
|
return (node BLOCK_QUOTE nodes : ns)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
blockToNodes opts (BulletList items) ns = do
|
|
|
|
|
nodes <- mapM (blocksToNodes opts) items
|
2016-11-26 08:46:28 -05:00
|
|
|
|
return (node (LIST ListAttributes{
|
|
|
|
|
listType = BULLET_LIST,
|
|
|
|
|
listDelim = PERIOD_DELIM,
|
|
|
|
|
listTight = isTightList items,
|
|
|
|
|
listStart = 1 }) (map (node ITEM) nodes) : ns)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
blockToNodes opts (OrderedList (start, _sty, delim) items) ns = do
|
|
|
|
|
nodes <- mapM (blocksToNodes opts) items
|
2016-11-26 08:46:28 -05:00
|
|
|
|
return (node (LIST ListAttributes{
|
|
|
|
|
listType = ORDERED_LIST,
|
|
|
|
|
listDelim = case delim of
|
|
|
|
|
OneParen -> PAREN_DELIM
|
|
|
|
|
TwoParens -> PAREN_DELIM
|
|
|
|
|
_ -> PERIOD_DELIM,
|
|
|
|
|
listTight = isTightList items,
|
|
|
|
|
listStart = start }) (map (node ITEM) nodes) : ns)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
blockToNodes _ HorizontalRule ns = return (node THEMATIC_BREAK [] : ns)
|
|
|
|
|
blockToNodes opts (Header lev _ ils) ns =
|
|
|
|
|
return (node (HEADING lev) (inlinesToNodes opts ils) : ns)
|
|
|
|
|
blockToNodes opts (Div _ bs) ns = do
|
|
|
|
|
nodes <- blocksToNodes opts bs
|
2016-11-26 08:46:28 -05:00
|
|
|
|
return (nodes ++ ns)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
blockToNodes opts (DefinitionList items) ns =
|
|
|
|
|
blockToNodes opts (BulletList items') ns
|
2015-03-23 11:35:44 -07:00
|
|
|
|
where items' = map dlToBullet items
|
|
|
|
|
dlToBullet (term, ((Para xs : ys) : zs)) =
|
|
|
|
|
Para (term ++ [LineBreak] ++ xs) : ys ++ concat zs
|
|
|
|
|
dlToBullet (term, ((Plain xs : ys) : zs)) =
|
|
|
|
|
Plain (term ++ [LineBreak] ++ xs) : ys ++ concat zs
|
|
|
|
|
dlToBullet (term, xs) =
|
|
|
|
|
Para term : concat xs
|
2017-08-13 10:43:43 -07:00
|
|
|
|
blockToNodes opts t@(Table capt aligns _widths headers rows) ns = do
|
2017-08-08 11:01:05 -07:00
|
|
|
|
let allcells = concat (headers:rows)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
let isLineBreak LineBreak = Any True
|
|
|
|
|
isLineBreak _ = Any False
|
2017-08-08 11:01:05 -07:00
|
|
|
|
let isPlainOrPara [Para _] = True
|
|
|
|
|
isPlainOrPara [Plain _] = True
|
|
|
|
|
isPlainOrPara [] = True
|
|
|
|
|
isPlainOrPara _ = False
|
2017-08-13 10:43:43 -07:00
|
|
|
|
let isSimple = all isPlainOrPara allcells &&
|
2017-08-08 11:01:05 -07:00
|
|
|
|
not ( getAny (query isLineBreak allcells) )
|
2017-08-08 09:14:13 -07:00
|
|
|
|
if isEnabled Ext_pipe_tables opts && isSimple
|
|
|
|
|
then do
|
2017-08-08 11:01:05 -07:00
|
|
|
|
-- We construct a table manually as a CUSTOM_BLOCK, for
|
|
|
|
|
-- two reasons: (1) cmark-gfm currently doesn't support
|
|
|
|
|
-- rendering TABLE nodes; (2) we can align the column sides;
|
|
|
|
|
-- (3) we can render the caption as a regular paragraph.
|
|
|
|
|
let capt' = node PARAGRAPH (inlinesToNodes opts capt)
|
|
|
|
|
-- backslash | in code and raw:
|
|
|
|
|
let fixPipe (Code attr xs) =
|
|
|
|
|
Code attr (substitute "|" "\\|" xs)
|
|
|
|
|
fixPipe (RawInline format xs) =
|
|
|
|
|
RawInline format (substitute "|" "\\|" xs)
|
|
|
|
|
fixPipe x = x
|
|
|
|
|
let toCell [Plain ils] = T.strip
|
|
|
|
|
$ nodeToCommonmark [] Nothing
|
|
|
|
|
$ node (CUSTOM_INLINE mempty mempty)
|
|
|
|
|
$ inlinesToNodes opts
|
|
|
|
|
$ walk (fixPipe . softBreakToSpace) ils
|
|
|
|
|
toCell [Para ils] = T.strip
|
|
|
|
|
$ nodeToCommonmark [] Nothing
|
|
|
|
|
$ node (CUSTOM_INLINE mempty mempty)
|
|
|
|
|
$ inlinesToNodes opts
|
|
|
|
|
$ walk (fixPipe . softBreakToSpace) ils
|
|
|
|
|
toCell [] = ""
|
|
|
|
|
toCell xs = error $ "toCell encountered " ++ show xs
|
|
|
|
|
let separator = " | "
|
|
|
|
|
let starter = "| "
|
|
|
|
|
let ender = " |"
|
|
|
|
|
let rawheaders = map toCell headers
|
|
|
|
|
let rawrows = map (map toCell) rows
|
|
|
|
|
let maximum' [] = 0
|
|
|
|
|
maximum' xs = maximum xs
|
|
|
|
|
let colwidths = map (maximum' . map T.length) $
|
|
|
|
|
transpose (rawheaders:rawrows)
|
|
|
|
|
let toHeaderLine len AlignDefault = T.replicate len "-"
|
|
|
|
|
toHeaderLine len AlignLeft = ":" <>
|
|
|
|
|
T.replicate (max (len - 1) 1) "-"
|
|
|
|
|
toHeaderLine len AlignRight =
|
|
|
|
|
T.replicate (max (len - 1) 1) "-" <> ":"
|
|
|
|
|
toHeaderLine len AlignCenter = ":" <>
|
|
|
|
|
T.replicate (max (len - 2) 1) (T.pack "-") <> ":"
|
|
|
|
|
let rawheaderlines = zipWith toHeaderLine colwidths aligns
|
|
|
|
|
let headerlines = starter <> T.intercalate separator rawheaderlines <>
|
|
|
|
|
ender
|
|
|
|
|
let padContent (align, w) t' =
|
|
|
|
|
let padding = w - T.length t'
|
|
|
|
|
halfpadding = padding `div` 2
|
|
|
|
|
in case align of
|
|
|
|
|
AlignRight -> T.replicate padding " " <> t'
|
|
|
|
|
AlignCenter -> T.replicate halfpadding " " <> t' <>
|
|
|
|
|
T.replicate (padding - halfpadding) " "
|
|
|
|
|
_ -> t' <> T.replicate padding " "
|
|
|
|
|
let toRow xs = starter <> T.intercalate separator
|
|
|
|
|
(zipWith padContent (zip aligns colwidths) xs) <>
|
|
|
|
|
ender
|
|
|
|
|
let table' = toRow rawheaders <> "\n" <> headerlines <> "\n" <>
|
|
|
|
|
T.intercalate "\n" (map toRow rawrows)
|
|
|
|
|
return (node (CUSTOM_BLOCK table' mempty) [] :
|
|
|
|
|
if null capt
|
|
|
|
|
then ns
|
|
|
|
|
else capt' : ns)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
else do -- fall back to raw HTML
|
|
|
|
|
s <- writeHtml5String def $! Pandoc nullMeta [t]
|
|
|
|
|
return (node (HTML_BLOCK s) [] : ns)
|
|
|
|
|
blockToNodes _ Null ns = return ns
|
|
|
|
|
|
|
|
|
|
inlinesToNodes :: WriterOptions -> [Inline] -> [Node]
|
|
|
|
|
inlinesToNodes opts = foldr (inlineToNodes opts) []
|
|
|
|
|
|
|
|
|
|
inlineToNodes :: WriterOptions -> Inline -> [Node] -> [Node]
|
2017-08-08 13:17:29 -07:00
|
|
|
|
inlineToNodes opts (Str s) = (node (TEXT (T.pack s')) [] :)
|
|
|
|
|
where s' = if isEnabled Ext_smart opts
|
|
|
|
|
then unsmartify opts s
|
|
|
|
|
else s
|
2017-08-08 09:14:13 -07:00
|
|
|
|
inlineToNodes _ Space = (node (TEXT (T.pack " ")) [] :)
|
|
|
|
|
inlineToNodes _ LineBreak = (node LINEBREAK [] :)
|
2017-08-08 13:17:29 -07:00
|
|
|
|
inlineToNodes opts SoftBreak
|
|
|
|
|
| isEnabled Ext_hard_line_breaks opts = (node LINEBREAK [] :)
|
|
|
|
|
| writerWrapText opts == WrapNone = (node (TEXT " ") [] :)
|
|
|
|
|
| otherwise = (node SOFTBREAK [] :)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
inlineToNodes opts (Emph xs) = (node EMPH (inlinesToNodes opts xs) :)
|
|
|
|
|
inlineToNodes opts (Strong xs) = (node STRONG (inlinesToNodes opts xs) :)
|
|
|
|
|
inlineToNodes opts (Strikeout xs) =
|
|
|
|
|
if isEnabled Ext_strikeout opts
|
|
|
|
|
then (node STRIKETHROUGH (inlinesToNodes opts xs) :)
|
|
|
|
|
else ((node (HTML_INLINE (T.pack "<s>")) [] : inlinesToNodes opts xs ++
|
|
|
|
|
[node (HTML_INLINE (T.pack "</s>")) []]) ++ )
|
|
|
|
|
inlineToNodes opts (Superscript xs) =
|
|
|
|
|
((node (HTML_INLINE (T.pack "<sup>")) [] : inlinesToNodes opts xs ++
|
2015-12-29 19:51:08 -08:00
|
|
|
|
[node (HTML_INLINE (T.pack "</sup>")) []]) ++ )
|
2017-08-08 09:14:13 -07:00
|
|
|
|
inlineToNodes opts (Subscript xs) =
|
|
|
|
|
((node (HTML_INLINE (T.pack "<sub>")) [] : inlinesToNodes opts xs ++
|
2015-12-29 19:51:08 -08:00
|
|
|
|
[node (HTML_INLINE (T.pack "</sub>")) []]) ++ )
|
2017-08-08 09:14:13 -07:00
|
|
|
|
inlineToNodes opts (SmallCaps xs) =
|
2017-03-04 14:43:52 +01:00
|
|
|
|
((node (HTML_INLINE (T.pack "<span class=\"smallcaps\">")) []
|
2017-08-08 09:14:13 -07:00
|
|
|
|
: inlinesToNodes opts xs ++
|
2015-12-29 19:51:08 -08:00
|
|
|
|
[node (HTML_INLINE (T.pack "</span>")) []]) ++ )
|
2017-08-08 09:14:13 -07:00
|
|
|
|
inlineToNodes opts (Link _ ils (url,tit)) =
|
|
|
|
|
(node (LINK (T.pack url) (T.pack tit)) (inlinesToNodes opts ils) :)
|
2017-10-13 10:36:27 -07:00
|
|
|
|
-- title beginning with fig: indicates implicit figure
|
|
|
|
|
inlineToNodes opts (Image alt ils (url,'f':'i':'g':':':tit)) =
|
|
|
|
|
inlineToNodes opts (Image alt ils (url,tit))
|
2017-08-08 09:14:13 -07:00
|
|
|
|
inlineToNodes opts (Image _ ils (url,tit)) =
|
|
|
|
|
(node (IMAGE (T.pack url) (T.pack tit)) (inlinesToNodes opts ils) :)
|
|
|
|
|
inlineToNodes _ (RawInline fmt xs)
|
2015-12-29 19:51:08 -08:00
|
|
|
|
| fmt == Format "html" = (node (HTML_INLINE (T.pack xs)) [] :)
|
|
|
|
|
| otherwise = (node (CUSTOM_INLINE (T.pack xs) (T.empty)) [] :)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
inlineToNodes opts (Quoted qt ils) =
|
|
|
|
|
((node (TEXT start) [] :
|
|
|
|
|
inlinesToNodes opts ils ++ [node (TEXT end) []]) ++)
|
2015-03-23 11:35:44 -07:00
|
|
|
|
where (start, end) = case qt of
|
2017-08-08 13:17:29 -07:00
|
|
|
|
SingleQuote
|
|
|
|
|
| isEnabled Ext_smart opts -> ("'","'")
|
|
|
|
|
| otherwise -> ("‘", "’")
|
|
|
|
|
DoubleQuote
|
|
|
|
|
| isEnabled Ext_smart opts -> ("\"", "\"")
|
|
|
|
|
| otherwise -> ("“", "”")
|
2017-08-08 09:14:13 -07:00
|
|
|
|
inlineToNodes _ (Code _ str) = (node (CODE (T.pack str)) [] :)
|
|
|
|
|
inlineToNodes _ (Math mt str) =
|
2015-03-23 11:35:44 -07:00
|
|
|
|
case mt of
|
|
|
|
|
InlineMath ->
|
2015-12-29 19:51:08 -08:00
|
|
|
|
(node (HTML_INLINE (T.pack ("\\(" ++ str ++ "\\)"))) [] :)
|
2015-03-23 11:35:44 -07:00
|
|
|
|
DisplayMath ->
|
2015-12-29 19:51:08 -08:00
|
|
|
|
(node (HTML_INLINE (T.pack ("\\[" ++ str ++ "\\]"))) [] :)
|
2017-08-08 09:14:13 -07:00
|
|
|
|
inlineToNodes opts (Span _ ils) = (inlinesToNodes opts ils ++)
|
|
|
|
|
inlineToNodes opts (Cite _ ils) = (inlinesToNodes opts ils ++)
|
|
|
|
|
inlineToNodes _ (Note _) = id -- should not occur
|
2015-03-23 11:35:44 -07:00
|
|
|
|
-- we remove Note elements in preprocessing
|