@@ -0,0 +1,361 @@
2+Copyright (C) 2008-2015 John MacFarlane <jgm@berkeley.edu>
4+This program is free software; you can redistribute it and/or modify
5+it under the terms of the GNU General Public License as published by
6+the Free Software Foundation; either version 2 of the License, or
7+(at your option) any later version.
9+This program is distributed in the hope that it will be useful,
10+but WITHOUT ANY WARRANTY; without even the implied warranty of
11+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+GNU General Public License for more details.
14+You should have received a copy of the GNU General Public License
15+along with this program; if not, write to the Free Software
16+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20+ Module : Text.Pandoc.Writers.ZimWiki
21+ Copyright : Copyright (C) 2008-2015 John MacFarlane, 2016 Alex Ivkin
22+ License : GNU GPL, version 2 or above
24+ Maintainer : Alex Ivkin <alex@ivkin.net>
26+ Portability : portable
28+Conversion of 'Pandoc' documents to ZimWiki markup.
30+http://zim-wiki.org/manual/Help/Wiki_Syntax.html
33+module Text.Pandoc.Writers.ZimWiki ( writeZimWiki ) where
34+import Text.Pandoc.Definition
35+import Text.Pandoc.Options ( WriterOptions(writerTableOfContents, writerStandalone, writerTemplate, writerWrapText), WrapOption(..) )
36+import Text.Pandoc.Shared ( escapeURI, removeFormatting, trimr, substitute )
37+import Text.Pandoc.Writers.Shared ( defField, metaToJSON )
38+import Text.Pandoc.ImageSize
39+import Text.Pandoc.Templates ( renderTemplate' )
40+import Data.List ( intercalate, isPrefixOf, transpose, isInfixOf )
41+import Data.Text ( breakOnAll, pack )
42+import Data.Default (Default(..))
43+import Network.URI ( isURI )
44+import Control.Monad ( zipWithM )
45+import Control.Monad.State ( modify, State, get, evalState )
46+--import Control.Monad.Reader ( ReaderT, runReaderT, ask, local )
48+data WriterState = WriterState {
50+ stIndent :: String -- Indent after the marker at the beginning of list items
53+instance Default WriterState where
54+ def = WriterState { stItemNum = 1, stIndent = "" }
56+-- | Convert Pandoc to ZimWiki.
57+writeZimWiki :: WriterOptions -> Pandoc -> String
58+writeZimWiki opts document = evalState (pandocToZimWiki opts document) (WriterState 1 "")
60+-- | Return ZimWiki representation of document.
61+pandocToZimWiki :: WriterOptions -> Pandoc -> State WriterState String
62+pandocToZimWiki opts (Pandoc meta blocks) = do
63+ metadata <- metaToJSON opts
64+ (fmap trimr . blockListToZimWiki opts)
65+ (inlineListToZimWiki opts)
67+ body <- blockListToZimWiki opts blocks
68+ --let header = "Content-Type: text/x-zim-wiki\nWiki-Format: zim 0.4\n"
70+ let context = defField "body" main
71+ $ defField "toc" (writerTableOfContents opts)
73+ if writerStandalone opts
74+ then return $ renderTemplate' (writerTemplate opts) context
77+-- | Escape special characters for ZimWiki.
78+escapeString :: String -> String
79+escapeString = substitute "__" "''__''" .
80+ substitute "**" "''**''" .
81+ substitute "~~" "''~~''" .
82+ substitute "//" "''//''"
84+-- | Convert Pandoc block element to ZimWiki.
85+blockToZimWiki :: WriterOptions -> Block -> State WriterState String
87+blockToZimWiki _ Null = return ""
89+blockToZimWiki opts (Div _attrs bs) = do
90+ contents <- blockListToZimWiki opts bs
91+ return $ contents ++ "\n"
93+blockToZimWiki opts (Plain inlines) = inlineListToZimWiki opts inlines
95+-- title beginning with fig: indicates that the image is a figure
96+-- ZimWiki doesn't support captions - so combine together alt and caption into alt
97+blockToZimWiki opts (Para [Image attr txt (src,'f':'i':'g':':':tit)]) = do
100+ else (" " ++) `fmap` inlineListToZimWiki opts txt
101+ let opt = if null txt
103+ else "|" ++ if null tit then capt else tit ++ capt
104+ -- Relative links fail isURI and receive a colon
105+ prefix = if isURI src then "" else ":"
106+ return $ "{{" ++ prefix ++ src ++ imageDims opts attr ++ opt ++ "}}\n"
108+blockToZimWiki opts (Para inlines) = do
109+ indent <- stIndent <$> get
110+ -- useTags <- stUseTags <$> get
111+ contents <- inlineListToZimWiki opts inlines
112+ return $ contents ++ if null indent then "\n" else ""
114+blockToZimWiki opts (RawBlock f str)
115+ | f == Format "zimwiki" = return str
116+ | f == Format "html" = do cont <- indentFromHTML opts str; return cont
117+ | otherwise = return "" -- $ "** unknown raw block "++ show f ++ "=" ++ str ++ " **"
119+blockToZimWiki _ HorizontalRule = return "\n----\n"
121+blockToZimWiki opts (Header level _ inlines) = do
122+ contents <- inlineListToZimWiki opts $ removeFormatting inlines -- emphasis, links etc. not allowed in headers
123+ let eqs = replicate ( 7 - level ) '='
124+ return $ eqs ++ " " ++ contents ++ " " ++ eqs ++ "\n"
126+blockToZimWiki _ (CodeBlock (_,classes,_) str) = do
127+ return $ case classes of
128+ [] -> "'''\n" ++ cleanupCode str ++ "\n'''\n" -- no lang block is a quote block
129+ (x:_) -> "{{{code: lang=\"" ++ x ++ "\" linenumbers=\"True\"\n" ++ str ++ "\n}}}\n" -- for zim's code plugin, go verbatim on the lang spec
131+blockToZimWiki opts (BlockQuote blocks) = do
132+ contents <- blockListToZimWiki opts blocks
133+ return $ unlines $ map ("> " ++) $ lines contents
135+blockToZimWiki opts (Table capt aligns _ headers rows) = do
136+ captionDoc <- if null capt
139+ c <- inlineListToZimWiki opts capt
140+ return $ "" ++ c ++ "\n"
141+ headers' <- if all null headers
142+ then zipWithM (tableItemToZimWiki opts) aligns (rows !! 0)
143+ else zipWithM (tableItemToZimWiki opts) aligns headers
144+ rows' <- mapM (zipWithM (tableItemToZimWiki opts) aligns) rows
145+ let widths = map (maximum . map length) $ transpose (headers':rows')
146+ let padTo (width, al) s =
147+ case (width - length s) of
149+ if al == AlignLeft || al == AlignDefault
150+ then s ++ replicate x ' '
151+ else if al == AlignRight
152+ then replicate x ' ' ++ s
153+ else replicate (x `div` 2) ' ' ++
154+ s ++ replicate (x - x `div` 2) ' '
156+ let borderCell (width, al) _ =
158+ then ":"++ replicate (width-1) '-'
159+ else if al == AlignDefault
160+ then replicate width '-'
161+ else if al == AlignRight
162+ then replicate (width-1) '-' ++ ":"
163+ else ":" ++ replicate (width-2) '-' ++ ":"
164+ let underheader = "|" ++ intercalate "|" (zipWith borderCell (zip widths aligns) headers') ++ "|"
165+ let renderRow sep cells = sep ++ intercalate sep (zipWith padTo (zip widths aligns) cells) ++ sep
166+ return $ captionDoc ++
167+ (if null headers' then "" else renderRow "|" headers' ++ "\n") ++ underheader ++ "\n" ++
168+ unlines (map (renderRow "|") rows')
170+blockToZimWiki opts (BulletList items) = do
171+ indent <- stIndent <$> get
172+ modify $ \s -> s { stIndent = stIndent s ++ "\t" }
173+ contents <- (mapM (listItemToZimWiki opts) items)
174+ modify $ \s -> s{ stIndent = indent } -- drop 1 (stIndent s) }
175+ return $ vcat contents ++ if null indent then "\n" else ""
177+blockToZimWiki opts (OrderedList _ items) = do
178+ indent <- stIndent <$> get
179+ modify $ \s -> s { stIndent = stIndent s ++ "\t", stItemNum = 1 }
180+ contents <- (mapM (orderedListItemToZimWiki opts) items)
181+ modify $ \s -> s{ stIndent = indent } -- drop 1 (stIndent s) }
182+ return $ vcat contents ++ if null indent then "\n" else ""
184+blockToZimWiki opts (DefinitionList items) = do
185+ contents <- (mapM (definitionListItemToZimWiki opts) items)
186+ return $ vcat contents
188+definitionListItemToZimWiki :: WriterOptions -> ([Inline],[[Block]]) -> State WriterState String
189+definitionListItemToZimWiki opts (label, items) = do
190+ labelText <- inlineListToZimWiki opts label
191+ contents <- mapM (blockListToZimWiki opts) items
192+ indent <- stIndent <$> get
193+ return $ indent ++ "* **" ++ labelText ++ "** " ++ concat contents
195+-- Auxiliary functions for lists:
196+indentFromHTML :: WriterOptions -> String -> State WriterState String
197+indentFromHTML _ str = do
198+ indent <- stIndent <$> get
199+ itemnum <- stItemNum <$> get
200+ if isInfixOf "<li>" str then return $ indent ++ show itemnum ++ "."
201+ else if isInfixOf "</li>" str then return "\n"
202+ else if isInfixOf "<li value=" str then do
204+ let val = drop 10 $ reverse $ drop 1 $ reverse str
205+ --let val = take ((length valls) - 2) valls
206+ modify $ \s -> s { stItemNum = read val }
207+ return "" -- $ indent ++ val ++ "." -- zim does its own numbering
208+ else if isInfixOf "<ol>" str then do
209+ let olcount=countSubStrs "<ol>" str
210+ modify $ \s -> s { stIndent = stIndent s ++ replicate olcount '\t', stItemNum = 1 }
211+ return "" -- $ "OL-ON[" ++ newfix ++"]"
212+ else if isInfixOf "</ol>" str then do
213+ let olcount=countSubStrs "/<ol>" str
214+ modify $ \s -> s{ stIndent = drop olcount (stIndent s) }
215+ return "" -- $ "OL-OFF[" ++ newfix ++"]"
217+ return $ "" -- ** unknown inner HTML "++ str ++"**"
219+countSubStrs :: String -> String -> Int
220+countSubStrs sub str = length $ breakOnAll (pack sub) (pack str)
222+cleanupCode :: String -> String
223+cleanupCode = substitute "<nowiki>" "" . substitute "</nowiki>" ""
225+vcat :: [String] -> String
226+vcat = intercalate "\n"
228+-- | Convert bullet list item (list of blocks) to ZimWiki.
229+listItemToZimWiki :: WriterOptions -> [Block] -> State WriterState String
230+listItemToZimWiki opts items = do
231+ contents <- blockListToZimWiki opts items
232+ indent <- stIndent <$> get
233+ return $ indent ++ "* " ++ contents
235+-- | Convert ordered list item (list of blocks) to ZimWiki.
236+orderedListItemToZimWiki :: WriterOptions -> [Block] -> State WriterState String
237+orderedListItemToZimWiki opts items = do
238+ contents <- blockListToZimWiki opts items
239+ indent <- stIndent <$> get
240+ itemnum <- stItemNum <$> get
241+ --modify $ \s -> s { stItemNum = itemnum + 1 } -- this is not strictly necessary for zim as zim does its own renumbering
242+ return $ indent ++ show itemnum ++ ". " ++ contents
244+-- Auxiliary functions for tables:
245+tableItemToZimWiki :: WriterOptions -> Alignment -> [Block] -> State WriterState String
246+tableItemToZimWiki opts align' item = do
247+ let mkcell x = (if align' == AlignRight || align' == AlignCenter
250+ (if align' == AlignLeft || align' == AlignCenter
253+ contents <- blockListToZimWiki opts item -- local (\s -> s { stBackSlashLB = True }) $
254+ return $ mkcell contents
256+-- | Convert list of Pandoc block elements to ZimWiki.
257+blockListToZimWiki :: WriterOptions -> [Block] -> State WriterState String
258+blockListToZimWiki opts blocks = vcat <$> mapM (blockToZimWiki opts) blocks
260+-- | Convert list of Pandoc inline elements to ZimWiki.
261+inlineListToZimWiki :: WriterOptions -> [Inline] -> State WriterState String
262+inlineListToZimWiki opts lst = concat <$> (mapM (inlineToZimWiki opts) lst)
264+-- | Convert Pandoc inline element to ZimWiki.
265+inlineToZimWiki :: WriterOptions -> Inline -> State WriterState String
267+inlineToZimWiki opts (Emph lst) = do
268+ contents <- inlineListToZimWiki opts lst
269+ return $ "//" ++ contents ++ "//"
271+inlineToZimWiki opts (Strong lst) = do
272+ contents <- inlineListToZimWiki opts lst
273+ return $ "**" ++ contents ++ "**"
275+inlineToZimWiki opts (Strikeout lst) = do
276+ contents <- inlineListToZimWiki opts lst
277+ return $ "~~" ++ contents ++ "~~"
279+inlineToZimWiki opts (Superscript lst) = do
280+ contents <- inlineListToZimWiki opts lst
281+ return $ "^{" ++ contents ++ "}"
283+inlineToZimWiki opts (Subscript lst) = do
284+ contents <- inlineListToZimWiki opts lst
285+ return $ "_{" ++ contents ++ "}"
287+inlineToZimWiki opts (Quoted SingleQuote lst) = do
288+ contents <- inlineListToZimWiki opts lst
289+ return $ "\8216" ++ contents ++ "\8217"
291+inlineToZimWiki opts (Quoted DoubleQuote lst) = do
292+ contents <- inlineListToZimWiki opts lst
293+ return $ "\8220" ++ contents ++ "\8221"
295+inlineToZimWiki opts (Span _attrs ils) = inlineListToZimWiki opts ils
297+inlineToZimWiki opts (SmallCaps lst) = inlineListToZimWiki opts lst
299+inlineToZimWiki opts (Cite _ lst) = inlineListToZimWiki opts lst
301+inlineToZimWiki _ (Code _ str) = return $ "''" ++ str ++ "''"
303+inlineToZimWiki _ (Str str) = return $ escapeString str
305+inlineToZimWiki _ (Math mathType str) = return $ delim ++ str ++ delim -- note: str should NOT be escaped
306+ where delim = case mathType of
310+-- | f == Format "html" = return $ "<html>" ++ str ++ "</html>"
311+inlineToZimWiki opts (RawInline f str)
312+ | f == Format "zimwiki" = return str
313+ | f == Format "html" = do cont <- indentFromHTML opts str; return cont
314+ | otherwise = return ""
316+inlineToZimWiki _ (LineBreak) = return "\n" -- was \\\\
318+inlineToZimWiki opts SoftBreak =
319+ case writerWrapText opts of
320+ WrapNone -> return " "
321+ WrapAuto -> return " "
322+ WrapPreserve -> return "\n"
324+inlineToZimWiki _ Space = return " "
326+inlineToZimWiki opts (Link _ txt (src, _)) = do
327+ label <- inlineListToZimWiki opts txt
329+ [Str s] | "mailto:" `isPrefixOf` src -> return $ "<" ++ s ++ ">"
330+ | escapeURI s == src -> return src
332+ then return $ "[[" ++ src ++ "|" ++ label ++ "]]"
333+ else return $ "[[" ++ src' ++ "|" ++ label ++ "]]"
334+ where src' = case src of
335+ '/':xs -> xs -- with leading / it's a
336+ _ -> src -- link to a help page
337+inlineToZimWiki opts (Image attr alt (source, tit)) = do
338+ alt' <- inlineListToZimWiki opts alt
339+ let txt = case (tit, alt) of
341+ ("", _ ) -> "|" ++ alt'
342+ (_ , _ ) -> "|" ++ tit
343+ -- Relative links fail isURI and receive a colon
344+ prefix = if isURI source then "" else ":"
345+ return $ "{{" ++ prefix ++ source ++ imageDims opts attr ++ txt ++ "}}"
347+inlineToZimWiki opts (Note contents) = do
348+ contents' <- blockListToZimWiki opts contents
349+ return $ "((" ++ contents' ++ "))"
350+ -- note - may not work for notes with multiple blocks
352+imageDims :: WriterOptions -> Attr -> String
353+imageDims opts attr = go (toPx $ dimension Width attr) (toPx $ dimension Height attr)
355+ toPx = fmap (showInPixel opts) . checkPct
356+ checkPct (Just (Percent _)) = Nothing
357+ checkPct maybeDim = maybeDim
358+ go (Just w) Nothing = "?" ++ w
359+ go (Just w) (Just h) = "?" ++ w ++ "x" ++ h
360+ go Nothing (Just h) = "?0x" ++ h
361+ go Nothing Nothing = ""