git.iko.soy/typeable/pandoc
on master help sign in
Added a new FictionBook2 (FB2) writer.
commitb39597a910514ee22e8bf8c23995e66f2257d487 authorSergey Astanin <s.astanin@gmail.com> dateTue 15 Feb 2011, 19:40 (16 years ago) parentaee8791
4 files changed+628−30
README+24−15 src/Text/Pandoc.hs+17−8 src/Text/Pandoc/Writers/FB2.hs+570 src/pandoc.hs+17−7
README+24−15
@@ -16,7 +16,7 @@ another, and a command-line tool that uses this library. It can read
1616[LaTeX], and [DocBook XML]; and it can write plain text, [markdown],
1717[reStructuredText], [XHTML], [HTML 5], [LaTeX] (including [beamer]
1818slide shows), [ConTeXt], [RTF], [DocBook XML], [OpenDocument XML],
19−[ODT], [Word docx], [GNU Texinfo], [MediaWiki markup], [EPUB],
19+[ODT], [Word docx], [GNU Texinfo], [MediaWiki markup], [EPUB], [FictionBook2],
2020[Textile], [groff man] pages, [Emacs Org-Mode], [AsciiDoc], and [Slidy],
2121[Slideous], [DZSlides], or [S5] HTML slide shows. It can also produce
2222[PDF] output on systems where LaTeX is installed.
@@ -122,7 +122,7 @@ invoked under the name `hsmarkdown`, `pandoc` will behave as if the
122122recognized. However, this approach does not work under Cygwin, due to
123123problems with its simulation of symbolic links.
124124
125−[Cygwin]: http://www.cygwin.com/
125+[Cygwin]: http://www.cygwin.com/
126126[`iconv`]: http://www.gnu.org/software/libiconv/
127127[CTAN]: http://www.ctan.org "Comprehensive TeX Archive Network"
128128[TeX Live]: http://www.tug.org/texlive/
@@ -152,7 +152,8 @@ General options
152152 `textile` (Textile), `org` (Emacs Org-Mode), `texinfo` (GNU Texinfo),
153153 `docbook` (DocBook XML), `opendocument` (OpenDocument XML), `odt`
154154 (OpenOffice text document), `docx` (Word docx), `epub` (EPUB book),
155− `asciidoc` (AsciiDoc), `slidy` (Slidy HTML and javascript slide show),
155+ `fb2` (FictionBook2 e-book), `asciidoc` (AsciiDoc),
156+ `slidy` (Slidy HTML and javascript slide show),
156157 `slideous` (Slideous HTML and javascript slide show),
157158 `dzslides` (HTML5 + javascript slide show), `s5` (S5 HTML and javascript
158159 slide show), or `rtf` (rich text format). Note that `odt` and `epub`
@@ -248,7 +249,8 @@ General writer options
248249`-s`, `--standalone`
249250: Produce output with an appropriate header and footer (e.g. a
250251 standalone HTML, LaTeX, or RTF file, not a fragment). This option
251− is set automatically for `pdf`, `epub`, `docx`, and `odt` output.
252+ is set automatically for `pdf`, `epub`, `fb2`, `docx`, and `odt`
253+ output.
252254
253255`--template=`*FILE*
254256: Use *FILE* as a custom template for the generated document. Implies
@@ -590,7 +592,7 @@ Math rendering in HTML
590592`--gladtex`
591593: Enclose TeX math in `<eq>` tags in HTML output. These can then
592594 be processed by [gladTeX] to produce links to images of the typeset
593− formulas.
595+ formulas.
594596
595597`--mimetex`[=*URL*]
596598: Render TeX math using the [mimeTeX] CGI script. If *URL* is not
@@ -628,7 +630,7 @@ Options for wrapper scripts
628630[jsMath]: http://www.math.union.edu/~dpvc/jsmath/
629631[MathJax]: http://www.mathjax.org/
630632[gladTeX]: http://ans.hsh.no/home/mgg/gladtex/
631−[mimeTeX]: http://www.forkosh.com/mimetex.html
633+[mimeTeX]: http://www.forkosh.com/mimetex.html
632634[CSL]: http://CitationStyles.org
633635
634636Templates
@@ -1258,7 +1260,7 @@ or hyphens.
12581260### Compact and loose lists ###
12591261
12601262Pandoc behaves differently from `Markdown.pl` on some "edge
1261−cases" involving lists. Consider this source:
1263+cases" involving lists. Consider this source:
12621264
12631265 + First
12641266 + Second:
@@ -1352,7 +1354,7 @@ to the dashed line below it:[^4]
13521354
13531355 - If the dashed line is flush with the header text on the right side
13541356 but extends beyond it on the left, the column is right-aligned.
1355− - If the dashed line is flush with the header text on the left side
1357+ - If the dashed line is flush with the header text on the left side
13561358 but extends beyond it on the right, the column is left-aligned.
13571359 - If the dashed line extends beyond the header text on both sides,
13581360 the column is centered.
@@ -1713,7 +1715,12 @@ Docbook
17131715Docx
17141716 ~ It will be rendered using OMML math markup.
17151717
1716−HTML, Slidy, Slideous, DZSlides, S5, EPUB
1718+FictionBook2
1719+ ~ If the `--webtex` option is used, formulas are rendered as images
1720+ using Google Charts or other compatible web service, downloaded
1721+ and embedded in the e-book. Otherwise, they will appear verbatim.
1722+
1723+HTML, Slidy, DZSlides, S5, EPUB
17171724 ~ The way math is rendered in HTML will depend on the
17181725 command-line options selected:
17191726
@@ -1827,7 +1834,7 @@ Note that in LaTeX environments, like
18271834 \begin{tabular}{|l|l|}\hline
18281835 Age & Frequency \\ \hline
18291836 18--25 & 15 \\
1830− 26--35 & 33 \\
1837+ 26--35 & 33 \\
18311838 36--45 & 22 \\ \hline
18321839 \end{tabular}
18331840
@@ -1971,7 +1978,7 @@ If you just want a regular inline image, just make sure it is not
19711978the only thing in the paragraph. One way to do this is to insert a
19721979nonbreaking space after the image:
19731980
1974− ![This image won't be a figure](/url/of/image.png)\
1981+ ![This image won't be a figure](/url/of/image.png)\
19751982
19761983
19771984Footnotes
@@ -1987,7 +1994,7 @@ Pandoc's markdown allows footnotes, using the following syntax:
19871994
19881995 [^longnote]: Here's one with multiple blocks.
19891996
1990− Subsequent paragraphs are indented to show that they
1997+ Subsequent paragraphs are indented to show that they
19911998 belong to the previous footnote.
19921999
19932000 { some.code }
@@ -2289,7 +2296,8 @@ Andrea Rossato, Eric Kow, infinity0x, Luke Plant, shreevatsa.public,
22892296Puneeth Chaganti, Paul Rivier, rodja.trappe, Bradley Kuhn, thsutton,
22902297Nathan Gass, Jonathan Daugherty, Jérémy Bobbio, Justin Bogner, qerub,
22912298Christopher Sawicki, Kelsey Hightower, Masayoshi Takahashi, Antoine
2292−Latter, Ralf Stephan, Eric Seidel, B. Scott Michel, Gavin Beatty.
2299+Latter, Ralf Stephan, Eric Seidel, B. Scott Michel, Gavin Beatty,
2300+Sergey Astanin.
22932301
22942302[markdown]: http://daringfireball.net/projects/markdown/
22952303[reStructuredText]: http://docutils.sourceforge.net/docs/ref/rst/introduction.html
@@ -2301,10 +2309,10 @@ Latter, Ralf Stephan, Eric Seidel, B. Scott Michel, Gavin Beatty.
23012309[XHTML]: http://www.w3.org/TR/xhtml1/
23022310[LaTeX]: http://www.latex-project.org/
23032311[beamer]: http://www.tex.ac.uk/CTAN/macros/latex/contrib/beamer
2304−[ConTeXt]: http://www.pragma-ade.nl/
2312+[ConTeXt]: http://www.pragma-ade.nl/
23052313[RTF]: http://en.wikipedia.org/wiki/Rich_Text_Format
23062314[DocBook XML]: http://www.docbook.org/
2307−[OpenDocument XML]: http://opendocument.xml.org/
2315+[OpenDocument XML]: http://opendocument.xml.org/
23082316[ODT]: http://en.wikipedia.org/wiki/OpenDocument
23092317[Textile]: http://redcloth.org/textile
23102318[MediaWiki markup]: http://www.mediawiki.org/wiki/Help:Formatting
@@ -2319,3 +2327,4 @@ Latter, Ralf Stephan, Eric Seidel, B. Scott Michel, Gavin Beatty.
23192327[ISO 8601 format]: http://www.w3.org/TR/NOTE-datetime
23202328[Word docx]: http://www.microsoft.com/interop/openup/openxml/default.aspx
23212329[PDF]: http://www.adobe.com/pdf/
2330+[FictionBook2]: http://www.fictionbook.org/index.php/Eng:XML_Schema_Fictionbook_2.1
src/Text/Pandoc.hs+17−8
@@ -20,10 +20,10 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
2020{- |
2121 Module : Text.Pandoc
2222 Copyright : Copyright (C) 2006-2010 John MacFarlane
23− License : GNU GPL, version 2 or above
23+ License : GNU GPL, version 2 or above
2424
2525 Maintainer : John MacFarlane <jgm@berkeley.edu>
26− Stability : alpha
26+ Stability : alpha
2727 Portability : portable
2828
2929This helper module exports the main writers, readers, and data
@@ -45,7 +45,7 @@ inline links:
4545> markdownToRST =
4646> (writeRST defaultWriterOptions {writerReferenceLinks = True}) .
4747> readMarkdown defaultParserState
48−>
48+>
4949> main = getContents >>= putStrLn . markdownToRST
5050
5151Note: all of the readers assume that the input text has @'\n'@
@@ -55,7 +55,7 @@ you should remove @'\r'@ characters using @filter (/='\r')@.
5555-}
5656
5757module Text.Pandoc
58− (
58+ (
5959 -- * Definitions
6060 module Text.Pandoc.Definition
6161 -- * Generics
@@ -63,6 +63,7 @@ module Text.Pandoc
6363 -- * Lists of readers and writers
6464 , readers
6565 , writers
66+ , iowriters
6667 -- * Readers: converting /to/ Pandoc format
6768 , readMarkdown
6869 , readRST
@@ -98,9 +99,10 @@ module Text.Pandoc
9899 , writeODT
99100 , writeDocx
100101 , writeEPUB
102+ , writeFB2
101103 , writeOrg
102104 , writeAsciiDoc
103− -- * Writer options used in writers
105+ -- * Writer options used in writers
104106 , WriterOptions (..)
105107 , HTMLSlideVariant (..)
106108 , HTMLMathMethod (..)
@@ -129,7 +131,7 @@ import Text.Pandoc.Readers.Textile
129131import Text.Pandoc.Readers.Native
130132import Text.Pandoc.Writers.Native
131133import Text.Pandoc.Writers.Markdown
132−import Text.Pandoc.Writers.RST
134+import Text.Pandoc.Writers.RST
133135import Text.Pandoc.Writers.LaTeX
134136import Text.Pandoc.Writers.ConTeXt
135137import Text.Pandoc.Writers.Texinfo
@@ -137,10 +139,11 @@ import Text.Pandoc.Writers.HTML
137139import Text.Pandoc.Writers.ODT
138140import Text.Pandoc.Writers.Docx
139141import Text.Pandoc.Writers.EPUB
142+import Text.Pandoc.Writers.FB2
140143import Text.Pandoc.Writers.Docbook
141144import Text.Pandoc.Writers.OpenDocument
142145import Text.Pandoc.Writers.Man
143−import Text.Pandoc.Writers.RTF
146+import Text.Pandoc.Writers.RTF
144147import Text.Pandoc.Writers.MediaWiki
145148import Text.Pandoc.Writers.Textile
146149import Text.Pandoc.Writers.Org
@@ -168,7 +171,7 @@ readers = [("native" , \_ -> readNative)
168171 ,("rst+lhs" , \st ->
169172 readRST st{ stateLiterateHaskell = True})
170173 ,("docbook" , readDocBook)
171− ,("textile" , readTextile) -- TODO : textile+lhs
174+ ,("textile" , readTextile) -- TODO : textile+lhs
172175 ,("html" , readHtml)
173176 ,("latex" , readLaTeX)
174177 ,("latex+lhs" , \st ->
@@ -218,6 +221,12 @@ writers = [("native" , writeNative)
218221 ,("asciidoc" , writeAsciiDoc)
219222 ]
220223
224+-- | Association list of formats and writers which require IO to work.
225+-- These writers produce text output as well as thoses in 'writers'.
226+iowriters :: [ (String, WriterOptions -> Pandoc -> IO String) ]
227+iowriters = [ ("fb2" , writeFB2)
228+ ]
229+
221230{-# DEPRECATED jsonFilter "Use toJsonFilter instead" #-}
222231-- | Converts a transformation on the Pandoc AST into a function
223232-- that reads and writes a JSON-encoded string. This is useful
src/Text/Pandoc/Writers/FB2.hsnew file+570
@@ -0,0 +1,570 @@
1+{-
2+Copyright (c) 2011-2012, Sergey Astanin
3+All rights reserved.
4+
5+This program is free software; you can redistribute it and/or modify
6+it under the terms of the GNU General Public License as published by
7+the Free Software Foundation; either version 2 of the License, or
8+(at your option) any later version.
9+
10+This program is distributed in the hope that it will be useful,
11+but WITHOUT ANY WARRANTY; without even the implied warranty of
12+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+GNU General Public License for more details.
14+
15+You should have received a copy of the GNU General Public License
16+along with this program; if not, write to the Free Software
17+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18+-}
19+
20+{- | Conversion of 'Pandoc' documents to FB2 (FictionBook2) format.
21+
22+FictionBook is an XML-based e-book format. For more information see:
23+<http://www.fictionbook.org/index.php/Eng:XML_Schema_Fictionbook_2.1>
24+
25+-}
26+module Text.Pandoc.Writers.FB2 (writeFB2) where
27+
28+import Control.Monad.State (StateT, evalStateT, get, modify)
29+import Control.Monad.State (liftM, liftM2, liftIO)
30+import Data.ByteString.Base64 (encode)
31+import Data.Char (toUpper, toLower, isSpace)
32+import Data.List (intersperse, intercalate)
33+import Data.Either (lefts, rights)
34+import Network.Browser (browse, request, setAllowRedirects, setOutHandler)
35+import Network.HTTP (catchIO_, getRequest, getHeaders, getResponseBody)
36+import Network.HTTP (lookupHeader, HeaderName(..), urlEncode)
37+import Network.URI (isURI, unEscapeString)
38+import System.FilePath (takeExtension)
39+import Text.XML.Light
40+import qualified Control.Exception as E
41+import qualified Data.ByteString as B
42+import qualified Text.XML.Light as X
43+import qualified Text.XML.Light.Cursor as XC
44+
45+import Text.Pandoc.Definition
46+import Text.Pandoc.Shared (WriterOptions(..), HTMLMathMethod(..))
47+import Text.Pandoc.Shared (orderedListMarkers, defaultWriterOptions)
48+import Text.Pandoc.Generic (bottomUp)
49+
50+-- | Data to be written at the end of the document:
51+-- (foot)notes, URLs, references, images.
52+data FbRenderState = FbRenderState
53+ { footnotes :: [ (Int, String, [Content]) ] -- ^ #, ID, text
54+ , imagesToFetch :: [ (String, String) ] -- ^ filename, URL or path
55+ , parentListMarker :: String -- ^ list marker of the parent ordered list
56+ , parentBulletLevel :: Int -- ^ nesting level of the unordered list
57+ , writerOptions :: WriterOptions
58+ } deriving (Show)
59+
60+-- | FictionBook building monad.
61+type FBM = StateT FbRenderState IO
62+
63+newFB :: FbRenderState
64+newFB = FbRenderState { footnotes = [], imagesToFetch = []
65+ , parentListMarker = "", parentBulletLevel = 0
66+ , writerOptions = defaultWriterOptions }
67+
68+data ImageMode = NormalImage | InlineImage deriving (Eq)
69+instance Show ImageMode where
70+ show NormalImage = "imageType"
71+ show InlineImage = "inlineImageType"
72+
73+-- | Produce an FB2 document from a 'Pandoc' document.
74+writeFB2 :: WriterOptions -- ^ conversion options
75+ -> Pandoc -- ^ document to convert
76+ -> IO String -- ^ FictionBook2 document (not encoded yet)
77+writeFB2 opts (Pandoc meta blocks) = flip evalStateT newFB $ do
78+ modify (\s -> s { writerOptions = opts { writerStandalone = True } })
79+ desc <- description meta
80+ fp <- frontpage meta
81+ secs <- renderSections 1 blocks
82+ let body = el "body" $ fp ++ secs
83+ notes <- renderFootnotes
84+ (imgs,missing) <- liftM imagesToFetch get >>= \s -> liftIO (fetchImages s)
85+ let body' = replaceImagesWithAlt missing body
86+ let fb2_xml = el "FictionBook" (fb2_attrs, [desc, body'] ++ notes ++ imgs)
87+ return $ xml_head ++ (showContent fb2_xml)
88+ where
89+ xml_head = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
90+ fb2_attrs =
91+ let xmlns = "http://www.gribuser.ru/xml/fictionbook/2.0"
92+ xlink = "http://www.w3.org/1999/xlink"
93+ in [ uattr "xmlns" xmlns
94+ , attr ("xmlns", "l") xlink ]
95+ --
96+ frontpage :: Meta -> FBM [Content]
97+ frontpage meta' = do
98+ t <- cMapM toXml . docTitle $ meta'
99+ return $
100+ [ el "title" (el "p" t)
101+ , el "annotation" (map (el "p" . cMap plain)
102+ (docAuthors meta' ++ [docDate meta']))
103+ ]
104+ description :: Meta -> FBM Content
105+ description meta' = do
106+ bt <- booktitle meta'
107+ let as = authors meta'
108+ dd <- docdate meta'
109+ return $ el "description"
110+ [ el "title-info" (bt ++ as ++ dd)
111+ , el "document-info" [ el "program-used" "pandoc" ] -- FIXME: +version
112+ ]
113+ booktitle :: Meta -> FBM [Content]
114+ booktitle meta' = do
115+ t <- cMapM toXml . docTitle $ meta'
116+ return $ if null t
117+ then []
118+ else [ el "book-title" t ]
119+ authors :: Meta -> [Content]
120+ authors meta' = cMap author (docAuthors meta')
121+ author :: [Inline] -> [Content]
122+ author ss =
123+ let ws = words . cMap plain $ ss
124+ email = (el "email") `fmap` (take 1 $ filter ('@' `elem`) ws)
125+ ws' = filter ('@' `notElem`) ws
126+ names = case ws' of
127+ (nickname:[]) -> [ el "nickname" nickname ]
128+ (fname:lname:[]) -> [ el "first-name" fname
129+ , el "last-name" lname ]
130+ (fname:rest) -> [ el "first-name" fname
131+ , el "middle-name" (concat . init $ rest)
132+ , el "last-name" (last rest) ]
133+ ([]) -> []
134+ in list $ el "author" (names ++ email)
135+ docdate :: Meta -> FBM [Content]
136+ docdate meta' = do
137+ let ss = docDate meta'
138+ d <- cMapM toXml ss
139+ return $ if null d
140+ then []
141+ else [el "date" d]
142+
143+-- | Divide the stream of blocks into sections and convert to XML
144+-- representation.
145+renderSections :: Int -> [Block] -> FBM [Content]
146+renderSections level blocks = do
147+ let secs = splitSections level blocks
148+ mapM (renderSection level) secs
149+
150+renderSection :: Int -> ([Inline], [Block]) -> FBM Content
151+renderSection level (ttl, body) = do
152+ title <- if null ttl
153+ then return []
154+ else return . list . el "title" . formatTitle $ ttl
155+ content <- if (hasSubsections body)
156+ then renderSections (level + 1) body
157+ else cMapM blockToXml body
158+ return $ el "section" (title ++ content)
159+ where
160+ hasSubsections = any isHeader
161+ isHeader (Header _ _) = True
162+ isHeader _ = False
163+
164+-- | Only <p> and <empty-line> are allowed within <title> in FB2.
165+formatTitle :: [Inline] -> [Content]
166+formatTitle inlines =
167+ let lns = split isLineBreak inlines
168+ lns' = map (el "p" . cMap plain) lns
169+ in intersperse (el "empty-line" ()) lns'
170+
171+split :: (a -> Bool) -> [a] -> [[a]]
172+split _ [] = []
173+split cond xs = let (b,a) = break cond xs
174+ in (b:split cond (drop 1 a))
175+
176+isLineBreak :: Inline -> Bool
177+isLineBreak LineBreak = True
178+isLineBreak _ = False
179+
180+-- | Divide the stream of block elements into sections: [(title, blocks)].
181+splitSections :: Int -> [Block] -> [([Inline], [Block])]
182+splitSections level blocks = reverse $ revSplit (reverse blocks)
183+ where
184+ revSplit [] = []
185+ revSplit rblocks =
186+ let (lastsec, before) = break sameLevel rblocks
187+ (header, prevblocks) =
188+ case before of
189+ ((Header n title):prevblocks') ->
190+ if n == level
191+ then (title, prevblocks')
192+ else ([], before)
193+ _ -> ([], before)
194+ in (header, reverse lastsec) : revSplit prevblocks
195+ sameLevel (Header n _) = n == level
196+ sameLevel _ = False
197+
198+-- | Make another FictionBook body with footnotes.
199+renderFootnotes :: FBM [Content]
200+renderFootnotes = do
201+ fns <- footnotes `liftM` get
202+ if null fns
203+ then return [] -- no footnotes
204+ else return . list $
205+ el "body" ([uattr "name" "notes"], map renderFN (reverse fns))
206+ where
207+ renderFN (n, idstr, cs) =
208+ let fn_texts = (el "title" (el "p" (show n))) : cs
209+ in el "section" ([uattr "id" idstr], fn_texts)
210+
211+-- | Fetch images and encode them for the FictionBook XML.
212+-- Return image data and a list of hrefs of the missing images.
213+fetchImages :: [(String,String)] -> IO ([Content],[String])
214+fetchImages links = do
215+ imgs <- mapM (uncurry fetchImage) links
216+ return $ (rights imgs, lefts imgs)
217+
218+-- | Fetch image data from disk or from network and make a <binary> XML section.
219+-- Return either (Left hrefOfMissingImage) or (Right xmlContent).
220+fetchImage :: String -> String -> IO (Either String Content)
221+fetchImage href link = do
222+ mbimg <-
223+ if isURI link
224+ then fetchURL link
225+ else do
226+ d <- nothingOnError $ B.readFile (unEscapeString link)
227+ let t = case map toLower (takeExtension link) of
228+ ".png" -> Just "image/png"
229+ ".jpg" -> Just "image/jpeg"
230+ ".jpeg" -> Just "image/jpeg"
231+ ".jpe" -> Just "image/jpeg"
232+ _ -> Nothing -- only PNG and JPEG are supported in FB2
233+ return $ liftM2 (,) t d
234+ case mbimg of
235+ Just (imgtype, imgdata) -> do
236+ let encdata = encode imgdata
237+ let encstr = map (toEnum . fromEnum) . B.unpack $ encdata
238+ return . Right $ el "binary"
239+ ( [uattr "id" href
240+ , uattr "content-type" imgtype]
241+ , txt encstr )
242+ _ -> return (Left ('#':href))
243+ where
244+ nothingOnError :: (IO B.ByteString) -> (IO (Maybe B.ByteString))
245+ nothingOnError action = liftM Just action `E.catch` omnihandler
246+ omnihandler :: E.SomeException -> IO (Maybe B.ByteString)
247+ omnihandler _ = return Nothing
248+
249+-- | Fetch URL, return its Content-Type and binary data on success.
250+fetchURL :: String -> IO (Maybe (String, B.ByteString))
251+fetchURL url = do
252+ flip catchIO_ (return Nothing) $ do
253+ r <- browse $ do
254+ setOutHandler (const (return ()))
255+ setAllowRedirects True
256+ liftM snd . request . getRequest $ url
257+ let content_type = lookupHeader HdrContentType (getHeaders r)
258+ content <- liftM (Just . toBS) . getResponseBody $ Right r
259+ return $ liftM2 (,) content_type content
260+ where
261+ toBS = B.pack . map (toEnum . fromEnum)
262+
263+footnoteID :: Int -> String
264+footnoteID i = "n" ++ (show i)
265+
266+linkID :: Int -> String
267+linkID i = "l" ++ (show i)
268+
269+-- | Convert a block-level Pandoc's element to FictionBook XML representation.
270+blockToXml :: Block -> FBM [Content]
271+blockToXml (Plain ss) = cMapM toXml ss -- FIXME: can lead to malformed FB2
272+blockToXml (Para [Math DisplayMath formula]) = insertMath NormalImage formula
273+blockToXml (Para [img@(Image _ _)]) = insertImage NormalImage img
274+blockToXml (Para ss) = liftM (list . el "p") $ cMapM toXml ss
275+blockToXml (CodeBlock _ s) = return . spaceBeforeAfter .
276+ map (el "p" . el "code") . lines $ s
277+blockToXml (RawBlock _ s) = return . spaceBeforeAfter .
278+ map (el "p" . el "code") . lines $ s
279+blockToXml (BlockQuote bs) = liftM (list . el "cite") $ cMapM blockToXml bs
280+blockToXml (OrderedList a bss) = do
281+ state <- get
282+ let pmrk = parentListMarker state
283+ let markers = map ((pmrk ++ " ") ++) $ orderedListMarkers a
284+ let mkitem mrk bs = do
285+ modify (\s -> s { parentListMarker = mrk })
286+ itemtext <- cMapM blockToXml . paraToPlain $ bs
287+ modify (\s -> s { parentListMarker = pmrk }) -- old parent marker
288+ return . el "p" $ [ txt mrk, txt " " ] ++ itemtext
289+ mapM (uncurry mkitem) (zip markers bss)
290+blockToXml (BulletList bss) = do
291+ state <- get
292+ let level = parentBulletLevel state
293+ let pmrk = parentListMarker state
294+ let prefix = replicate (length pmrk) ' '
295+ let bullets = ["\x2022", "\x25e6", "*", "\x2043", "\x2023"]
296+ let mrk = prefix ++ bullets !! (level `mod` (length bullets))
297+ let mkitem bs = do
298+ modify (\s -> s { parentBulletLevel = (level+1) })
299+ itemtext <- cMapM blockToXml . paraToPlain $ bs
300+ modify (\s -> s { parentBulletLevel = level }) -- restore bullet level
301+ return $ el "p" $ [ txt (mrk ++ " ") ] ++ itemtext
302+ mapM mkitem bss
303+blockToXml (DefinitionList defs) =
304+ cMapM mkdef defs
305+ where
306+ mkdef (term, bss) = do
307+ def <- cMapM (cMapM blockToXml . sep . paraToPlain . map indent) bss
308+ t <- wrap "strong" term
309+ return [ el "p" t, el "p" def ]
310+ sep blocks =
311+ if all needsBreak blocks then
312+ blocks ++ [Plain [LineBreak]]
313+ else
314+ blocks
315+ needsBreak (Para _) = False
316+ needsBreak (Plain ins) = LineBreak `notElem` ins
317+ needsBreak _ = True
318+blockToXml (Header _ _) = -- should never happen, see renderSections
319+ error "unexpected header in section text"
320+blockToXml HorizontalRule = return
321+ [ el "empty-line" ()
322+ , el "p" (txt (replicate 10 '—'))
323+ , el "empty-line" () ]
324+blockToXml (Table caption aligns _ headers rows) = do
325+ hd <- mkrow "th" headers aligns
326+ bd <- mapM (\r -> mkrow "td" r aligns) rows
327+ c <- return . el "emphasis" =<< cMapM toXml caption
328+ return [el "table" (hd : bd), el "p" c]
329+ where
330+ mkrow :: String -> [TableCell] -> [Alignment] -> FBM Content
331+ mkrow tag cells aligns' =
332+ (el "tr") `liftM` (mapM (mkcell tag) (zip cells aligns'))
333+ --
334+ mkcell :: String -> (TableCell, Alignment) -> FBM Content
335+ mkcell tag (cell, align) = do
336+ cblocks <- cMapM blockToXml cell
337+ return $ el tag ([align_attr align], cblocks)
338+ --
339+ align_attr a = Attr (QName "align" Nothing Nothing) (align_str a)
340+ align_str AlignLeft = "left"
341+ align_str AlignCenter = "center"
342+ align_str AlignRight = "right"
343+ align_str AlignDefault = "left"
344+blockToXml Null = return []
345+
346+-- Replace paragraphs with plain text and line break.
347+-- Necessary to simulate multi-paragraph lists in FB2.
348+paraToPlain :: [Block] -> [Block]
349+paraToPlain [] = []
350+paraToPlain (Para inlines : rest) =
351+ let p = (Plain (inlines ++ [LineBreak]))
352+ in p : paraToPlain rest
353+paraToPlain (p:rest) = p : paraToPlain rest
354+
355+-- Simulate increased indentation level. Will not really work
356+-- for multi-line paragraphs.
357+indent :: Block -> Block
358+indent = indentBlock
359+ where
360+ -- indentation space
361+ spacer :: String
362+ spacer = replicate 4 ' '
363+ --
364+ indentBlock (Plain ins) = Plain ((Str spacer):ins)
365+ indentBlock (Para ins) = Para ((Str spacer):ins)
366+ indentBlock (CodeBlock a s) =
367+ let s' = unlines . map (spacer++) . lines $ s
368+ in CodeBlock a s'
369+ indentBlock (BlockQuote bs) = BlockQuote (map indent bs)
370+ indentBlock (Header l ins) = Header l (indentLines ins)
371+ indentBlock everythingElse = everythingElse
372+ -- indent every (explicit) line
373+ indentLines :: [Inline] -> [Inline]
374+ indentLines ins = let lns = split isLineBreak ins :: [[Inline]]
375+ in intercalate [LineBreak] $ map ((Str spacer):) lns
376+
377+-- | Convert a Pandoc's Inline element to FictionBook XML representation.
378+toXml :: Inline -> FBM [Content]
379+toXml (Str s) = return [txt s]
380+toXml (Emph ss) = list `liftM` wrap "emphasis" ss
381+toXml (Strong ss) = list `liftM` wrap "strong" ss
382+toXml (Strikeout ss) = list `liftM` wrap "strikethrough" ss
383+toXml (Superscript ss) = list `liftM` wrap "sup" ss
384+toXml (Subscript ss) = list `liftM` wrap "sub" ss
385+toXml (SmallCaps ss) = cMapM toXml $ bottomUp (map toUpper) ss
386+toXml (Quoted SingleQuote ss) = do -- FIXME: should be language-specific
387+ inner <- cMapM toXml ss
388+ return $ [txt "‘"] ++ inner ++ [txt "’"]
389+toXml (Quoted DoubleQuote ss) = do
390+ inner <- cMapM toXml ss
391+ return $ [txt "“"] ++ inner ++ [txt "”"]
392+toXml (Cite _ ss) = cMapM toXml ss -- FIXME: support citation styles
393+toXml (Code _ s) = return [el "code" s]
394+toXml Space = return [txt " "]
395+toXml LineBreak = return [el "empty-line" ()]
396+toXml (Math _ formula) = insertMath InlineImage formula
397+toXml (RawInline _ _) = return [] -- raw TeX and raw HTML are suppressed
398+toXml (Link text (url,ttl)) = do
399+ fns <- footnotes `liftM` get
400+ let n = 1 + length fns
401+ let ln_id = linkID n
402+ let ln_ref = list . el "sup" . txt $ "[" ++ show n ++ "]"
403+ ln_text <- cMapM toXml text
404+ let ln_desc =
405+ let ttl' = dropWhile isSpace ttl
406+ in if null ttl'
407+ then list . el "p" $ el "code" url
408+ else list . el "p" $ [ txt (ttl' ++ ": "), el "code" url ]
409+ modify (\s -> s { footnotes = (n, ln_id, ln_desc) : fns })
410+ return $ ln_text ++
411+ [ el "a"
412+ ( [ attr ("l","href") ('#':ln_id)
413+ , uattr "type" "note" ]
414+ , ln_ref) ]
415+toXml img@(Image _ _) = insertImage InlineImage img
416+toXml (Note bs) = do
417+ fns <- footnotes `liftM` get
418+ let n = 1 + length fns
419+ let fn_id = footnoteID n
420+ fn_desc <- cMapM blockToXml bs
421+ modify (\s -> s { footnotes = (n, fn_id, fn_desc) : fns })
422+ let fn_ref = el "sup" . txt $ "[" ++ show n ++ "]"
423+ return . list $ el "a" ( [ attr ("l","href") ('#':fn_id)
424+ , uattr "type" "note" ]
425+ , fn_ref )
426+
427+insertMath :: ImageMode -> String -> FBM [Content]
428+insertMath immode formula = do
429+ htmlMath <- return . writerHTMLMathMethod . writerOptions =<< get
430+ case htmlMath of
431+ WebTeX url -> do
432+ let alt = [Code nullAttr formula]
433+ let imgurl = url ++ urlEncode formula
434+ let img = Image alt (imgurl, "")
435+ insertImage immode img
436+ _ -> return [el "code" formula]
437+
438+insertImage :: ImageMode -> Inline -> FBM [Content]
439+insertImage immode (Image alt (url,ttl)) = do
440+ images <- imagesToFetch `liftM` get
441+ let n = 1 + length images
442+ let fname = "image" ++ show n
443+ modify (\s -> s { imagesToFetch = (fname, url) : images })
444+ let ttlattr = case (immode, null ttl) of
445+ (NormalImage, False) -> [ uattr "title" ttl ]
446+ _ -> []
447+ return . list $
448+ el "image" $
449+ [ attr ("l","href") ('#':fname)
450+ , attr ("l","type") (show immode)
451+ , uattr "alt" (cMap plain alt) ]
452+ ++ ttlattr
453+insertImage _ _ = error "unexpected inline instead of image"
454+
455+replaceImagesWithAlt :: [String] -> Content -> Content
456+replaceImagesWithAlt missingHrefs body =
457+ let cur = XC.fromContent body
458+ cur' = replaceAll cur
459+ in XC.toTree . XC.root $ cur'
460+ where
461+ --
462+ replaceAll :: XC.Cursor -> XC.Cursor
463+ replaceAll c =
464+ let n = XC.current c
465+ c' = if isImage n && isMissing n
466+ then XC.modifyContent replaceNode c
467+ else c
468+ in case XC.nextDF c' of
469+ (Just cnext) -> replaceAll cnext
470+ Nothing -> c' -- end of document
471+ --
472+ isImage :: Content -> Bool
473+ isImage (Elem e) = (elName e) == (uname "image")
474+ isImage _ = False
475+ --
476+ isMissing (Elem img@(Element _ _ _ _)) =
477+ let imgAttrs = elAttribs img
478+ badAttrs = map (attr ("l","href")) missingHrefs
479+ in any (`elem` imgAttrs) badAttrs
480+ isMissing _ = False
481+ --
482+ replaceNode :: Content -> Content
483+ replaceNode n@(Elem img@(Element _ _ _ _)) =
484+ let attrs = elAttribs img
485+ alt = getAttrVal attrs (uname "alt")
486+ imtype = getAttrVal attrs (qname "l" "type")
487+ in case (alt, imtype) of
488+ (Just alt', Just imtype') ->
489+ if imtype' == show NormalImage
490+ then el "p" alt'
491+ else txt alt'
492+ (Just alt', Nothing) -> txt alt' -- no type attribute
493+ _ -> n -- don't replace if alt text is not found
494+ replaceNode n = n
495+ --
496+ getAttrVal :: [X.Attr] -> QName -> Maybe String
497+ getAttrVal attrs name =
498+ case filter ((name ==) . attrKey) attrs of
499+ (a:_) -> Just (attrVal a)
500+ _ -> Nothing
501+
502+
503+-- | Wrap all inlines with an XML tag (given its unqualified name).
504+wrap :: String -> [Inline] -> FBM Content
505+wrap tagname inlines = el tagname `liftM` cMapM toXml inlines
506+
507+-- " Create a singleton list.
508+list :: a -> [a]
509+list = (:[])
510+
511+-- | Convert an 'Inline' to plaintext.
512+plain :: Inline -> String
513+plain (Str s) = s
514+plain (Emph ss) = concat (map plain ss)
515+plain (Strong ss) = concat (map plain ss)
516+plain (Strikeout ss) = concat (map plain ss)
517+plain (Superscript ss) = concat (map plain ss)
518+plain (Subscript ss) = concat (map plain ss)
519+plain (SmallCaps ss) = concat (map plain ss)
520+plain (Quoted _ ss) = concat (map plain ss)
521+plain (Cite _ ss) = concat (map plain ss) -- FIXME
522+plain (Code _ s) = s
523+plain Space = " "
524+plain LineBreak = "\n"
525+plain (Math _ s) = s
526+plain (RawInline _ s) = s
527+plain (Link text (url,_)) = concat (map plain text ++ [" <", url, ">"])
528+plain (Image alt _) = concat (map plain alt)
529+plain (Note _) = "" -- FIXME
530+
531+-- | Create an XML element.
532+el :: (Node t)
533+ => String -- ^ unqualified element name
534+ -> t -- ^ node contents
535+ -> Content -- ^ XML content
536+el name cs = Elem $ unode name cs
537+
538+-- | Put empty lines around content
539+spaceBeforeAfter :: [Content] -> [Content]
540+spaceBeforeAfter cs =
541+ let emptyline = el "empty-line" ()
542+ in [emptyline] ++ cs ++ [emptyline]
543+
544+-- | Create a plain-text XML content.
545+txt :: String -> Content
546+txt s = Text $ CData CDataText s Nothing
547+
548+-- | Create an XML attribute with an unqualified name.
549+uattr :: String -> String -> Text.XML.Light.Attr
550+uattr name val = Attr (uname name) val
551+
552+-- | Create an XML attribute with a qualified name from given namespace.
553+attr :: (String, String) -> String -> Text.XML.Light.Attr
554+attr (ns, name) val = Attr (qname ns name) val
555+
556+-- | Unqualified name
557+uname :: String -> QName
558+uname name = QName name Nothing Nothing
559+
560+-- | Qualified name
561+qname :: String -> String -> QName
562+qname ns name = QName name Nothing (Just ns)
563+
564+-- | Abbreviation for 'concatMap'.
565+cMap :: (a -> [b]) -> [a] -> [b]
566+cMap = concatMap
567+
568+-- | Monadic equivalent of 'concatMap'.
569+cMapM :: (Monad m) => (a -> m [b]) -> [a] -> m [b]
570+cMapM f xs = concat `liftM` mapM f xs
src/pandoc.hs+17−7
@@ -56,7 +56,8 @@ import Control.Monad (when, unless, liftM)
5656import Network.HTTP (simpleHTTP, mkRequest, getResponseBody, RequestMethod(..))
5757import Network.URI (parseURI, isURI, URI(..))
5858import qualified Data.ByteString.Lazy as B
59−import Data.ByteString.Lazy.UTF8 (toString )
59+import Data.ByteString.Lazy.UTF8 (toString)
60+import Codec.Binary.UTF8.String (decodeString, encodeString)
6061import Text.CSL.Reference (Reference(..))
6162#if MIN_VERSION_base(4,4,0)
6263#else
@@ -694,8 +695,11 @@ options =
694695usageMessage :: String -> [OptDescr (Opt -> IO Opt)] -> String
695696usageMessage programName = usageInfo
696697 (programName ++ " [OPTIONS] [FILES]" ++ "\nInput formats: " ++
697− (wrapWords 16 78 $ map fst readers) ++ "\nOutput formats: " ++
698− (wrapWords 16 78 $ map fst writers ++ nonTextFormats) ++ "\nOptions:")
698+ (wrapWords 16 78 $ readers'names) ++ "\nOutput formats: " ++
699+ (wrapWords 16 78 $ writers'names ++ nonTextFormats) ++ "\nOptions:")
700+ where
701+ writers'names = map fst writers ++ map fst iowriters
702+ readers'names = map fst readers
699703
700704-- Determine default reader based on source file extensions
701705defaultReaderName :: String -> [FilePath] -> String
@@ -752,6 +756,7 @@ defaultWriterName x =
752756 ".org" -> "org"
753757 ".asciidoc" -> "asciidoc"
754758 ".pdf" -> "latex"
759+ ".fb2" -> "fb2"
755760 ['.',y] | y `elem` ['1'..'9'] -> "man"
756761 _ -> "html"
757762
@@ -1042,8 +1047,13 @@ main = do
10421047 writerFn "-" = UTF8.putStr
10431048 writerFn f = UTF8.writeFile f
10441049
1045− case lookup writerName' writers of
1046− Nothing
1050+ let purewriter = lookup writerName' writers
1051+ let iowriter = lookup writerName' iowriters
1052+ case (purewriter, iowriter) of
1053+ (Nothing, Just iow) -> do
1054+ d <- iow writerOptions doc2
1055+ writerFn outputFile d
1056+ (Nothing, Nothing)
10471057 | writerName' == "epub" ->
10481058 writeEPUB epubStylesheet epubFonts writerOptions doc2
10491059 >>= writeBinary
@@ -1052,13 +1062,13 @@ main = do
10521062 | writerName' == "docx" ->
10531063 writeDocx referenceDocx writerOptions doc2 >>= writeBinary
10541064 | otherwise -> err 9 ("Unknown writer: " ++ writerName')
1055− Just w
1065+ (Just w, _)
10561066 | pdfOutput -> do
10571067 res <- tex2pdf latexEngine $ w writerOptions doc2
10581068 case res of
10591069 Right pdf -> writeBinary pdf
10601070 Left err' -> err 43 $ toString err'
1061− Just w
1071+ (Just w, _)
10621072 | htmlFormat && ascii ->
10631073 writerFn outputFile . toEntities =<< selfcontain result
10641074 | otherwise ->