diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright Dino Morelli 2010
+Copyright Dino Morelli 2010, 2011
 
 All rights reserved.
 
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,6 +1,6 @@
 #! /usr/bin/env runhaskell
 
--- Copyright: 2010 Dino Morelli
+-- Copyright: 2010, 2011 Dino Morelli
 -- License: BSD3 (see LICENSE)
 -- Author: Dino Morelli <dino@ui3.info>
 
diff --git a/TODO b/TODO
--- a/TODO
+++ b/TODO
@@ -1,1 +1,3 @@
 - Better errors on parse failure, this will emerge along with more unit testing
+
+- More unit testing
diff --git a/epub-metadata.cabal b/epub-metadata.cabal
--- a/epub-metadata.cabal
+++ b/epub-metadata.cabal
@@ -1,36 +1,45 @@
 name:                epub-metadata
-version:             1.0.2
+version:             2.0.0
 cabal-version:       >= 1.2
 build-type:          Simple
 license:             BSD3
 license-file:        LICENSE
-copyright:           2010 Dino Morelli
+copyright:           2010, 2011 Dino Morelli
 author:              Dino Morelli
 maintainer:          Dino Morelli <dino@ui3.info>
 stability:           experimental
 homepage:            http://ui3.info/d/proj/epub-metadata.html
-synopsis:            Library and utility for parsing and manipulating 
-                     ePub metadata
-description:         Library for parsing and manipulating ePub OPF 
-                     metadata. An attempt has been made here to very 
-                     thoroughly implement the metadata portion of the OPF 
-                     Package Document specification. Also included is a 
-                     command-line utility to dump ePub metadata to stdout 
-                     in a human-readable form.
+synopsis:            Library and utility for parsing and manipulating ePub OPF package data
+description:         Library and utility for parsing and manipulating ePub 
+                     OPF package data. An attempt has been made here to 
+                     very thoroughly implement the OPF Package Document 
+                     specification. Also included is a command-line 
+                     utility to dump OPF package data to stdout in a 
+                     human-readable form.
 category:            Codec, Text
 tested-with:         GHC>=6.12.1
 
 library
    exposed-modules:  Codec.Epub.IO,
-                     Codec.Epub.Opf.Metadata,
-                     Codec.Epub.Opf.Metadata.Format,
-                     Codec.Epub.Opf.Metadata.Parse
+                     Codec.Epub.Opf.Common,
+                     Codec.Epub.Opf.Format.Guide,
+                     Codec.Epub.Opf.Format.Manifest,
+                     Codec.Epub.Opf.Format.Metadata,
+                     Codec.Epub.Opf.Format.Package,
+                     Codec.Epub.Opf.Format.Spine,
+                     Codec.Epub.Opf.Format.Util,
+                     Codec.Epub.Opf.Package,
+                     Codec.Epub.Opf.Package.Guide,
+                     Codec.Epub.Opf.Package.Manifest,
+                     Codec.Epub.Opf.Package.Metadata,
+                     Codec.Epub.Opf.Package.Spine,
+                     Codec.Epub.Opf.Parse
    hs-source-dirs:   src
-   build-depends:    base >= 3 && < 5, HSH, hxt, mtl
+   build-depends:    base >= 3 && < 5, containers, HSH, hxt >= 9, mtl
    ghc-options:      -Wall
 
 executable           epub-meta
-   main-is:          Codec/Epub/Opf/epub-meta.hs
+   main-is:          Codec/Epub/Opf/Cli/epub-meta.hs
    hs-source-dirs:   src
    build-depends:    base >= 3 && < 5
    ghc-options:      -Wall
diff --git a/src/Codec/Epub/IO.hs b/src/Codec/Epub/IO.hs
--- a/src/Codec/Epub/IO.hs
+++ b/src/Codec/Epub/IO.hs
@@ -1,4 +1,4 @@
--- Copyright: 2010 Dino Morelli
+-- Copyright: 2010, 2011 Dino Morelli
 -- License: BSD3 (see LICENSE)
 -- Author: Dino Morelli <dino@ui3.info>
 
@@ -13,10 +13,13 @@
    ( extractFileFromZip, opfPath )
    where
 
+import Control.Arrow.ListArrows ( (>>>), deep )
 import Control.Monad.Error
 import HSH.Command
 import Text.Printf
-import Text.XML.HXT.Arrow
+import Text.XML.HXT.Arrow.XmlArrow ( getAttrValue, hasName, isElem )
+import Text.XML.HXT.Arrow.XmlState ( no, runX, withValidate )
+import Text.XML.HXT.Arrow.ReadDocument ( readString )
 
 
 {- | Extract a file from a zipfile.
@@ -46,7 +49,7 @@
       "META-INF/container.xml"
 
    result <- liftIO $ runX (
-      readString [(a_validate, v_0)] containerContents
+      readString [withValidate no] containerContents
       >>> deep (isElem >>> hasName "rootfile")
       >>> getAttrValue "full-path"
       )
diff --git a/src/Codec/Epub/Opf/Cli/Opts.hs b/src/Codec/Epub/Opf/Cli/Opts.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Cli/Opts.hs
@@ -0,0 +1,56 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module Codec.Epub.Opf.Cli.Opts
+   ( Options (..)
+   , parseOpts, usageText
+   )
+   where
+
+import System.Console.GetOpt
+
+
+data Options = Options
+   { optHelp :: Bool
+   , optVerbose :: Bool
+   }
+
+
+defaultOptions :: Options
+defaultOptions = Options
+   { optHelp = False
+   , optVerbose = False
+   }
+
+
+options :: [OptDescr (Options -> Options)]
+options =
+   [ Option ['h'] ["help"] 
+      (NoArg (\opts -> opts { optHelp = True } ))
+      "This help text"
+   , Option ['v'] ["verbose"]
+      (NoArg (\opts -> opts { optVerbose = True } )) 
+      "Display all OPF package info, including manifest, spine and guide"
+   ]
+
+
+parseOpts :: [String] -> IO (Options, [String])
+parseOpts argv = 
+   case getOpt Permute options argv of
+      (o,n,[]  ) -> return (foldl (flip id) defaultOptions o, n)
+      (_,_,errs) -> ioError $ userError (concat errs ++ usageText)
+
+
+usageText :: String
+usageText = (usageInfo header options) ++ "\n" ++ footer
+   where
+      header = init $ unlines
+         [ "Usage: epub-meta [OPTIONS] EPUBFILE"
+         , "Examine ePub OPF package data"
+         , ""
+         , "Options:"
+         ]
+      footer = init $ unlines
+         [ "Version 2.0.0  Dino Morelli <dino@ui3.info>"
+         ]
diff --git a/src/Codec/Epub/Opf/Cli/epub-meta.hs b/src/Codec/Epub/Opf/Cli/epub-meta.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Cli/epub-meta.hs
@@ -0,0 +1,25 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+import Control.Monad.Error
+import System.Environment ( getArgs )
+import System.Exit
+
+import Codec.Epub.Opf.Cli.Opts
+import Codec.Epub.Opf.Format.Package
+import Codec.Epub.Opf.Parse
+
+
+main :: IO ()
+main = do
+   (opts, paths) <- getArgs >>= parseOpts
+
+   when ((optHelp opts) || (null paths)) $ do
+      putStrLn usageText
+      exitWith $ ExitFailure 1
+
+   let zipPath = head paths
+   result <- runErrorT $ parseEpubOpf zipPath
+
+   putStr $ either id (formatPackage (optVerbose opts)) result
diff --git a/src/Codec/Epub/Opf/Common.hs b/src/Codec/Epub/Opf/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Common.hs
@@ -0,0 +1,17 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{- | Data types for working with the metadata of ePub documents
+
+   These data types were constructed by studying the IDPF OPF 
+   specification for ePub documents found here:
+
+   <http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html>
+-}
+module Codec.Epub.Opf.Common
+   where
+
+
+-- | Attribute value used to relate things to each other in the OPF data
+type MFItemId = String
diff --git a/src/Codec/Epub/Opf/Format/Guide.hs b/src/Codec/Epub/Opf/Format/Guide.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Format/Guide.hs
@@ -0,0 +1,31 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+-- | Module for pretty-printing ePub metadata info
+module Codec.Epub.Opf.Format.Guide
+   where
+
+import Control.Monad.Writer.Lazy
+import Text.Printf
+
+import Codec.Epub.Opf.Format.Util
+import Codec.Epub.Opf.Package.Guide
+
+
+tellGuideRef :: MonadWriter (Seq Char) m => GuideRef -> m ()
+tellGuideRef (GuideRef grty title href) =
+   tellSeq $ printf "   type: %s%s, href: %s\n"
+      grty (titleToString title) href
+
+   where
+      titleToString = maybe "" (printf ", title: %s")
+
+
+tellGuide :: MonadWriter (Seq Char) m => [GuideRef] -> m ()
+tellGuide []  = return ()
+tellGuide grs = do
+   tellSeq "guide items:\n"
+   mapM_ tellGuideRef grs
diff --git a/src/Codec/Epub/Opf/Format/Manifest.hs b/src/Codec/Epub/Opf/Format/Manifest.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Format/Manifest.hs
@@ -0,0 +1,27 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+-- | Module for pretty-printing ePub metadata info
+module Codec.Epub.Opf.Format.Manifest
+   where
+
+import Control.Monad.Writer.Lazy
+import Text.Printf
+
+import Codec.Epub.Opf.Format.Util
+import Codec.Epub.Opf.Package.Manifest
+
+
+tellManifestItem :: MonadWriter (Seq Char) m => ManifestItem -> m ()
+tellManifestItem (ManifestItem mfId href mediaType) =
+   tellSeq $ printf "   id: %s, href: %s, media-type: %s\n"
+      mfId href mediaType
+
+
+tellManifest :: MonadWriter (Seq Char) m => [ManifestItem] -> m ()
+tellManifest mas = do
+   tellSeq "manifest items:\n"
+   mapM_ tellManifestItem mas
diff --git a/src/Codec/Epub/Opf/Format/Metadata.hs b/src/Codec/Epub/Opf/Format/Metadata.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Format/Metadata.hs
@@ -0,0 +1,120 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+-- | Module for pretty-printing ePub metadata info
+module Codec.Epub.Opf.Format.Metadata
+   where
+
+import Control.Monad.Writer.Lazy
+import Text.Printf
+
+import Codec.Epub.Opf.Format.Util
+import Codec.Epub.Opf.Package.Metadata
+
+
+tellTitle :: MonadWriter (Seq Char) m => MetaTitle -> m ()
+tellTitle (MetaTitle Nothing title) = tellSeq $ printf "title: %s\n" title
+tellTitle (MetaTitle lang title) =
+   tellSeq $ printf "title\n%s%s" (formatSubline "lang" lang)
+      (formatSubline "title" (Just title))
+
+
+tellCreator :: MonadWriter (Seq Char) m => MetaCreator -> m ()
+tellCreator (MetaCreator Nothing Nothing creator) =
+   tellSeq $ printf "creator: %s\n" creator
+tellCreator (MetaCreator role fileAs creator) =
+   tellSeq $ printf "creator\n%s%s%s"
+      (formatSubline "role" role)
+      (formatSubline "file-as" fileAs)
+      (formatSubline "creator" (Just creator))
+
+
+tellContributor :: MonadWriter (Seq Char) m => MetaCreator -> m ()
+tellContributor (MetaCreator Nothing Nothing contributor) =
+   tellSeq $ printf "contributor: %s\n" contributor
+tellContributor (MetaCreator role fileAs contributor) =
+   tellSeq $ printf "contributor\n%s%s%s"
+      (formatSubline "role" role)
+      (formatSubline "file-as" fileAs)
+      (formatSubline "creator" (Just contributor))
+
+
+tellDate :: MonadWriter (Seq Char) m => MetaDate -> m ()
+tellDate (MetaDate Nothing date) =
+   tellSeq $ printf "date: %s\n" date
+tellDate (MetaDate event date) =
+   tellSeq $ printf "date\n%s%s"
+      (formatSubline "event" event)
+      (formatSubline "date" (Just date))
+
+
+tellType :: MonadWriter (Seq Char) m => Maybe String -> m ()
+tellType = maybe (return ()) (tellSeq . (printf "type: %s\n"))
+
+
+tellFormat :: MonadWriter (Seq Char) m => Maybe String -> m ()
+tellFormat = maybe (return ()) (tellSeq . (printf "format: %s\n"))
+
+
+tellId :: MonadWriter (Seq Char) m => MetaId -> m ()
+tellId (MetaId idVal scheme content) =
+   tellSeq $ printf "identifier\n%s%s%s"
+      (formatSubline "id" (Just idVal))
+      (formatSubline "scheme" scheme)
+      (formatSubline "identifier" (Just content))
+
+
+tellSource :: MonadWriter (Seq Char) m => Maybe String -> m ()
+tellSource = maybe (return ()) (tellSeq . (printf "source: %s\n"))
+
+
+tellSubject :: MonadWriter (Seq Char) m => String -> m ()
+tellSubject = tellSeq . (printf "subject: %s\n")
+
+
+tellDescription :: MonadWriter (Seq Char) m => Maybe String -> m ()
+tellDescription = maybe (return ()) (tellSeq . (printf "description: %s\n"))
+
+
+tellPublisher :: MonadWriter (Seq Char) m => Maybe String -> m ()
+tellPublisher = maybe (return ()) (tellSeq . (printf "publisher: %s\n"))
+
+
+tellLang :: MonadWriter (Seq Char) m => String -> m ()
+tellLang = tellSeq . (printf "language: %s\n")
+
+
+tellRelation :: MonadWriter (Seq Char) m => Maybe String -> m ()
+tellRelation = maybe (return ()) (tellSeq . (printf "relation: %s\n"))
+
+
+tellCoverage :: MonadWriter (Seq Char) m => Maybe String -> m ()
+tellCoverage = maybe (return ()) (tellSeq . (printf "coverage: %s\n"))
+
+
+tellRights :: MonadWriter (Seq Char) m => Maybe String -> m ()
+tellRights = maybe (return ()) (tellSeq . (printf "rights: %s\n"))
+
+
+tellMetadata :: MonadWriter (Seq Char) m => Metadata -> m ()
+tellMetadata (Metadata titles creators contributors subjects desc 
+      publisher dates mType format ids source langs relation 
+      coverage rights) = do
+   mapM_ tellTitle titles
+   mapM_ tellCreator creators
+   mapM_ tellContributor contributors
+   mapM_ tellDate dates
+   tellType mType
+   tellFormat format
+   mapM_ tellId ids
+   tellSource source
+   mapM_ tellSubject subjects
+   tellDescription desc
+   tellPublisher publisher
+   mapM_ tellLang langs
+   tellRelation relation
+   tellCoverage coverage
+   tellRights rights
diff --git a/src/Codec/Epub/Opf/Format/Package.hs b/src/Codec/Epub/Opf/Format/Package.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Format/Package.hs
@@ -0,0 +1,40 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+-- | Module for pretty-printing OPF package data
+module Codec.Epub.Opf.Format.Package
+   ( formatPackage
+   )
+   where
+
+import Control.Monad.Writer.Lazy
+import Data.Foldable ( toList )
+
+import Codec.Epub.Opf.Format.Guide
+import Codec.Epub.Opf.Format.Manifest
+import Codec.Epub.Opf.Format.Metadata
+import Codec.Epub.Opf.Format.Spine
+import Codec.Epub.Opf.Format.Util
+import Codec.Epub.Opf.Package
+
+
+tellPackage :: MonadWriter (Seq Char) m => (String, String) -> m ()
+tellPackage (version, uniqueId) = do
+   tellSeq "package\n"
+   tellSeq $ formatSubline "version" (Just version)
+   tellSeq $ formatSubline "unique-identifier" (Just uniqueId)
+
+
+formatPackage :: Bool -> Package -> String
+formatPackage showAll (Package v u meta ma sp gu) =
+   toList . execWriter $ do
+      tellPackage (v, u)
+      tellMetadata meta
+
+      when showAll $ do
+         tellManifest ma
+         tellSpine sp
+         tellGuide gu
diff --git a/src/Codec/Epub/Opf/Format/Spine.hs b/src/Codec/Epub/Opf/Format/Spine.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Format/Spine.hs
@@ -0,0 +1,33 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+-- | Module for pretty-printing ePub metadata info
+module Codec.Epub.Opf.Format.Spine
+   where
+
+import Control.Monad.Writer.Lazy
+import Text.Printf
+
+import Codec.Epub.Opf.Format.Util
+import Codec.Epub.Opf.Package.Spine
+
+
+tellSpineItemref :: MonadWriter (Seq Char) m => SpineItemref -> m ()
+tellSpineItemref (SpineItemref idref linear) =
+   tellSeq $ printf "   idref: %s%s\n" idref (linearToString linear)
+
+   where
+      boolToYn True  = "yes"
+      boolToYn False = "no"
+
+      linearToString Nothing = ""
+      linearToString (Just l) = printf ", linear: %s" (boolToYn l)
+
+
+tellSpine :: MonadWriter (Seq Char) m => Spine -> m ()
+tellSpine (Spine toc itemRefs) = do
+   tellSeq $ printf "spine toc: %s, itemrefs:\n" toc
+   mapM_ tellSpineItemref itemRefs
diff --git a/src/Codec/Epub/Opf/Format/Util.hs b/src/Codec/Epub/Opf/Format/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Format/Util.hs
@@ -0,0 +1,26 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+-- | Functions shared by several formatting modules
+module Codec.Epub.Opf.Format.Util
+   ( formatSubline
+   , tellSeq
+   , Seq
+   )
+   where
+
+import Control.Monad.Writer.Lazy
+import Data.Sequence ( Seq, fromList )
+import Text.Printf
+
+
+formatSubline :: String -> Maybe String -> String
+formatSubline _   Nothing = ""
+formatSubline key (Just value) = printf "   %s: %s\n" key value
+
+
+tellSeq :: MonadWriter (Seq a) m => [a] -> m ()
+tellSeq = tell . fromList
diff --git a/src/Codec/Epub/Opf/Metadata.hs b/src/Codec/Epub/Opf/Metadata.hs
deleted file mode 100644
--- a/src/Codec/Epub/Opf/Metadata.hs
+++ /dev/null
@@ -1,92 +0,0 @@
--- Copyright: 2010 Dino Morelli
--- License: BSD3 (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{- | Data types for working with the metadata of ePub documents
-
-   These data types were constructed by studying the IDPF OPF 
-   specification for ePub documents found here:
-
-   <http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html>
--}
-module Codec.Epub.Opf.Metadata
-   ( OPFPackage (..)
-   , EMTitle (..)
-   , EMCreator (..)
-   , EMDate (..)
-   , EMId (..)
-   , EpubMeta (..)
-   , emptyEpubMeta
-   )
-   where
-
-
-{- | opf:package tag
-
-   Note that we are not yet storing the data that comes after
-   \/package\/metadata in an OPF Package Document. But that may
-   be added at a later time.
--}
-data OPFPackage = OPFPackage
-   { opVersion :: String  -- ^ version attr
-   , opUniqueId :: String  -- ^ unique-identifier attr
-   , opMeta :: EpubMeta  -- ^ metadata child element contents
-   }
-   deriving (Eq, Show)
-
-
--- | dc:title tag, xml:lang attr, content
-data EMTitle = EMTitle (Maybe String) String
-   deriving (Eq, Show)
-
--- | dc:creator tag, opf:role attr, opf:file-as attr, content
-data EMCreator = EMCreator (Maybe String) (Maybe String) String
-   deriving (Eq, Show)
-
--- | dc:date tag, opf:event attr, content
-data EMDate = EMDate (Maybe String) String
-   deriving (Eq, Show)
-
--- | dc:identifier tag, id attr, opf:scheme attr, content
-data EMId = EMId String (Maybe String) String
-   deriving (Eq, Show)
-
--- | opf:metadata tag
-data EpubMeta = EpubMeta
-   { emTitles :: [EMTitle]   -- ^ at least one required
-   , emCreators :: [EMCreator]
-   , emContributors :: [EMCreator]
-   , emSubjects :: [String]  -- ^ dc:subject tags
-   , emDescription :: Maybe String  -- ^ dc:description tags
-   , emPublisher :: Maybe String  -- ^ dc:publisher tag
-   , emDates :: [EMDate]
-   , emType :: Maybe String  -- ^ dc:type tag
-   , emFormat :: Maybe String  -- ^ dc:format tag
-   , emIds :: [EMId]          -- ^ at least one required
-   , emSource :: Maybe String  -- ^ dc:source tag
-   , emLangs :: [String]    -- ^ dc:language tags, at least one required
-   , emRelation :: Maybe String  -- ^ dc:relation tag
-   , emCoverage :: Maybe String  -- ^ dc:coverage tag
-   , emRights :: Maybe String  -- ^ dc:rights tag
-   }
-   deriving (Eq, Show)
-
--- | Note: This isn't valid as-is, some required values are empty lists!
-emptyEpubMeta :: EpubMeta
-emptyEpubMeta = EpubMeta
-   { emTitles = []   -- one required
-   , emCreators = []
-   , emContributors = []
-   , emSubjects = []
-   , emDescription = Nothing
-   , emPublisher = Nothing
-   , emDates = []
-   , emType = Nothing
-   , emFormat = Nothing
-   , emIds = []       -- one required
-   , emSource = Nothing
-   , emLangs = []     -- one required
-   , emRelation = Nothing
-   , emCoverage = Nothing
-   , emRights = Nothing
-   }
diff --git a/src/Codec/Epub/Opf/Metadata/Format.hs b/src/Codec/Epub/Opf/Metadata/Format.hs
deleted file mode 100644
--- a/src/Codec/Epub/Opf/Metadata/Format.hs
+++ /dev/null
@@ -1,131 +0,0 @@
--- Copyright: 2010 Dino Morelli
--- License: BSD3 (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
--- | Module for pretty-printing ePub metadata info
-module Codec.Epub.Opf.Metadata.Format
-   ( opfToString
-   )
-   where
-
-import Text.Printf
-
-import Codec.Epub.Opf.Metadata
-
-
-formatSubline :: String -> Maybe String -> String
-formatSubline _   Nothing = ""
-formatSubline key (Just value) = printf "   %s: %s\n" key value
-
-
-packageToString :: (String, String) -> String
-packageToString (version, uniqueId) =
-   "package\n" ++
-   (formatSubline "version" (Just version)) ++
-   (formatSubline "unique-identifier" (Just uniqueId))
-
-
-titleToString :: EMTitle -> String
-titleToString (EMTitle Nothing title) = printf "title: %s\n" title
-titleToString (EMTitle lang title) =
-   "title\n" ++
-   (formatSubline "lang" lang) ++
-   (formatSubline "title" (Just title))
-
-
-creatorToString :: EMCreator -> String
-creatorToString (EMCreator Nothing Nothing creator) =
-   printf "creator: %s\n" creator
-creatorToString (EMCreator role fileAs creator) =
-   "creator\n" ++
-   (formatSubline "role" role) ++
-   (formatSubline "file-as" fileAs) ++
-   (formatSubline "creator" (Just creator))
-
-
-contributorToString :: EMCreator -> String
-contributorToString (EMCreator Nothing Nothing contributor) =
-   printf "contributor: %s\n" contributor
-contributorToString (EMCreator role fileAs contributor) =
-   "contributor\n" ++
-   (formatSubline "role" role) ++
-   (formatSubline "file-as" fileAs) ++
-   (formatSubline "creator" (Just contributor))
-
-
-subjectToString :: String -> String
-subjectToString = printf "subject: %s\n"
-
-
-descriptionToString :: Maybe String -> String
-descriptionToString = maybe "" (printf "description: %s\n")
-
-
-publisherToString :: Maybe String -> String
-publisherToString = maybe "" (printf "publisher: %s\n")
-
-
-dateToString :: EMDate -> String
-dateToString (EMDate Nothing date) =
-   printf "date: %s\n" date
-dateToString (EMDate event date) =
-   "date\n" ++
-   (formatSubline "event" event) ++
-   (formatSubline "date" (Just date))
-
-
-typeToString :: Maybe String -> String
-typeToString = maybe "" (printf "type: %s\n")
-
-
-formatToString :: Maybe String -> String
-formatToString = maybe "" (printf "format: %s\n")
-
-
-idToString :: EMId -> String
-idToString (EMId idVal scheme content) =
-   "identifier\n" ++
-   (formatSubline "id" (Just idVal)) ++
-   (formatSubline "scheme" scheme) ++
-   (formatSubline "identifier" (Just content))
-
-
-sourceToString :: Maybe String -> String
-sourceToString = maybe "" (printf "source: %s\n")
-
-
-langToString :: String -> String
-langToString = printf "language: %s\n"
-
-
-relationToString :: Maybe String -> String
-relationToString = maybe "" (printf "relation: %s\n")
-
-
-coverageToString :: Maybe String -> String
-coverageToString = maybe "" (printf "coverage: %s\n")
-
-
-rightsToString :: Maybe String -> String
-rightsToString = maybe "" (printf "rights: %s\n")
-
-
--- | Format an ePub metadata into a String
-opfToString :: OPFPackage -> String
-opfToString (OPFPackage v u em) = concat $
-   [packageToString (v, u)] ++
-   (map titleToString $ emTitles em) ++
-   (map creatorToString $ emCreators em) ++
-   (map contributorToString $ emContributors em) ++
-   (map dateToString $ emDates em) ++
-   [typeToString . emType $ em] ++
-   [formatToString . emFormat $ em] ++
-   (map idToString $ emIds em) ++
-   [sourceToString . emSource $ em] ++
-   (map subjectToString $ emSubjects em) ++
-   [descriptionToString . emDescription $ em] ++
-   [publisherToString . emPublisher $ em] ++
-   (map langToString $ emLangs em) ++
-   [relationToString . emRelation $ em] ++
-   [coverageToString . emCoverage $ em] ++
-   [rightsToString . emRights $ em]
diff --git a/src/Codec/Epub/Opf/Metadata/Parse.hs b/src/Codec/Epub/Opf/Metadata/Parse.hs
deleted file mode 100644
--- a/src/Codec/Epub/Opf/Metadata/Parse.hs
+++ /dev/null
@@ -1,210 +0,0 @@
--- Copyright: 2010 Dino Morelli
--- License: BSD3 (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{-# LANGUAGE Arrows #-}
-{-# LANGUAGE FlexibleContexts #-}
-
--- | Module for extracting the metadata from an ePub file
-module Codec.Epub.Opf.Metadata.Parse
-   ( parseXmlToOpf
-   , parseEpubOpf
-   )
-   where
-
-import Control.Applicative
-import Control.Monad.Error
-import Data.Tree.NTree.TypeDefs ( NTree )
-import Prelude hiding ( cos )
-import Text.XML.HXT.Arrow
-
-import Codec.Epub.IO
-import Codec.Epub.Opf.Metadata
-
-
--- HXT helpers
-
-atTag :: (ArrowXml a) => String -> a (NTree XNode) XmlTree
-atTag tag = deep (isElem >>> hasName tag)
-
-atQTag :: (ArrowXml a) => QName -> a (NTree XNode) XmlTree
-atQTag tag = deep (isElem >>> hasQName tag)
-
-text :: (ArrowXml a) => a (NTree XNode) String
-text = getChildren >>> getText
-
-notNullA :: (ArrowList a) => a [b] [b]
-notNullA = isA $ not . null
-
-
-mbQTagText :: (ArrowXml a) => QName -> a (NTree XNode) (Maybe String)
-mbQTagText tag =
-   ( atQTag tag >>>
-     text >>> notNullA >>> arr Just )
-   `orElse`
-   (constA Nothing)
-
-
-mbGetAttrValue :: (ArrowXml a) =>
-   String -> a XmlTree (Maybe String)
-mbGetAttrValue n =
-   (getAttrValue n >>> notNullA >>> arr Just)
-   `orElse` (constA Nothing)
-
-mbGetQAttrValue :: (ArrowXml a) =>
-   QName -> a XmlTree (Maybe String)
-mbGetQAttrValue qn =
-   (getQAttrValue qn >>> notNullA >>> arr Just)
-   `orElse` (constA Nothing)
-
-
-{- ePub parsing helpers
-
-   Note that these URIs could conceivably change in the future
-   Is it ok that they're hardcoded like this?
-
-   Well, ok, the xml namespace URI will probably never change.
--}
-
-dcName, opfName, xmlName :: String -> QName
-dcName local = mkQName "dc" local "http://purl.org/dc/elements/1.1/"
-opfName local = mkQName "opf" local "http://www.idpf.org/2007/opf"
-xmlName local = mkQName "xml" local "http://www.w3.org/XML/1998/namespace"
-
-
-getPackage :: (ArrowXml a) => a (NTree XNode) (String, String)
-getPackage = atTag "package" >>>
-   proc x -> do
-      v <- getAttrValue "version" -< x
-      u <- getAttrValue "unique-identifier" -< x
-      returnA -< (v, u)
-
-
-getTitle :: (ArrowXml a) => a (NTree XNode) EMTitle
-getTitle = atQTag (dcName "title") >>>
-   proc x -> do
-      l <- mbGetQAttrValue (xmlName "lang") -< x
-      c <- text -< x
-      returnA -< EMTitle l c
-
-
-{- Since creators and contributors have the same exact XML structure,
-   this arrow is used to get either of them
--}
-getCreator :: (ArrowXml a) => String -> a (NTree XNode) EMCreator
-getCreator tag = atQTag (dcName tag) >>> ( unwrapArrow $ EMCreator
-   <$> (WrapArrow $ mbGetQAttrValue (opfName "role"))
-   <*> (WrapArrow $ mbGetQAttrValue (opfName "file-as"))
-   <*> (WrapArrow $ text)
-   )
-
-
-getSubject :: (ArrowXml a) => a (NTree XNode) String
-getSubject = atQTag (dcName "subject") >>> text
-
-
-getDescription :: (ArrowXml a) => a (NTree XNode) (Maybe String)
-getDescription = mbQTagText $ dcName "description"
-
-
-getPublisher :: (ArrowXml a) => a (NTree XNode) (Maybe String)
-getPublisher = mbQTagText $ dcName "publisher"
-
-
-getDate :: (ArrowXml a) => a (NTree XNode) EMDate
-getDate = atQTag (dcName "date") >>>
-   proc x -> do
-      e <- mbGetQAttrValue (opfName "event") -< x
-      c <- text -< x
-      returnA -< EMDate e c
-
-
-getType :: (ArrowXml a) => a (NTree XNode) (Maybe String)
-getType = mbQTagText $ dcName "type"
-
-
-getFormat :: (ArrowXml a) => a (NTree XNode) (Maybe String)
-getFormat = mbQTagText $ dcName "format"
-
-
-getId :: (ArrowXml a) => a (NTree XNode) EMId
-getId = atQTag (dcName "identifier") >>>
-   proc x -> do
-      mbi <- mbGetAttrValue "id" -< x
-      s <- mbGetQAttrValue (opfName "scheme") -< x
-      c <- text -< x
-      let i = maybe "[WARNING: missing required id attribute]" id mbi
-      returnA -< EMId i s c
-
-
-getSource :: (ArrowXml a) => a (NTree XNode) (Maybe String)
-getSource = mbQTagText $ dcName "source"
-
-
-getLang :: (ArrowXml a) => a (NTree XNode) String
-getLang = atQTag (dcName "language") >>> text
-
-
-getRelation :: (ArrowXml a) => a (NTree XNode) (Maybe String)
-getRelation = mbQTagText $ dcName "relation"
-
-
-getCoverage :: (ArrowXml a) => a (NTree XNode) (Maybe String)
-getCoverage = mbQTagText $ dcName "coverage"
-
-
-getRights :: (ArrowXml a) => a (NTree XNode) (Maybe String)
-getRights = mbQTagText $ dcName "rights"
-
-
-getMeta :: (ArrowXml a) => a (NTree XNode) EpubMeta
-getMeta = atTag "metadata" >>> ( unwrapArrow $ EpubMeta
-   <$> (WrapArrow $ listA getTitle)
-   <*> (WrapArrow $ listA $ getCreator "creator")
-   <*> (WrapArrow $ listA $ getCreator "contributor")
-   <*> (WrapArrow $ listA getSubject)
-   <*> (WrapArrow $ getDescription)
-   <*> (WrapArrow $ getPublisher)
-   <*> (WrapArrow $ listA getDate)
-   <*> (WrapArrow $ getType)
-   <*> (WrapArrow $ getFormat)
-   <*> (WrapArrow $ listA getId)
-   <*> (WrapArrow $ getSource)
-   <*> (WrapArrow $ listA getLang)
-   <*> (WrapArrow $ getRelation)
-   <*> (WrapArrow $ getCoverage)
-   <*> (WrapArrow $ getRights)
-   )
-
-
-getBookData :: (ArrowXml a) => a (NTree XNode) OPFPackage
-getBookData = 
-   proc x -> do
-      (v, u) <- getPackage -< x
-      m <- getMeta -< x
-      returnA -< OPFPackage v u m
-
-
-{- | Extract the ePub metadata contained in the OPF Package Document 
-   contained in the supplied string
--}
-parseXmlToOpf :: (MonadIO m) => String -> m [OPFPackage]
-parseXmlToOpf opfContents =
-   liftIO $ runX (
-      readString [(a_validate, v_0)] opfContents
-      >>> propagateNamespaces
-      >>> getBookData
-      )
-
-
--- | Given the path to an ePub file, extract the metadata
-parseEpubOpf :: (MonadIO m, MonadError String m) =>
-   FilePath -> m OPFPackage
-parseEpubOpf zipPath = do
-   opfContents <- extractFileFromZip zipPath =<< opfPath zipPath
-   result <- parseXmlToOpf opfContents
-
-   case result of
-      (em : []) -> return em
-      _         -> throwError
-         "ERROR: we didn't come up with a single EpubMeta"
diff --git a/src/Codec/Epub/Opf/Package.hs b/src/Codec/Epub/Opf/Package.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Package.hs
@@ -0,0 +1,43 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{- | Data types for working with the metadata of ePub documents
+
+   These data types were constructed by studying the IDPF OPF 
+   specification for ePub documents found here:
+
+   <http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html>
+-}
+module Codec.Epub.Opf.Package
+   ( Package (..)
+   , MetaTitle (..)
+   , MetaCreator (..)
+   , MetaDate (..)
+   , MetaId (..)
+   , Metadata (..)
+   , ManifestItem (..)
+   , SpineItemref (..)
+   , Spine (..)
+   , GuideRef (..)
+   , emptyMetadata
+   )
+   where
+
+import Codec.Epub.Opf.Package.Guide
+import Codec.Epub.Opf.Package.Manifest
+import Codec.Epub.Opf.Package.Metadata
+import Codec.Epub.Opf.Package.Spine
+
+
+{- | package tag
+-}
+data Package = Package
+   { opVersion :: String  -- ^ version attr
+   , opUniqueId :: String  -- ^ unique-identifier attr
+   , opMeta :: Metadata  -- ^ metadata child element contents
+   , opManifest :: [ManifestItem] -- ^ manifest child element contents. One required
+   , opSpine :: Spine  -- ^ spine child element contents
+   , opGuide :: [GuideRef] -- ^ guide child element contents
+   }
+   deriving (Eq, Show)
diff --git a/src/Codec/Epub/Opf/Package/Guide.hs b/src/Codec/Epub/Opf/Package/Guide.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Package/Guide.hs
@@ -0,0 +1,24 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{- | Data types for working with the metadata of ePub documents
+
+   These data types were constructed by studying the IDPF OPF 
+   specification for ePub documents found here:
+
+   <http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html>
+-}
+module Codec.Epub.Opf.Package.Guide
+   ( GuideRef (..)
+   )
+   where
+
+
+-- | package\/guide\/reference tag
+data GuideRef = GuideRef
+   { grType :: String  -- ^ type attr. Must follow 13th edition of the Chicago Manual of Style
+   , grTitle :: Maybe String  -- ^ title attr
+   , grHref :: String  -- ^ href attr. Must reference item in manifest
+   }
+   deriving (Eq, Show)
diff --git a/src/Codec/Epub/Opf/Package/Manifest.hs b/src/Codec/Epub/Opf/Package/Manifest.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Package/Manifest.hs
@@ -0,0 +1,31 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{- | Data types for working with the metadata of ePub documents
+
+   These data types were constructed by studying the IDPF OPF 
+   specification for ePub documents found here:
+
+   <http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html>
+-}
+module Codec.Epub.Opf.Package.Manifest
+   ( ManifestItem (..)
+   )
+   where
+
+import Codec.Epub.Opf.Common
+
+
+-- | manifest attribute values
+type MFItemHref = String
+type MFItemMediaType = String
+
+
+-- | package\/manifest\/item tag
+data ManifestItem = ManifestItem
+   { mfiId :: MFItemId  -- ^ id attr
+   , mfiHref :: MFItemHref  -- ^ href attr
+   , mfiMediaType :: MFItemMediaType  -- ^ media-type attr
+   }
+   deriving (Eq, Show)
diff --git a/src/Codec/Epub/Opf/Package/Metadata.hs b/src/Codec/Epub/Opf/Package/Metadata.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Package/Metadata.hs
@@ -0,0 +1,81 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{- | Data types for working with the metadata of ePub documents
+
+   These data types were constructed by studying the IDPF OPF 
+   specification for ePub documents found here:
+
+   <http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html>
+-}
+module Codec.Epub.Opf.Package.Metadata
+   ( MetaTitle (..)
+   , MetaCreator (..)
+   , MetaDate (..)
+   , MetaId (..)
+   , Metadata (..)
+   , emptyMetadata
+   )
+   where
+
+
+-- | package\/metadata\/dc:title tag, xml:lang attr, content
+data MetaTitle = MetaTitle (Maybe String) String
+   deriving (Eq, Show)
+
+{- | package\/metadata\/dc:creator tag, opf:role attr, opf:file-as attr,
+   content
+-}
+data MetaCreator = MetaCreator (Maybe String) (Maybe String) String
+   deriving (Eq, Show)
+
+-- | package\/metadata\/dc:date tag, opf:event attr, content
+data MetaDate = MetaDate (Maybe String) String
+   deriving (Eq, Show)
+
+{- | package\/metadata\/dc:identifier tag, id attr, opf:scheme attr,
+   content
+-}
+data MetaId = MetaId String (Maybe String) String
+   deriving (Eq, Show)
+
+-- | package\/metadata tag
+data Metadata = Metadata
+   { metaTitles :: [MetaTitle]   -- ^ at least one required
+   , metaCreators :: [MetaCreator]
+   , metaContributors :: [MetaCreator]
+   , metaSubjects :: [String]  -- ^ dc:subject tags
+   , metaDescription :: Maybe String  -- ^ dc:description tags
+   , metaPublisher :: Maybe String  -- ^ dc:publisher tag
+   , metaDates :: [MetaDate]
+   , metaType :: Maybe String  -- ^ dc:type tag
+   , metaFormat :: Maybe String  -- ^ dc:format tag
+   , metaIds :: [MetaId]          -- ^ at least one required
+   , metaSource :: Maybe String  -- ^ dc:source tag
+   , metaLangs :: [String]    -- ^ dc:language tags, at least one required
+   , metaRelation :: Maybe String  -- ^ dc:relation tag
+   , metaCoverage :: Maybe String  -- ^ dc:coverage tag
+   , metaRights :: Maybe String  -- ^ dc:rights tag
+   }
+   deriving (Eq, Show)
+
+-- | Note: This isn't valid as-is, some required values are empty lists!
+emptyMetadata :: Metadata
+emptyMetadata = Metadata
+   { metaTitles = []   -- one required
+   , metaCreators = []
+   , metaContributors = []
+   , metaSubjects = []
+   , metaDescription = Nothing
+   , metaPublisher = Nothing
+   , metaDates = []
+   , metaType = Nothing
+   , metaFormat = Nothing
+   , metaIds = []       -- one required
+   , metaSource = Nothing
+   , metaLangs = []     -- one required
+   , metaRelation = Nothing
+   , metaCoverage = Nothing
+   , metaRights = Nothing
+   }
diff --git a/src/Codec/Epub/Opf/Package/Spine.hs b/src/Codec/Epub/Opf/Package/Spine.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Package/Spine.hs
@@ -0,0 +1,34 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{- | Data types for working with the metadata of ePub documents
+
+   These data types were constructed by studying the IDPF OPF 
+   specification for ePub documents found here:
+
+   <http://www.idpf.org/2007/opf/OPF_2.0_final_spec.html>
+-}
+module Codec.Epub.Opf.Package.Spine
+   ( Spine (..)
+   , SpineItemref (..)
+   )
+   where
+
+import Codec.Epub.Opf.Common
+
+
+-- | package\/spine\/itemref tag
+data SpineItemref = SpineItemref
+   { siIdRef  :: MFItemId  -- ^ idref attr. Must reference item in manifest
+   , siLinear :: Maybe Bool  -- ^ linear attr
+   }
+   deriving (Eq, Show)
+
+
+-- | package\/spine tag
+data Spine = Spine
+   { spineToc    :: MFItemId  -- ^ toc attr. Must reference the NCX in the manifest
+   , spineItemrefs :: [ SpineItemref ] -- one required
+   }
+   deriving (Eq, Show)
diff --git a/src/Codec/Epub/Opf/Parse.hs b/src/Codec/Epub/Opf/Parse.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Epub/Opf/Parse.hs
@@ -0,0 +1,270 @@
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE Arrows #-}
+{-# LANGUAGE FlexibleContexts #-}
+
+-- | Module for extracting the metadata from an ePub file
+module Codec.Epub.Opf.Parse
+   ( parseXmlToOpf
+   , parseEpubOpf
+   )
+   where
+
+import Control.Applicative
+import Control.Arrow.ListArrows
+import Control.Monad.Error
+import Data.Tree.NTree.TypeDefs ( NTree )
+import Text.XML.HXT.Arrow.Namespace ( propagateNamespaces )
+import Text.XML.HXT.Arrow.XmlArrow
+import Text.XML.HXT.Arrow.XmlState ( no, runX, withValidate )
+import Text.XML.HXT.Arrow.ReadDocument ( readString )
+import Text.XML.HXT.DOM.TypeDefs
+
+import Codec.Epub.IO
+import Codec.Epub.Opf.Package
+
+
+-- HXT helpers
+
+atTag :: (ArrowXml a) => String -> a (NTree XNode) XmlTree
+atTag tag = deep (isElem >>> hasName tag)
+
+atQTag :: (ArrowXml a) => QName -> a (NTree XNode) XmlTree
+atQTag tag = deep (isElem >>> hasQName tag)
+
+text :: (ArrowXml a) => a (NTree XNode) String
+text = getChildren >>> getText
+
+notNullA :: (ArrowList a) => a [b] [b]
+notNullA = isA $ not . null
+
+
+mbQTagText :: (ArrowXml a) => QName -> a (NTree XNode) (Maybe String)
+mbQTagText tag =
+   ( atQTag tag >>>
+     text >>> notNullA >>> arr Just )
+   `orElse`
+   (constA Nothing)
+
+
+mbGetAttrValue :: (ArrowXml a) =>
+   String -> a XmlTree (Maybe String)
+mbGetAttrValue n =
+   (getAttrValue n >>> notNullA >>> arr Just)
+   `orElse` (constA Nothing)
+
+mbGetQAttrValue :: (ArrowXml a) =>
+   QName -> a XmlTree (Maybe String)
+mbGetQAttrValue qn =
+   (getQAttrValue qn >>> notNullA >>> arr Just)
+   `orElse` (constA Nothing)
+
+
+{- ePub parsing helpers
+
+   Note that these URIs could conceivably change in the future
+   Is it ok that they're hardcoded like this?
+
+   Well, ok, the xml namespace URI will probably never change.
+-}
+
+dcName, opfName, xmlName :: String -> QName
+dcName local = mkQName "dc" local "http://purl.org/dc/elements/1.1/"
+opfName local = mkQName "opf" local "http://www.idpf.org/2007/opf"
+xmlName local = mkQName "xml" local "http://www.w3.org/XML/1998/namespace"
+
+
+getPackage :: (ArrowXml a) => a (NTree XNode) (String, String)
+getPackage = atTag "package" >>>
+   proc x -> do
+      v <- getAttrValue "version" -< x
+      u <- getAttrValue "unique-identifier" -< x
+      returnA -< (v, u)
+
+
+getTitle :: (ArrowXml a) => a (NTree XNode) MetaTitle
+getTitle = atQTag (dcName "title") >>>
+   proc x -> do
+      l <- mbGetQAttrValue (xmlName "lang") -< x
+      c <- text -< x
+      returnA -< MetaTitle l c
+
+
+{- Since creators and contributors have the same exact XML structure,
+   this arrow is used to get either of them
+-}
+getCreator :: (ArrowXml a) => String -> a (NTree XNode) MetaCreator
+getCreator tag = atQTag (dcName tag) >>> ( unwrapArrow $ MetaCreator
+   <$> (WrapArrow $ mbGetQAttrValue (opfName "role"))
+   <*> (WrapArrow $ mbGetQAttrValue (opfName "file-as"))
+   <*> (WrapArrow $ text)
+   )
+
+
+getSubject :: (ArrowXml a) => a (NTree XNode) String
+getSubject = atQTag (dcName "subject") >>> text
+
+
+getDescription :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getDescription = mbQTagText $ dcName "description"
+
+
+getPublisher :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getPublisher = mbQTagText $ dcName "publisher"
+
+
+getDate :: (ArrowXml a) => a (NTree XNode) MetaDate
+getDate = atQTag (dcName "date") >>>
+   proc x -> do
+      e <- mbGetQAttrValue (opfName "event") -< x
+      c <- text -< x
+      returnA -< MetaDate e c
+
+
+getType :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getType = mbQTagText $ dcName "type"
+
+
+getFormat :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getFormat = mbQTagText $ dcName "format"
+
+
+getId :: (ArrowXml a) => a (NTree XNode) MetaId
+getId = atQTag (dcName "identifier") >>>
+   proc x -> do
+      mbi <- mbGetAttrValue "id" -< x
+      s <- mbGetQAttrValue (opfName "scheme") -< x
+      c <- text -< x
+      let i = maybe "[WARNING: missing required id attribute]" id mbi
+      returnA -< MetaId i s c
+
+
+getSource :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getSource = mbQTagText $ dcName "source"
+
+
+getLang :: (ArrowXml a) => a (NTree XNode) String
+getLang = atQTag (dcName "language") >>> text
+
+
+getRelation :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getRelation = mbQTagText $ dcName "relation"
+
+
+getCoverage :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getCoverage = mbQTagText $ dcName "coverage"
+
+
+getRights :: (ArrowXml a) => a (NTree XNode) (Maybe String)
+getRights = mbQTagText $ dcName "rights"
+
+
+getMeta :: (ArrowXml a) => a (NTree XNode) Metadata
+getMeta = atTag "metadata" >>> ( unwrapArrow $ Metadata
+   <$> (WrapArrow $ listA getTitle)
+   <*> (WrapArrow $ listA $ getCreator "creator")
+   <*> (WrapArrow $ listA $ getCreator "contributor")
+   <*> (WrapArrow $ listA getSubject)
+   <*> (WrapArrow $ getDescription)
+   <*> (WrapArrow $ getPublisher)
+   <*> (WrapArrow $ listA getDate)
+   <*> (WrapArrow $ getType)
+   <*> (WrapArrow $ getFormat)
+   <*> (WrapArrow $ listA getId)
+   <*> (WrapArrow $ getSource)
+   <*> (WrapArrow $ listA getLang)
+   <*> (WrapArrow $ getRelation)
+   <*> (WrapArrow $ getCoverage)
+   <*> (WrapArrow $ getRights)
+   )
+
+
+getManifestItem :: (ArrowXml a) => a (NTree XNode) ManifestItem
+getManifestItem = atTag "item" >>>
+   proc x -> do
+      i <- getAttrValue "id" -< x
+      h <- getAttrValue "href" -< x
+      m <- getAttrValue "media-type" -< x
+      returnA -< ManifestItem i h m
+
+
+getManifest :: (ArrowXml a) => a (NTree XNode) [ManifestItem]
+getManifest = atTag "manifest" >>>
+   proc x -> do
+      l <- listA getManifestItem -< x
+      returnA -< l
+
+
+getSpineItemref :: (ArrowXml a) => a (NTree XNode) SpineItemref
+getSpineItemref = atTag "itemref" >>>
+   proc x -> do
+      i <- getAttrValue "idref" -< x
+      ml <- mbGetAttrValue "linear" -< x
+      let l = maybe Nothing (\v -> if v == "no" then Just False else Just True) ml
+      returnA -< SpineItemref i l
+
+
+getSpine :: (ArrowXml a) => a (NTree XNode) Spine
+getSpine = atTag "spine" >>>
+   proc x -> do
+      i <- getAttrValue "toc" -< x
+      l <- listA getSpineItemref -< x
+      returnA -< (Spine i l)
+
+
+getGuideRef :: (ArrowXml a) => a (NTree XNode) GuideRef
+getGuideRef = atTag "reference" >>>
+   proc x -> do
+      t <- getAttrValue "type" -< x
+      mt <- mbGetAttrValue "title" -< x
+      h <- getAttrValue "href" -< x
+      returnA -< GuideRef t mt h
+
+
+getGuide :: (ArrowXml a) => a (NTree XNode) [GuideRef]
+getGuide = atTag "guide" >>>
+   proc x -> do
+      l <- listA getGuideRef -< x
+      returnA -< l
+
+
+getBookData :: (ArrowXml a) => a (NTree XNode) Package
+getBookData = 
+   proc x -> do
+      (v, u) <- getPackage -< x
+      m <- getMeta -< x
+      mf <- getManifest -< x
+      sp <- getSpine -< x
+      gl <- listA getGuide -< x
+      let g = case gl of
+                []  -> []
+                [e] -> e
+                _   -> error "ERROR: more than one guide entries"        
+      returnA -< (Package v u m mf sp g)
+
+
+{- | Extract the ePub OPF Package data contained in the supplied 
+   XML string
+-}
+parseXmlToOpf :: (MonadIO m) => String -> m [Package]
+parseXmlToOpf opfContents =
+   liftIO $ runX (
+      readString [withValidate no] opfContents
+      >>> propagateNamespaces
+      >>> getBookData
+      )
+
+
+-- | Given the path to an ePub file, extract the OPF Package data
+parseEpubOpf :: (MonadIO m, MonadError String m) =>
+   FilePath -> m Package
+parseEpubOpf zipPath = do
+   opfContents <- extractFileFromZip zipPath =<< opfPath zipPath
+   result <- parseXmlToOpf opfContents
+
+   case result of
+      (em : []) -> return em
+      _         -> throwError
+         "ERROR: Parse didn't result in a single document metadata"
diff --git a/src/Codec/Epub/Opf/epub-meta.hs b/src/Codec/Epub/Opf/epub-meta.hs
deleted file mode 100644
--- a/src/Codec/Epub/Opf/epub-meta.hs
+++ /dev/null
@@ -1,24 +0,0 @@
--- Copyright: 2010 Dino Morelli
--- License: BSD3 (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-import Control.Monad.Error
-import System.Environment ( getArgs )
-import System.Exit
-
-import Codec.Epub.Opf.Metadata.Format
-import Codec.Epub.Opf.Metadata.Parse
-
-
-main :: IO ()
-main = do
-   as <- getArgs
-
-   when (length as /= 1) $ do
-      putStrLn "usage: epub-meta EPUBFILE"
-      exitWith $ ExitFailure 1
-
-   let zipPath = head as
-   result <- runErrorT $ parseEpubOpf zipPath
-
-   putStr $ either (++ "\n") opfToString result
diff --git a/testsuite/runtests.hs b/testsuite/runtests.hs
--- a/testsuite/runtests.hs
+++ b/testsuite/runtests.hs
@@ -1,4 +1,4 @@
--- Copyright: 2010 Dino Morelli
+-- Copyright: 2010, 2011 Dino Morelli
 -- License: BSD3 (see LICENSE)
 -- Author: Dino Morelli <dino@ui3.info>
 
@@ -6,8 +6,8 @@
 import Test.HUnit ( Counts, Test (..), assertEqual, runTestTT )
 import Test.HUnit.Base ( Assertion )
 
-import Codec.Epub.Opf.Metadata
-import Codec.Epub.Opf.Metadata.Parse
+import Codec.Epub.Opf.Package
+import Codec.Epub.Opf.Parse
 
 
 main = runTestTT tests >> return ()
@@ -28,55 +28,97 @@
 testFull = TestCase $ do
    xmlString <- readFile $ "testsuite" </> "testFull.opf"
    actual <- parseXmlToOpf xmlString
-   let expected = [ OPFPackage "2.0" "isbn" ( EpubMeta
-         { emTitles =
-            [ EMTitle Nothing "Title Of This Book"
-            , EMTitle (Just "fr") "Titre De Ce Livre"
-            ]
-         , emCreators =
-            [ EMCreator
-               (Just "aut")
-               (Just "Wiggins, Josephine B.")
-               "Josephine B. Wiggins"
-            , EMCreator
-               (Just "aut")
-               Nothing
-               "Horatio Cromwell"
-            , EMCreator
-               Nothing
-               Nothing
-               "Natalia Jenkins"
-            ]
-         , emContributors =
-            [ EMCreator
-               (Just "ill")
-               (Just "Knickerbocker, Reginald Q.")
-               "Reginald Q. Knickerbocker"
-            , EMCreator
-               (Just "edt")
-               Nothing
-               "Beverly Abercrombie"
-            ]
-         , emSubjects = ["Fiction", "Science Fiction"]
-         , emDescription = Just "This document is a stub used for unit testing. It is missing the rest of the tags that normally occur after metadata."
-         , emPublisher = Just "Fictional Books Ltd."
-         , emDates =
-            [ EMDate (Just "published") "2010"
-            , EMDate (Just "created") "2010-05-07"
-            , EMDate (Just "modified") "2010-05-08T10:20:57"
-            , EMDate Nothing "2009-08-03T16:22:20"
-            ]
-         , emType = Just "test OPF Package Document"
-         , emFormat = Just "ePub publication"
-         , emIds =
-            [ EMId "isbn" (Just "ISBN") "1-82057-821-9"
-            , EMId "other" Nothing "1386506873266"]
-         , emSource = Just "document source"
-         , emLangs = ["en-us"]
-         , emRelation = Just "document relation"
-         , emCoverage = Just "coverage information"
-         , emRights = Just "Copyright: 2010 Dino Morelli, License: BSD3"
-         } ) ]
+   let expected =
+         [ Package
+            { opVersion = "2.0"
+            , opUniqueId = "isbn"
+            , opMeta = Metadata
+               { metaTitles =
+                  [ MetaTitle Nothing "Title Of This Book"
+                  , MetaTitle (Just "fr") "Titre De Ce Livre"
+                  ]
+               , metaCreators = 
+                  [ MetaCreator
+                     (Just "aut")
+                     (Just "Wiggins, Josephine B.")
+                     "Josephine B. Wiggins"
+                  , MetaCreator
+                     (Just "aut")
+                     Nothing
+                     "Horatio Cromwell"
+                  , MetaCreator
+                     Nothing
+                     Nothing
+                     "Natalia Jenkins"
+                  ]
+               , metaContributors = 
+                  [ MetaCreator 
+                     (Just "ill") 
+                     (Just "Knickerbocker, Reginald Q.") 
+                     "Reginald Q. Knickerbocker"
+                  , MetaCreator 
+                     (Just "edt") 
+                     Nothing "Beverly Abercrombie"
+                  ]
+               , metaSubjects = ["Fiction","Science Fiction"]
+               , metaDescription = Just "This document is a stub used for unit testing. It is missing the rest of the tags that normally occur after metadata."
+               , metaPublisher = Just "Fictional Books Ltd."
+               , metaDates = 
+                  [ MetaDate (Just "published") "2010"
+                  , MetaDate (Just "created") "2010-05-07"
+                  , MetaDate (Just "modified") "2010-05-08T10:20:57"
+                  , MetaDate Nothing "2009"
+                  ]
+               , metaType = Just "test OPF Package Document"
+               , metaFormat = Just "ePub publication"
+               , metaIds = 
+                  [ MetaId "isbn" (Just "ISBN") "1-82057-821-9"
+                  , MetaId "other" Nothing "1386506873266"
+                  ]
+               , metaSource = Just "document source"
+               , metaLangs = ["en-us"]
+               , metaRelation = Just "document relation"
+               , metaCoverage = Just "coverage information"
+               , metaRights = Just "Copyright: 2010 Dino Morelli, License: BSD3"
+               }
+            , opManifest = 
+               [ ManifestItem 
+                  { mfiId = "ncx"
+                  , mfiHref = "toc.ncx"
+                  , mfiMediaType = "application/x-dtbncx+xml"
+                  }
+               , ManifestItem 
+                  { mfiId = "titlePage"
+                  , mfiHref = "content/titlePage.html"
+                  , mfiMediaType = "application/xhtml+xml"
+                  }
+               , ManifestItem 
+                  { mfiId = "someContent"
+                  , mfiHref = "content/someContent.html"
+                  , mfiMediaType = "application/xhtml+xml"
+                  }
+               ]
+            , opSpine = Spine
+               { spineToc = "ncx"
+               , spineItemrefs = 
+                  [ SpineItemref {siIdRef = "titlePage", siLinear = Nothing}
+                  , SpineItemref {siIdRef = "someContent", siLinear = Nothing}
+                  ]
+               }
+            , opGuide = 
+               [ GuideRef 
+                  { grType = "title-page"
+                  , grTitle = Just "Title page"
+                  , grHref = "content/titlePage.html"
+                  }
+               , GuideRef 
+                  { grType = "text"
+                  , grTitle = Just "Title Of This Book"
+                  , grHref = "content/someContent.html"
+                  }
+               ]
+            }
+         ]
    assertEqual "very full" expected actual
 
 
@@ -87,23 +129,38 @@
 testMinimal = TestCase $ do
    xmlString <- readFile $ "testsuite" </> "testMinimal.opf"
    actual <- parseXmlToOpf xmlString
-   let expected = [ OPFPackage "2.0" "isbn" ( EpubMeta
-         { emTitles = [EMTitle Nothing "Title Of This Book"]
-         , emCreators = []
-         , emContributors = []
-         , emSubjects = []
-         , emDescription = Nothing
-         , emPublisher = Nothing
-         , emDates = []
-         , emType = Nothing
-         , emFormat = Nothing
-         , emIds = [EMId "isbn" (Just "ISBN") "1-82057-821-9"]
-         , emSource = Nothing
-         , emLangs = ["en-us"]
-         , emRelation = Nothing
-         , emCoverage = Nothing
-         , emRights = Nothing
-         } ) ]
+   let expected = 
+         [ Package 
+            { opVersion = "2.0"
+            , opUniqueId = "isbn"
+            , opMeta = Metadata 
+               { metaTitles = [MetaTitle Nothing "Title Of This Book"]
+               , metaCreators = []
+               , metaContributors = []
+               , metaSubjects = []
+               , metaDescription = Nothing
+               , metaPublisher = Nothing
+               , metaDates = []
+               , metaType = Nothing
+               , metaFormat = Nothing
+               , metaIds = [MetaId "isbn" (Just "ISBN") "1-82057-821-9"]
+               , metaSource = Nothing
+               , metaLangs = ["en-us"]
+               , metaRelation = Nothing
+               , metaCoverage = Nothing
+               , metaRights = Nothing
+               }
+            , opManifest = 
+               [ ManifestItem 
+                  { mfiId = "ncx"
+                  , mfiHref = "toc.ncx"
+                  , mfiMediaType = "application/x-dtbncx+xml"
+                  }
+               ]
+            , opSpine = Spine {spineToc = "ncx", spineItemrefs = []}
+            , opGuide = []
+            }
+         ]
    assertEqual "minimal" expected actual
 
 
@@ -114,5 +171,14 @@
 testMissingAll = TestCase $ do
    xmlString <- readFile $ "testsuite" </> "testMissingAll.opf"
    actual <- parseXmlToOpf xmlString
-   let expected = [ OPFPackage "" "" emptyEpubMeta ]
+   let expected =
+         [ Package 
+            { opVersion = ""
+            , opUniqueId = ""
+            , opMeta = emptyMetadata
+            , opManifest = []
+            , opSpine = Spine {spineToc = "", spineItemrefs = []}
+            , opGuide = []
+            }
+         ]
    assertEqual "missing all" expected actual
diff --git a/testsuite/testFull.opf b/testsuite/testFull.opf
--- a/testsuite/testFull.opf
+++ b/testsuite/testFull.opf
@@ -19,7 +19,7 @@
       <dc:date opf:event="published">2010</dc:date>
       <dc:date opf:event="created">2010-05-07</dc:date>
       <dc:date opf:event="modified">2010-05-08T10:20:57</dc:date>
-      <dc:date>2009-08-03T16:22:20</dc:date>
+      <dc:date>2009</dc:date>
       <dc:language>en-us</dc:language>
       <dc:publisher>Fictional Books Ltd.</dc:publisher>
       <dc:identifier id="other">1386506873266</dc:identifier>
@@ -31,9 +31,30 @@
       <dc:relation>document relation</dc:relation>
       <dc:coverage>coverage information</dc:coverage>
       <dc:rights>Copyright: 2010 Dino Morelli, License: BSD3</dc:rights>
-      <meta name="series_index" content="1"/>
    </metadata>
 
-   <!-- This document is a stub used for unit testing. It is missing 
-        the rest of the tags that normally occur after metadata -->
+   <manifest>
+      <item id="ncx"
+         href="toc.ncx"
+         media-type="application/x-dtbncx+xml" />
+      <item id="titlePage"
+         href="content/titlePage.html"
+         media-type="application/xhtml+xml" />
+      <item id="someContent"
+         href="content/someContent.html"
+         media-type="application/xhtml+xml" />
+   </manifest>
+
+   <spine toc="ncx">
+      <itemref idref="titlePage" />
+      <itemref idref="someContent" />
+   </spine>
+
+   <guide>
+      <reference type="title-page" title="Title page"
+         href="content/titlePage.html" />
+      <reference type="text" title="Title Of This Book"
+         href="content/someContent.html" />
+   </guide>
+
 </package>
diff --git a/testsuite/testMinimal.opf b/testsuite/testMinimal.opf
--- a/testsuite/testMinimal.opf
+++ b/testsuite/testMinimal.opf
@@ -12,6 +12,13 @@
       <dc:language>en-us</dc:language>
    </metadata>
 
-   <!-- This document is a stub used for unit testing. It is missing 
-        the rest of the tags that normally occur after metadata -->
+   <manifest>
+      <item id="ncx"
+         href="toc.ncx"
+         media-type="application/x-dtbncx+xml" />
+   </manifest>
+
+   <spine toc="ncx">
+   </spine>
+
 </package>
diff --git a/testsuite/testMissingAll.opf b/testsuite/testMissingAll.opf
--- a/testsuite/testMissingAll.opf
+++ b/testsuite/testMissingAll.opf
@@ -6,6 +6,10 @@
       xmlns:xml="http://www.w3.org/XML/1998/namespace">
    </metadata>
 
-   <!-- This document is a stub used for unit testing. It is missing 
-        the rest of the tags that normally occur after metadata -->
+   <manifest>
+   </manifest>
+
+   <spine>
+   </spine>
+
 </package>
diff --git a/util/show-opf.hs b/util/show-opf.hs
new file mode 100644
--- /dev/null
+++ b/util/show-opf.hs
@@ -0,0 +1,13 @@
+#! /usr/bin/runhaskell -isrc
+
+-- Copyright: 2010, 2011 Dino Morelli
+-- License: BSD3 (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+import System.Environment ( getArgs )
+
+import Codec.Epub.Opf.Parse
+
+
+main :: IO ()
+main = (fmap head) getArgs >>= readFile >>= parseXmlToOpf >>= print
