@@ -0,0 +1,604 @@
2+Copyright (C) 2014 Jesse Rosenthal <jrosenthal@jhu.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.Readers.DocX.Parse
21+ Copyright : Copyright (C) 2014 Jesse Rosenthal
22+ License : GNU GPL, version 2 or above
24+ Maintainer : Jesse Rosenthal <jrosenthal@jhu.edu>
26+ Portability : portable
28+Conversion of DocX archive into DocX haskell type
32+module Text.Pandoc.Readers.DocX.Parse ( DocX(..)
54+import Codec.Archive.Zip
58+import System.FilePath
59+import Data.Bits ((.|.))
60+import qualified Data.ByteString.Lazy as B
61+import qualified Text.Pandoc.UTF8 as UTF8
63+attrToNSPair :: Attr -> Maybe (String, String)
64+attrToNSPair (Attr (QName s _ (Just "xmlns")) val) = Just (s, val)
65+attrToNSPair _ = Nothing
68+type NameSpaces = [(String, String)]
70+data DocX = DocX Document Notes Numbering [Relationship] Media
73+archiveToDocX :: Archive -> Maybe DocX
74+archiveToDocX archive = do
75+ let notes = archiveToNotes archive
76+ rels = archiveToRelationships archive
77+ media = archiveToMedia archive
78+ doc <- archiveToDocument archive
79+ numbering <- archiveToNumbering archive
80+ return $ DocX doc notes numbering rels media
82+data Document = Document NameSpaces Body
85+archiveToDocument :: Archive -> Maybe Document
86+archiveToDocument zf = do
87+ entry <- findEntryByPath "word/document.xml" zf
88+ docElem <- (parseXMLDoc . UTF8.toStringLazy . fromEntry) entry
89+ let namespaces = mapMaybe attrToNSPair (elAttribs docElem)
90+ bodyElem <- findChild (QName "body" (lookup "w" namespaces) Nothing) docElem
91+ body <- elemToBody namespaces bodyElem
92+ return $ Document namespaces body
94+type Media = [(FilePath, B.ByteString)]
96+filePathIsMedia :: FilePath -> Bool
98+ let (dir, _) = splitFileName fp
100+ (dir == "word/media/")
102+getMediaPair :: Archive -> FilePath -> Maybe (FilePath, B.ByteString)
104+ case findEntryByPath fp zf of
105+ Just e -> Just (fp, fromEntry e)
108+archiveToMedia :: Archive -> Media
110+ mapMaybe (getMediaPair zf) (filter filePathIsMedia (filesInArchive zf))
112+data Numbering = Numbering NameSpaces [Numb] [AbstractNumb]
115+data Numb = Numb String String -- right now, only a key to an abstract num
118+data AbstractNumb = AbstractNumb String [Level]
121+-- (ilvl, format, string, start)
122+type Level = (String, String, String, Maybe Integer)
124+lookupLevel :: String -> String -> Numbering -> Maybe Level
125+lookupLevel numId ilvl (Numbering _ numbs absNumbs) = do
126+ absNumId <- lookup numId $ map (\(Numb nid absnumid) -> (nid, absnumid)) numbs
127+ lvls <- lookup absNumId $ map (\(AbstractNumb aid ls) -> (aid, ls)) absNumbs
128+ lvl <- lookup ilvl $ map (\l@(i, _, _, _) -> (i, l)) lvls
131+numElemToNum :: NameSpaces -> Element -> Maybe Numb
132+numElemToNum ns element |
133+ qName (elName element) == "num" &&
134+ qURI (elName element) == (lookup "w" ns) = do
135+ numId <- findAttr (QName "numId" (lookup "w" ns) (Just "w")) element
136+ absNumId <- findChild (QName "abstractNumId" (lookup "w" ns) (Just "w")) element
137+ >>= findAttr (QName "val" (lookup "w" ns) (Just "w"))
138+ return $ Numb numId absNumId
139+numElemToNum _ _ = Nothing
141+absNumElemToAbsNum :: NameSpaces -> Element -> Maybe AbstractNumb
142+absNumElemToAbsNum ns element |
143+ qName (elName element) == "abstractNum" &&
144+ qURI (elName element) == (lookup "w" ns) = do
145+ absNumId <- findAttr
146+ (QName "abstractNumId" (lookup "w" ns) (Just "w"))
148+ let levelElems = findChildren
149+ (QName "lvl" (lookup "w" ns) (Just "w"))
151+ levels = mapMaybe id $ map (levelElemToLevel ns) levelElems
152+ return $ AbstractNumb absNumId levels
153+absNumElemToAbsNum _ _ = Nothing
155+levelElemToLevel :: NameSpaces -> Element -> Maybe Level
156+levelElemToLevel ns element |
157+ qName (elName element) == "lvl" &&
158+ qURI (elName element) == (lookup "w" ns) = do
159+ ilvl <- findAttr (QName "ilvl" (lookup "w" ns) (Just "w")) element
160+ fmt <- findChild (QName "numFmt" (lookup "w" ns) (Just "w")) element
161+ >>= findAttr (QName "val" (lookup "w" ns) (Just "w"))
162+ txt <- findChild (QName "lvlText" (lookup "w" ns) (Just "w")) element
163+ >>= findAttr (QName "val" (lookup "w" ns) (Just "w"))
164+ let start = findChild (QName "start" (lookup "w" ns) (Just "w")) element
165+ >>= findAttr (QName "val" (lookup "w" ns) (Just "w"))
166+ >>= (\s -> listToMaybe (map fst (reads s :: [(Integer, String)])))
167+ return (ilvl, fmt, txt, start)
168+levelElemToLevel _ _ = Nothing
170+archiveToNumbering :: Archive -> Maybe Numbering
171+archiveToNumbering zf =
172+ case findEntryByPath "word/numbering.xml" zf of
173+ Nothing -> Just $ Numbering [] [] []
175+ numberingElem <- (parseXMLDoc . UTF8.toStringLazy . fromEntry) entry
176+ let namespaces = mapMaybe attrToNSPair (elAttribs numberingElem)
177+ numElems = findChildren
178+ (QName "num" (lookup "w" namespaces) (Just "w"))
180+ absNumElems = findChildren
181+ (QName "abstractNum" (lookup "w" namespaces) (Just "w"))
183+ nums = mapMaybe id $ map (numElemToNum namespaces) numElems
184+ absNums = mapMaybe id $ map (absNumElemToAbsNum namespaces) absNumElems
185+ return $ Numbering namespaces nums absNums
187+data Notes = Notes NameSpaces (Maybe [(String, [BodyPart])]) (Maybe [(String, [BodyPart])])
190+noteElemToNote :: NameSpaces -> Element -> Maybe (String, [BodyPart])
191+noteElemToNote ns element
192+ | qName (elName element) `elem` ["endnote", "footnote"] &&
193+ qURI (elName element) == (lookup "w" ns) =
195+ noteId <- findAttr (QName "id" (lookup "w" ns) (Just "w")) element
196+ let bps = map fromJust
198+ $ map (elemToBodyPart ns)
199+ $ filterChildrenName (isParOrTbl ns) element
200+ return $ (noteId, bps)
201+noteElemToNote _ _ = Nothing
203+getFootNote :: String -> Notes -> Maybe [BodyPart]
204+getFootNote s (Notes _ fns _) = fns >>= (lookup s)
206+getEndNote :: String -> Notes -> Maybe [BodyPart]
207+getEndNote s (Notes _ _ ens) = ens >>= (lookup s)
209+elemToNotes :: NameSpaces -> String -> Element -> Maybe [(String, [BodyPart])]
210+elemToNotes ns notetype element
211+ | qName (elName element) == (notetype ++ "s") &&
212+ qURI (elName element) == (lookup "w" ns) =
215+ $ map (noteElemToNote ns)
216+ $ findChildren (QName notetype (lookup "w" ns) (Just "w")) element
217+elemToNotes _ _ _ = Nothing
219+archiveToNotes :: Archive -> Notes
221+ let fnElem = findEntryByPath "word/footnotes.xml" zf
222+ >>= (parseXMLDoc . UTF8.toStringLazy . fromEntry)
223+ enElem = findEntryByPath "word/endnotes.xml" zf
224+ >>= (parseXMLDoc . UTF8.toStringLazy . fromEntry)
225+ fn_namespaces = case fnElem of
226+ Just e -> mapMaybe attrToNSPair (elAttribs e)
228+ en_namespaces = case enElem of
229+ Just e -> mapMaybe attrToNSPair (elAttribs e)
231+ ns = unionBy (\x y -> fst x == fst y) fn_namespaces en_namespaces
232+ fn = fnElem >>= (elemToNotes ns "footnote")
233+ en = enElem >>= (elemToNotes ns "endnote")
238+data Relationship = Relationship (RelId, Target)
241+lookupRelationship :: RelId -> [Relationship] -> Maybe Target
242+lookupRelationship relid rels =
243+ lookup relid (map (\(Relationship pair) -> pair) rels)
245+filePathIsRel :: FilePath -> Bool
247+ let (dir, name) = splitFileName fp
249+ (dir == "word/_rels/") && ((takeExtension name) == ".rels")
251+relElemToRelationship :: Element -> Maybe Relationship
252+relElemToRelationship element | qName (elName element) == "Relationship" =
254+ relId <- findAttr (QName "Id" Nothing Nothing) element
255+ target <- findAttr (QName "Target" Nothing Nothing) element
256+ return $ Relationship (relId, target)
257+relElemToRelationship _ = Nothing
260+archiveToRelationships :: Archive -> [Relationship]
261+archiveToRelationships archive =
262+ let relPaths = filter filePathIsRel (filesInArchive archive)
263+ entries = map fromJust $ filter isJust $ map (\f -> findEntryByPath f archive) relPaths
264+ relElems = map fromJust $ filter isJust $ map (parseXMLDoc . UTF8.toStringLazy . fromEntry) entries
265+ rels = map fromJust $ filter isJust $ map relElemToRelationship $ concatMap elChildren relElems
269+data Body = Body [BodyPart]
272+isParOrTbl :: NameSpaces -> QName -> Bool
273+isParOrTbl ns q = qName q `elem` ["p", "tbl"] &&
274+ qURI q == (lookup "w" ns)
276+elemToBody :: NameSpaces -> Element -> Maybe Body
277+elemToBody ns element | qName (elName element) == "body" && qURI (elName element) == (lookup "w" ns) =
281+ $ map (elemToBodyPart ns) $ filterChildrenName (isParOrTbl ns) element
282+elemToBody _ _ = Nothing
284+isRunOrLinkOrBookmark :: NameSpaces -> QName -> Bool
285+isRunOrLinkOrBookmark ns q = qName q `elem` ["r", "hyperlink", "bookmarkStart"] &&
286+ qURI q == (lookup "w" ns)
288+elemToNumInfo :: NameSpaces -> Element -> Maybe (String, String)
289+elemToNumInfo ns element
290+ | qName (elName element) == "p" &&
291+ qURI (elName element) == (lookup "w" ns) =
293+ pPr <- findChild (QName "pPr" (lookup "w" ns) (Just "w")) element
294+ numPr <- findChild (QName "numPr" (lookup "w" ns) (Just "w")) pPr
295+ lvl <- findChild (QName "ilvl" (lookup "w" ns) (Just "w")) numPr >>=
296+ findAttr (QName "val" (lookup "w" ns) (Just "w"))
297+ numId <- findChild (QName "numId" (lookup "w" ns) (Just "w")) numPr >>=
298+ findAttr (QName "val" (lookup "w" ns) (Just "w"))
300+elemToNumInfo _ _ = Nothing
302+-- isBookMarkTag :: NameSpaces -> QName -> Bool
303+-- isBookMarkTag ns q = qName q `elem` ["bookmarkStart", "bookmarkEnd"] &&
304+-- qURI q == (lookup "w" ns)
306+-- parChildrenToBookmark :: NameSpaces -> [Element] -> BookMark
307+-- parChildrenToBookmark ns (bms : bme : _)
308+-- | qName (elName bms) == "bookmarkStart" &&
309+-- qURI (elName bms) == (lookup "w" ns) &&
310+-- qName (elName bme) == "bookmarkEnd" &&
311+-- qURI (elName bme) == (lookup "w" ns) = do
312+-- bmId <- findAttr (QName "id" (lookup "w" ns) (Just "w")) bms
313+-- bmName <- findAttr (QName "name" (lookup "w" ns) (Just "w")) bms
314+-- return $ (bmId, bmName)
315+-- parChildrenToBookmark _ _ = Nothing
317+elemToBodyPart :: NameSpaces -> Element -> Maybe BodyPart
318+elemToBodyPart ns element
319+ | qName (elName element) == "p" &&
320+ qURI (elName element) == (lookup "w" ns) =
321+ let parstyle = elemToParagraphStyle ns element
322+ parparts = mapMaybe id
323+ $ map (elemToParPart ns)
324+ $ filterChildrenName (isRunOrLinkOrBookmark ns) element
326+ case elemToNumInfo ns element of
327+ Just (numId, lvl) -> Just $ ListItem parstyle numId lvl parparts
328+ Nothing -> Just $ Paragraph parstyle parparts
329+ | qName (elName element) == "tbl" &&
330+ qURI (elName element) == (lookup "w" ns) =
332+ caption = findChild (QName "tblPr" (lookup "w" ns) (Just "w")) element
333+ >>= findChild (QName "tblCaption" (lookup "w" ns) (Just "w"))
334+ >>= findAttr (QName "val" (lookup "w" ns) (Just "w"))
336+ findChild (QName "tblGrid" (lookup "w" ns) (Just "w")) element
338+ Just g -> elemToTblGrid ns g
340+ tblLook = findChild (QName "tblPr" (lookup "w" ns) (Just "w")) element
341+ >>= findChild (QName "tblLook" (lookup "w" ns) (Just "w"))
342+ >>= elemToTblLook ns
345+ (fromMaybe "" caption)
347+ (fromMaybe defaultTblLook tblLook)
348+ (mapMaybe (elemToRow ns) (elChildren element))
349+ | otherwise = Nothing
351+elemToTblLook :: NameSpaces -> Element -> Maybe TblLook
352+elemToTblLook ns element
353+ | qName (elName element) == "tblLook" &&
354+ qURI (elName element) == (lookup "w" ns) =
355+ let firstRow = findAttr (QName "firstRow" (lookup "w" ns) (Just "w")) element
356+ val = findAttr (QName "val" (lookup "w" ns) (Just "w")) element
361+ Nothing -> case val of
362+ Just bitMask -> testBitMask bitMask 0x020
365+ Just $ TblLook{firstRowFormatting = firstRowFmt}
366+elemToTblLook _ _ = Nothing
368+testBitMask :: String -> Int -> Bool
369+testBitMask bitMaskS n =
370+ case (reads ("0x" ++ bitMaskS) :: [(Int, String)]) of
372+ ((n', _) : _) -> ((n' .|. n) /= 0)
374+data ParagraphStyle = ParagraphStyle { pStyle :: [String]
375+ , indent :: Maybe Integer
379+defaultParagraphStyle :: ParagraphStyle
380+defaultParagraphStyle = ParagraphStyle { pStyle = []
384+elemToParagraphStyle :: NameSpaces -> Element -> ParagraphStyle
385+elemToParagraphStyle ns element =
386+ case findChild (QName "pPr" (lookup "w" ns) (Just "w")) element of
392+ (findAttr (QName "val" (lookup "w" ns) (Just "w")))
393+ (findChildren (QName "pStyle" (lookup "w" ns) (Just "w")) pPr)
395+ findChild (QName "ind" (lookup "w" ns) (Just "w")) pPr >>=
396+ findAttr (QName "left" (lookup "w" ns) (Just "w")) >>=
399+ Nothing -> defaultParagraphStyle
402+data BodyPart = Paragraph ParagraphStyle [ParPart]
403+ | ListItem ParagraphStyle String String [ParPart]
404+ | Tbl String TblGrid TblLook [Row]
408+type TblGrid = [Integer]
410+data TblLook = TblLook {firstRowFormatting::Bool}
413+defaultTblLook :: TblLook
414+defaultTblLook = TblLook{firstRowFormatting = False}
416+stringToInteger :: String -> Maybe Integer
417+stringToInteger s = listToMaybe $ map fst (reads s :: [(Integer, String)])
419+elemToTblGrid :: NameSpaces -> Element -> TblGrid
420+elemToTblGrid ns element
421+ | qName (elName element) == "tblGrid" &&
422+ qURI (elName element) == (lookup "w" ns) =
424+ cols = findChildren (QName "gridCol" (lookup "w" ns) (Just "w")) element
427+ findAttr (QName "val" (lookup "w" ns) (Just ("w"))) e
431+elemToTblGrid _ _ = []
433+data Row = Row [Cell]
437+elemToRow :: NameSpaces -> Element -> Maybe Row
439+ | qName (elName element) == "tr" &&
440+ qURI (elName element) == (lookup "w" ns) =
442+ cells = findChildren (QName "tc" (lookup "w" ns) (Just "w")) element
444+ Just $ Row (mapMaybe (elemToCell ns) cells)
445+elemToRow _ _ = Nothing
447+data Cell = Cell [BodyPart]
450+elemToCell :: NameSpaces -> Element -> Maybe Cell
451+elemToCell ns element
452+ | qName (elName element) == "tc" &&
453+ qURI (elName element) == (lookup "w" ns) =
454+ Just $ Cell (mapMaybe (elemToBodyPart ns) (elChildren element))
455+elemToCell _ _ = Nothing
457+data ParPart = PlainRun Run
458+ | BookMark BookMarkId Anchor
459+ | InternalHyperLink Anchor [Run]
460+ | ExternalHyperLink RelId [Run]
464+data Run = Run RunStyle [RunElem]
469+data RunElem = TextRun String | LnBrk
472+data RunStyle = RunStyle { isBold :: Bool
474+ , isSmallCaps :: Bool
476+ , isSuperScript :: Bool
477+ , isSubScript :: Bool
478+ , underline :: Maybe String
479+ , rStyle :: Maybe String }
482+defaultRunStyle :: RunStyle
483+defaultRunStyle = RunStyle { isBold = False
485+ , isSmallCaps = False
487+ , isSuperScript = False
488+ , isSubScript = False
489+ , underline = Nothing
493+elemToRunStyle :: NameSpaces -> Element -> RunStyle
494+elemToRunStyle ns element =
495+ case findChild (QName "rPr" (lookup "w" ns) (Just "w")) element of
499+ isBold = isJust $ findChild (QName "b" (lookup "w" ns) (Just "w")) rPr
500+ , isItalic = isJust $ findChild (QName "i" (lookup "w" ns) (Just "w")) rPr
501+ , isSmallCaps = isJust $ findChild (QName "smallCaps" (lookup "w" ns) (Just "w")) rPr
502+ , isStrike = isJust $ findChild (QName "strike" (lookup "w" ns) (Just "w")) rPr
504+ (Just "superscript" ==
505+ (findChild (QName "vertAlign" (lookup "w" ns) (Just "w")) rPr >>=
506+ findAttr (QName "val" (lookup "w" ns) (Just "w"))))
508+ (Just "subscript" ==
509+ (findChild (QName "vertAlign" (lookup "w" ns) (Just "w")) rPr >>=
510+ findAttr (QName "val" (lookup "w" ns) (Just "w"))))
512+ findChild (QName "u" (lookup "w" ns) (Just "w")) rPr >>=
513+ findAttr (QName "val" (lookup "w" ns) (Just "w"))
515+ findChild (QName "rStyle" (lookup "w" ns) (Just "w")) rPr >>=
516+ findAttr (QName "val" (lookup "w" ns) (Just "w"))
518+ Nothing -> defaultRunStyle
520+elemToRun :: NameSpaces -> Element -> Maybe Run
522+ | qName (elName element) == "r" &&
523+ qURI (elName element) == (lookup "w" ns) =
525+ findChild (QName "footnoteReference" (lookup "w" ns) (Just "w")) element >>=
526+ findAttr (QName "id" (lookup "w" ns) (Just "w"))
528+ Just s -> Just $ Footnote s
531+ findChild (QName "endnoteReference" (lookup "w" ns) (Just "w")) element >>=
532+ findAttr (QName "id" (lookup "w" ns) (Just "w"))
534+ Just s -> Just $ Endnote s
536+ Run (elemToRunStyle ns element)
537+ (elemToRunElems ns element)
538+elemToRun _ _ = Nothing
540+elemToRunElem :: NameSpaces -> Element -> Maybe RunElem
541+elemToRunElem ns element
542+ | qName (elName element) == "t" &&
543+ qURI (elName element) == (lookup "w" ns) =
544+ Just $ TextRun (strContent element)
545+ | qName (elName element) == "br" &&
546+ qURI (elName element) == (lookup "w" ns) =
548+ | otherwise = Nothing
551+elemToRunElems :: NameSpaces -> Element -> [RunElem]
552+elemToRunElems ns element
553+ | qName (elName element) == "r" &&
554+ qURI (elName element) == (lookup "w" ns) =
555+ mapMaybe (elemToRunElem ns) (elChildren element)
558+elemToDrawing :: NameSpaces -> Element -> Maybe ParPart
559+elemToDrawing ns element
560+ | qName (elName element) == "drawing" &&
561+ qURI (elName element) == (lookup "w" ns) =
562+ let a_ns = "http://schemas.openxmlformats.org/drawingml/2006/main"
564+ findElement (QName "blip" (Just a_ns) (Just "a")) element
565+ >>= findAttr (QName "embed" (lookup "r" ns) (Just "r"))
566+ >>= (\s -> Just $ Drawing s)
567+elemToDrawing _ _ = Nothing
570+elemToParPart :: NameSpaces -> Element -> Maybe ParPart
571+elemToParPart ns element
572+ | qName (elName element) == "r" &&
573+ qURI (elName element) == (lookup "w" ns) =
574+ case findChild (QName "drawing" (lookup "w" ns) (Just "w")) element of
575+ Just drawingElem -> elemToDrawing ns drawingElem
577+ r <- elemToRun ns element
579+elemToParPart ns element
580+ | qName (elName element) == "bookmarkStart" &&
581+ qURI (elName element) == (lookup "w" ns) = do
582+ bmId <- findAttr (QName "id" (lookup "w" ns) (Just "w")) element
583+ bmName <- findAttr (QName "name" (lookup "w" ns) (Just "w")) element
584+ return $ BookMark bmId bmName
585+elemToParPart ns element
586+ | qName (elName element) == "hyperlink" &&
587+ qURI (elName element) == (lookup "w" ns) =
588+ let runs = map fromJust $ filter isJust $ map (elemToRun ns)
589+ $ findChildren (QName "r" (lookup "w" ns) (Just "w")) element
591+ case findAttr (QName "anchor" (lookup "w" ns) (Just "w")) element of
593+ Just $ InternalHyperLink anchor runs
595+ case findAttr (QName "id" (lookup "r" ns) (Just "r")) element of
596+ Just relId -> Just $ ExternalHyperLink relId runs
598+elemToParPart _ _ = Nothing
602+type BookMarkId = String