diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
-# Vim swap files
-*.sw?
-
 # stack build artifacts
 .stack-work/
+stack.yaml.lock
+
+# Generated by hasktags
+tags
diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2008-2018 Dino Morelli
+Copyright (c) 2008 Dino Morelli
 
 Permission to use, copy, modify, and/or distribute this software for any purpose
 with or without fee is hereby granted, provided that the above copyright notice
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -72,7 +72,7 @@
 Binaries
 
 - epub-tools is available for Arch Linux [from the AUR](http://aur.archlinux.org/packages/epub-tools/)
-- Download [binaries for Windows](http://ui3.info/d/proj/epub-tools/epub-tools-2.10-win.zip)
+- Download [binaries for Windows](https://github.com/dino-/epub-tools/releases/download/2.11/epub-tools-2.11-win.zip)
 
 
 Getting source for development
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,174 +1,188 @@
+3.0 (2023-07-09)
+
+  * Fixed link to Windows binary dist
+  * Moved stackage resolver up to lts-20.12
+  * Upgraded cabal file to v2.2
+  * Updated .gitignore
+  * Extensive updates to publication date handling
+  * Fixed copyright date issues
+  * Reorganized source code
+  * Fixed a bug in epub opf editing
+  * Supporting another EPUB3 date event type
+  * Fixed an sdist problem with missing autogen-modules
+
+
 2.11 (2018-01-16)
 
-   * Switched license from BSD3 to ISC
-   * Fixes and additions to some epubname rules
-   * Fixed a couple of cabal file issues
+  * Switched license from BSD3 to ISC
+  * Fixes and additions to some epubname rules
+  * Fixed a couple of cabal file issues
 
 
 2.10 (2017-12-24)
 
-   * Changed the Stackage resolver from a nightly to an lts
-   * Now using a less-fragile `Printf*` type signature
-   * Removed an unused GHC options compile directive
-   * Added a missing module to all other-modules stanzas
-   * Added a unit test for authors with a middle-name
+  * Changed the Stackage resolver from a nightly to an lts
+  * Now using a less-fragile `Printf*` type signature
+  * Removed an unused GHC options compile directive
+  * Added a missing module to all other-modules stanzas
+  * Added a unit test for authors with a middle-name
 
 
 2.9 (2016-10-19)
 
-   * Switched build to stack
-   * Added hsinstall installation script and updated windows dist script
-   * Various cabal file updates
-   * Moved copyright date up to 2016
-   * Updated README with better instructions for getting this software
+  * Switched build to stack
+  * Added hsinstall installation script and updated windows dist script
+  * Various cabal file updates
+  * Moved copyright date up to 2016
+  * Updated README with better instructions for getting this software
 
 
 2.8 (2015-09-18)
 
-   * Fixed some magazine and anthology naming rules
+  * Fixed some magazine and anthology naming rules
 
 
 2.7 (2015-05-29)
 
-   * Added back Control.Applicative import for GHC 7.8 compatibility
+  * Added back Control.Applicative import for GHC 7.8 compatibility
 
 
 2.6 (2015-05-25)
 
-   * Fixed an error in bad DSL command index reporting
-   * Replaced deprecated Control.Monad.Error with
-     Control.Monad.Except
-   * Replaced deprecated System.Cmd with System.Process
-   * Removed useless import of Control.Applicative
-   * Updated cabal homepage, tested-with and source-repository
-   * Removed unused path to rules file in unit tests
-   * Reformatted TODO and development notes into Markdown documents
-   * Updated boringfile with cabal sandbox filespecs
-   * Switched over to using Data.Version instead of hard-coded
-     version string
+  * Fixed an error in bad DSL command index reporting
+  * Replaced deprecated Control.Monad.Error with
+    Control.Monad.Except
+  * Replaced deprecated System.Cmd with System.Process
+  * Removed useless import of Control.Applicative
+  * Updated cabal homepage, tested-with and source-repository
+  * Removed unused path to rules file in unit tests
+  * Reformatted TODO and development notes into Markdown documents
+  * Updated boringfile with cabal sandbox filespecs
+  * Switched over to using Data.Version instead of hard-coded
+    version string
 
 
 2.5 (2014-04-04)
 
-   * Additions and modifications to the stock rules to both support
-     more books and also use more generic rules than before
-   * Added code to set proper case for Roman numerals in titles
-   * Added code to handle file-as names with parenthesized name info
-   * Simplified and consolidated special-character filtering code
-   * Added support for multiline fields in the Metadata
+  * Additions and modifications to the stock rules to both support
+    more books and also use more generic rules than before
+  * Added code to set proper case for Roman numerals in titles
+  * Added code to handle file-as names with parenthesized name info
+  * Simplified and consolidated special-character filtering code
+  * Added support for multiline fields in the Metadata
 
 
 2.4 (2014-03-19)
 
-   * Fixed problem in Windows cmd shell with missing UNIX HOME
-     env variable
-   * Now gracefully handling last-name-first creators. For books
-     with no file-as and the Creator text arranged last-name-first
-     with a comma, do the right thing.
-   * Added a new rule for generically-titled magazines with an issue
-   * Incorporated project website info into README.md and
-     changelog.md files. This information is now in source control
-     where it belongs.
-   * Added missing files to .cabal for sdist
-   * Changed copyright date range to 2014
+  * Fixed problem in Windows cmd shell with missing UNIX HOME
+    env variable
+  * Now gracefully handling last-name-first creators. For books
+    with no file-as and the Creator text arranged last-name-first
+    with a comma, do the right thing.
+  * Added a new rule for generically-titled magazines with an issue
+  * Incorporated project website info into README.md and
+    changelog.md files. This information is now in source control
+    where it belongs.
+  * Added missing files to .cabal for sdist
+  * Changed copyright date range to 2014
 
 
 2.3 (2013-09-20)
 
-   * These tools now support both epub2 and epub3
-   * Documentation changes and additions
+  * These tools now support both epub2 and epub3
+  * Documentation changes and additions
 
 
 2.2 (2013-04-14)
 
-   * Updated to build against recent changes and bug fixes in
-     epub-metadata 3.0
-   * All support data files have been brought into the binaries
-     now. This makes these tools more tolerant to being moved to a
-     different location than what they were configured for build with.
-   * Some documentation additions and changes
+  * Updated to build against recent changes and bug fixes in
+    epub-metadata 3.0
+  * All support data files have been brought into the binaries
+    now. This makes these tools more tolerant to being moved to a
+    different location than what they were configured for build with.
+  * Some documentation additions and changes
 
 
 2.1.1 (2013-02-03)
 
-   * Fixed a stack overflow problem with some epub documents
+  * Fixed a stack overflow problem with some epub documents
 
 
 2.1.0 (2013-02-01)
 
-   * Added new subjectMatch command to the DSL, similar to
-     authorMatch. This is being primarily used to detect anthology
-     publications.
-   * Removed some rules that are now handled by anthology detection,
-     and fixed relevant unit tests
-   * Clarified DSL documentation for authorMatch a little more
-   * Modified rules for some magazines to reflect changes to
-     recent editions
-   * Fixed an error in the DSL documentation
-   * Fixed a bug in epubzip where no epub file will be created if
-     none already exists
+  * Added new subjectMatch command to the DSL, similar to
+    authorMatch. This is being primarily used to detect anthology
+    publications.
+  * Removed some rules that are now handled by anthology detection,
+    and fixed relevant unit tests
+  * Clarified DSL documentation for authorMatch a little more
+  * Modified rules for some magazines to reflect changes to
+    recent editions
+  * Fixed an error in the DSL documentation
+  * Fixed a bug in epubzip where no epub file will be created if
+    none already exists
 
 
 2.0.0 (2012-10-31)
 
-   * Major redesign of the formatting rules system. Renaming
-     machinery is now described in a domain-specific language,
-     NOT in statically compiled code. Users are able to extend the
-     functionality with custom naming rules in conf files.
-   * Added interactive mode to ask about each file rename as they
-     happen, this is like darcs now!
-   * Added ability to specify target directory for books to be
-     moved to as part of renaming. Includes code to check that target
-     directory exists.
-   * Removed --overwrite option. Turns out, renameFile has always
-     been smart enough to not overwrite existing.
-   * Added / character to filters, a big no-no character for file
-     paths on most sane filesystems
-   * Publication year was looking for publication before
-     original-publication, causing problems in books that have
-     both tags
-   * Miscellaneous rules changes for various publications
+  * Major redesign of the formatting rules system. Renaming
+    machinery is now described in a domain-specific language,
+    NOT in statically compiled code. Users are able to extend the
+    functionality with custom naming rules in conf files.
+  * Added interactive mode to ask about each file rename as they
+    happen, this is like darcs now!
+  * Added ability to specify target directory for books to be
+    moved to as part of renaming. Includes code to check that target
+    directory exists.
+  * Removed --overwrite option. Turns out, renameFile has always
+    been smart enough to not overwrite existing.
+  * Added / character to filters, a big no-no character for file
+    paths on most sane filesystems
+  * Publication year was looking for publication before
+    original-publication, causing problems in books that have
+    both tags
+  * Miscellaneous rules changes for various publications
 
 
 1.1.2 (2012-01-29)
 
-   * Changed how this code provides epub zip file contents as a
-     ByteString to the epub-metadata library. Need to read this data
-     strictly to avoid dangling open files.
-   * Corrected for breakage due to change in title format of Eclipse magazine
-   * Some work done on the utility script for deploying Windows
-     binaries of these tools
-   * Added parsing support and test cases for more date formats
-   * Minor usage info changes
+  * Changed how this code provides epub zip file contents as a
+    ByteString to the epub-metadata library. Need to read this data
+    strictly to avoid dangling open files.
+  * Corrected for breakage due to change in title format of Eclipse magazine
+  * Some work done on the utility script for deploying Windows
+    binaries of these tools
+  * Added parsing support and test cases for more date formats
+  * Minor usage info changes
 
 
 1.1.1 (2011-11-15)
 
-   * Changed how publication date is found to more closely follow
-     the OPF spec recommendations
-   * Changed the switches related to publication date
-   * Redesigned unit test code and added more tests for new date code
+  * Changed how publication date is found to more closely follow
+    the OPF spec recommendations
+  * Changed the switches related to publication date
+  * Redesigned unit test code and added more tests for new date code
 
 
 1.1.0 (2011-11-04)
 
-   * Huge redesign of how formatting works, dramatically shortening
-     the code needed to handle any given book type. Code is much more
-     monadic now and consolidated into one module.
-   * Many changes/additions to magazine and compilation book name
-     formatting
-   * Fixed a group of bugs that occur when a creator has only a
-     single word for their name
-   * Extensive changes/additions to unit testing for above
+  * Huge redesign of how formatting works, dramatically shortening
+    the code needed to handle any given book type. Code is much more
+    monadic now and consolidated into one module.
+  * Many changes/additions to magazine and compilation book name
+    formatting
+  * Fixed a group of bugs that occur when a creator has only a
+    single word for their name
+  * Extensive changes/additions to unit testing for above
 
 
 1.0.0.1 (2011-10-27)
 
-   * Extensive changes to the cabal build of this project to bring
-     it up to Cabal 1.10
-   * Unit tests now use the test-suite cabal stanza
+  * Extensive changes to the cabal build of this project to bring
+    it up to Cabal 1.10
+  * Unit tests now use the test-suite cabal stanza
 
 
 1.0.0.0 (2011-04-23)
 
-   * Initial release
+  * Initial release
diff --git a/epub-tools.cabal b/epub-tools.cabal
--- a/epub-tools.cabal
+++ b/epub-tools.cabal
@@ -1,108 +1,144 @@
-name:                epub-tools
-cabal-version:       >= 1.10
-version:             2.11
-build-type:          Simple
-license:             ISC
-license-file:        LICENSE
-copyright:           2008-2018 Dino Morelli
-author:              Dino Morelli 
-maintainer:          Dino Morelli <dino@ui3.info>
-homepage:            https://github.com/dino-/epub-tools.git
-synopsis:            Command line utilities for working with epub files
-description:         A suite of command-line utilities for creating and manipulating epub book files. Included are: epubmeta, epubname, epubzip
-category:            Application, Console
-tested-with:         GHC >= 8.0.1
+cabal-version: 2.2
 
-extra-source-files:  .gitignore
-                     changelog.md
-                     doc/dev/notes.md
-                     doc/hcar/epubtoolsCommandlineepubU-De.tex
-                     doc/INSTALL
-                     README.md
-                     stack.yaml
-                     testsuite/EpubTools/*.hs
-                     testsuite/EpubTools/Test/EpubName/*.hs
-                     TODO.md
-                     util/all-books.hs
-                     util/install.hs
-                     util/win-dist.sh
+name:         epub-tools
+version:      3.0
+synopsis:     Command line utilities for working with epub files
+description:  A suite of command-line utilities for creating and manipulating epub book files. Included are: epubmeta, epubname, epubzip
+category:     Application, Console
+homepage:     https://github.com/dino-/epub-tools.git
+author:       Dino Morelli 
+maintainer:   Dino Morelli <dino@ui3.info>
+copyright:    2008 Dino Morelli
+license:      ISC
+license-file: LICENSE
+build-type:   Simple
+tested-with:  GHC >= 9.2.6
+extra-source-files:
+  .gitignore
+  changelog.md
+  doc/dev/notes.md
+  doc/hcar/epubtoolsCommandlineepubU-De.tex
+  doc/INSTALL
+  README.md
+  stack.yaml
+  TODO.md
+  util/all-books.hs
+  util/install.hs
+  util/win-dist.sh
 
-source-repository    head
-   type:             git
-   location:         https://github.com/dino-/epub-tools.git
+source-repository head
+   type: git
+   location: https://github.com/dino-/epub-tools.git
 
-executable           epubmeta
-   main-is:          EpubTools/epubmeta.hs
-   build-depends:    base >= 3 && < 5, bytestring, directory,
-                     epub-metadata >= 4.0, filepath, mtl, process,
-                     zip-archive
-   hs-source-dirs:   src
-   other-modules:    EpubTools.EpubMeta.Display
-                     EpubTools.EpubMeta.Edit
-                     EpubTools.EpubMeta.Export
-                     EpubTools.EpubMeta.Import
-                     EpubTools.EpubMeta.Opts
-                     EpubTools.EpubMeta.Util
-                     Paths_epub_tools
-   ghc-options:      -Wall -rtsopts -with-rtsopts=-K32m
-   default-language: Haskell2010
+common lang
+  default-language: Haskell2010
+  build-depends:
+      directory
+    , epub-metadata >= 5.1
+    , filepath
+    , mtl
+  ghc-options:
+    -fwarn-tabs
+    -Wall
+    -Wcompat
+    -Wincomplete-record-updates
+    -Wincomplete-uni-patterns
+    -Wredundant-constraints
+  autogen-modules:
+    Paths_epub_tools
 
-executable           epubname
-   main-is:          EpubTools/epubname.hs
-   build-depends:    base >= 3 && < 5, directory, epub-metadata >= 4.0,
-                     filepath, mtl, parsec, regex-compat
-   hs-source-dirs:   src
-   other-modules:    EpubTools.EpubName.Doc.Dsl
-                     EpubTools.EpubName.Doc.Rules
-                     EpubTools.EpubName.Format.Author
-                     EpubTools.EpubName.Format.Compile
-                     EpubTools.EpubName.Format.Format
-                     EpubTools.EpubName.Format.PubYear
-                     EpubTools.EpubName.Format.Util
-                     EpubTools.EpubName.Main
-                     EpubTools.EpubName.Opts
-                     EpubTools.EpubName.Prompt
-                     EpubTools.EpubName.Util
-                     Paths_epub_tools
-   ghc-options:      -Wall -rtsopts -with-rtsopts=-K32m
-   default-language: Haskell2010
+common rts
+  ghc-options:
+    -rtsopts
+    -with-rtsopts=-K32m
 
-test-suite           epubname-tests
-   type:             exitcode-stdio-1.0
-   main-is:          EpubTools/test-epubname.hs
-   build-depends:    base, directory, epub-metadata >= 4.0, filepath,
-                     HUnit, mtl, parsec, regex-compat
-   hs-source-dirs:   src testsuite
-   other-modules:    EpubTools.EpubName.Doc.Rules
-                     EpubTools.EpubName.Format.Author
-                     EpubTools.EpubName.Format.Compile
-                     EpubTools.EpubName.Format.Format
-                     EpubTools.EpubName.Format.PubYear
-                     EpubTools.EpubName.Format.Util
-                     EpubTools.EpubName.Main
-                     EpubTools.EpubName.Opts
-                     EpubTools.EpubName.Util
-                     EpubTools.Test.EpubName.Format
-                     EpubTools.Test.EpubName.PubYear
-                     Paths_epub_tools
-   ghc-options:      -Wall -rtsopts -with-rtsopts=-K32m
-   default-language: Haskell2010
+executable epubmeta
+  import: lang
+  import: rts
+  hs-source-dirs: src/app
+  main-is: epubmeta.hs
+  build-depends:
+      base >= 3 && < 5
+    , bytestring
+    , process
+    , zip-archive
+  other-modules:
+    EpubTools.EpubMeta.Display
+    EpubTools.EpubMeta.Edit
+    EpubTools.EpubMeta.Export
+    EpubTools.EpubMeta.Import
+    EpubTools.EpubMeta.Opts
+    EpubTools.EpubMeta.Util
+    Paths_epub_tools
 
-executable           epubzip
-   main-is:          EpubTools/epubzip.hs
-   build-depends:    base >= 3 && < 5, directory, epub-metadata >= 4.0,
-                     filepath, mtl, parsec, regex-compat
-   hs-source-dirs:   src
-   other-modules:    EpubTools.EpubName.Doc.Rules
-                     EpubTools.EpubName.Format.Author
-                     EpubTools.EpubName.Format.Compile
-                     EpubTools.EpubName.Format.Format
-                     EpubTools.EpubName.Format.PubYear
-                     EpubTools.EpubName.Format.Util
-                     EpubTools.EpubName.Main
-                     EpubTools.EpubName.Opts
-                     EpubTools.EpubName.Util
-                     EpubTools.EpubZip.Opts
-                     Paths_epub_tools
-   ghc-options:      -Wall
-   default-language: Haskell2010
+executable epubname
+  import: lang
+  import: rts
+  hs-source-dirs: src/app
+  main-is: epubname.hs
+  build-depends:
+      base >= 3 && < 5
+    , containers
+    , parsec
+    , regex-compat
+  other-modules:
+    EpubTools.EpubName.Doc.Dsl
+    EpubTools.EpubName.Doc.Rules
+    EpubTools.EpubName.Format.Author
+    EpubTools.EpubName.Format.Compile
+    EpubTools.EpubName.Format.Format
+    EpubTools.EpubName.Format.PubYear
+    EpubTools.EpubName.Format.Util
+    EpubTools.EpubName.Main
+    EpubTools.EpubName.Opts
+    EpubTools.EpubName.Prompt
+    EpubTools.EpubName.Util
+    Paths_epub_tools
+
+test-suite epubname-tests
+  import: lang
+  import: rts
+  type: exitcode-stdio-1.0
+  hs-source-dirs: src/app src/tests
+  main-is: test-epubname.hs
+  build-depends:
+      base >= 3 && < 5
+    , containers
+    , HUnit
+    , parsec
+    , regex-compat
+  other-modules:
+    EpubTools.EpubName.Doc.Rules
+    EpubTools.EpubName.Format.Author
+    EpubTools.EpubName.Format.Compile
+    EpubTools.EpubName.Format.Format
+    EpubTools.EpubName.Format.PubYear
+    EpubTools.EpubName.Format.Util
+    EpubTools.EpubName.Main
+    EpubTools.EpubName.Opts
+    EpubTools.EpubName.Util
+    EpubTools.Test.EpubName.Format
+    EpubTools.Test.EpubName.PubYear
+    Paths_epub_tools
+
+executable epubzip
+  import: lang
+  hs-source-dirs: src/app
+  main-is: epubzip.hs
+  build-depends:
+      base >= 3 && < 5
+    , containers
+    , parsec
+    , regex-compat
+  other-modules:
+    EpubTools.EpubName.Doc.Rules
+    EpubTools.EpubName.Format.Author
+    EpubTools.EpubName.Format.Compile
+    EpubTools.EpubName.Format.Format
+    EpubTools.EpubName.Format.PubYear
+    EpubTools.EpubName.Format.Util
+    EpubTools.EpubName.Main
+    EpubTools.EpubName.Opts
+    EpubTools.EpubName.Util
+    EpubTools.EpubZip.Opts
+    Paths_epub_tools
diff --git a/src/EpubTools/EpubMeta/Display.hs b/src/EpubTools/EpubMeta/Display.hs
deleted file mode 100644
--- a/src/EpubTools/EpubMeta/Display.hs
+++ /dev/null
@@ -1,26 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubMeta.Display
-   where
-
-import Codec.Epub
-import Control.Monad ( when )
-
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
-
-
-display :: Options -> FilePath -> EM ()
-display opts zipPath = do
-   xml <- getPkgXmlFromZip zipPath
-
-   pkg <- format `fmap` getPackage xml
-   meta <- format `fmap` getMetadata xml
-   liftIO $ mapM_ putStr [pkg, meta]
-
-   when (optVerbose opts) $ do
-      mf <- format `fmap` getManifest xml
-      spine <- format `fmap` getSpine xml
-      guide <- format `fmap` getGuide xml
-      liftIO $ mapM_ putStr [mf, spine, guide]
diff --git a/src/EpubTools/EpubMeta/Edit.hs b/src/EpubTools/EpubMeta/Edit.hs
deleted file mode 100644
--- a/src/EpubTools/EpubMeta/Edit.hs
+++ /dev/null
@@ -1,83 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{-# LANGUAGE ScopedTypeVariables #-}
-
-module EpubTools.EpubMeta.Edit
-   ( edit )
-   where
-
-import Control.Exception
-import Control.Monad
-import System.Directory
-import System.FilePath
-import System.Environment
-import System.Exit
-import System.Process ( system )
-import Text.Printf
-
-import EpubTools.EpubMeta.Export
-import EpubTools.EpubMeta.Import
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
-
-
-{- Turn an (IO a) action into a (Maybe a) with a value of Nothing
-   if an IO exception is raised
--}
-ioMaybe :: IO a -> IO (Maybe a)
-ioMaybe action = do
-   result :: (Either SomeException a) <- try action
-   either (const . return $ Nothing) (return . Just) result
-
-
-{- Look through the user's environment looking for their preferred 
-   editor. Use /usr/bin/vi as a default if none found
--}
-findEditor :: EM FilePath
-findEditor = do
-   mbEditor <- liftIO $ foldr (liftM2 mplus) (findExecutable "vi")
-      [ ioMaybe $ getEnv "EDITOR"
-      , ioMaybe $ getEnv "VISUAL"
-      ]
-
-   maybe (throwError "epubmeta: ERROR: Could not find a suitable editor in your EDITOR or VISUAL environment variables, and could not find the vi binary. Fix this situation or use epubmeta export/import.") return mbEditor
-
-
-edit :: Options -> FilePath -> EM ()
-edit opts = edit' $ optEdit opts
-
-
-edit' :: Edit -> FilePath -> EM ()
-
-edit' NoBackup zipPath = do
-   -- Make a temp path for the OPF document
-   tempDir <- liftIO $ getTemporaryDirectory
-   let opfPath = tempDir </> "epubmeta-temp-content.opf"
-
-   -- Dump the OPF document to this path
-   exportOpf (defaultOptions { optExport = ToPath opfPath }) zipPath
-
-   -- Open this file in the user's editor and wait for it
-   editor <- findEditor
-   ec <- liftIO $ system $ printf "%s %s" editor opfPath
-
-   case ec of
-      ExitSuccess -> do
-         -- Modify the book with the now-edited OPF data
-         importOpf (defaultOptions { optImport = Just opfPath }) zipPath
-
-         -- Delete the temp file
-         liftIO $ removeFile opfPath
-
-      ExitFailure _ -> do
-         -- Delete the temp file
-         liftIO $ removeFile opfPath
-
-         throwError "epubmeta: ERROR: Something went wrong during editing, your file has not been changed."
-
-edit' (Backup suffix) zipPath = do
-   liftIO $ copyFile zipPath (zipPath ++ suffix)
-   edit' NoBackup zipPath
-
-edit' NotEditing _ = undefined
diff --git a/src/EpubTools/EpubMeta/Export.hs b/src/EpubTools/EpubMeta/Export.hs
deleted file mode 100644
--- a/src/EpubTools/EpubMeta/Export.hs
+++ /dev/null
@@ -1,35 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubMeta.Export
-   ( exportOpf )
-   where
-
-import Codec.Epub.IO
-import System.FilePath
-
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
-
-
-write :: FilePath -> String -> EM ()
-write path contents = liftIO $ do
-   writeFile path contents
-   putStrLn $ "OPF XML saved as: " ++ path
-
-
-exportOpf :: Options -> FilePath -> EM ()
-exportOpf opts = export' (optExport opts)
-
-
-export' :: Export -> FilePath -> EM ()
-
-export' Existing zipPath = do
-   (fullPath, contents) <- getPkgPathXmlFromZip zipPath
-   write (takeFileName fullPath) contents
-
-export' (ToPath path) zipPath = do
-   contents <- getPkgXmlFromZip zipPath
-   write path contents
-
-export' NoExport _ = undefined
diff --git a/src/EpubTools/EpubMeta/Import.hs b/src/EpubTools/EpubMeta/Import.hs
deleted file mode 100644
--- a/src/EpubTools/EpubMeta/Import.hs
+++ /dev/null
@@ -1,44 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubMeta.Import
-   ( importOpf )
-   where
-
-import Codec.Archive.Zip ( Entry (..), addEntryToArchive, readEntry, toArchive )
-import Codec.Epub.IO ( getPkgPathXmlFromBS, writeArchive )
-import qualified Data.ByteString as BS
-import qualified Data.ByteString.Lazy as BL
-import Data.Maybe ( fromJust )
-import System.Directory ( removeFile )
-
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
-
-
-importOpf :: Options -> FilePath -> EM ()
-importOpf opts zipPath = do
-   -- The file we want to insert into the book
-   let pathToNewOpf = fromJust . optImport $ opts
-
-   -- Make new file into a Zip Entry
-   tempEntry <- liftIO $ readEntry [] pathToNewOpf
-
-   -- The path in the book where new file must go
-   strictBytes <- liftIO $ BS.readFile zipPath
-   (pathToOldFile, _) <- getPkgPathXmlFromBS strictBytes
-
-   -- Adjust the entry's path
-   let newEntry = tempEntry { eRelativePath = pathToOldFile }
-
-   -- Bytes of existing archive already loaded, make an Archive from that
-   let oldArchive = toArchive . BL.fromChunks $ [strictBytes]
-
-   -- Add new entry to it (replacing old one)
-   let newArchive = addEntryToArchive newEntry oldArchive
-
-   -- We're ready to write the new file, delete the old one
-   liftIO $ removeFile zipPath
-
-   -- Write the new archive out
-   liftIO $ writeArchive zipPath newArchive
diff --git a/src/EpubTools/EpubMeta/Opts.hs b/src/EpubTools/EpubMeta/Opts.hs
deleted file mode 100644
--- a/src/EpubTools/EpubMeta/Opts.hs
+++ /dev/null
@@ -1,111 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubMeta.Opts
-   ( Edit (..)
-   , Export (..)
-   , Options (..), defaultOptions
-   , parseOpts, usageText
-   )
-   where
-
-import Data.Version ( showVersion )
-import Paths_epub_tools ( version )
-import System.Console.GetOpt
-
-import EpubTools.EpubMeta.Util
-
-
--- There are three states for this, representing with a new type
-data Edit
-   = Backup String
-   | NoBackup
-   | NotEditing
-   deriving Eq
-
-
-maybeToEdit :: Maybe String -> Edit
-maybeToEdit (Just suffix) = Backup suffix
-maybeToEdit Nothing       = NoBackup
-
-
--- There are three states for this, representing with a new type
-data Export
-   = ToPath String   -- User specified a file to export to
-   | Existing        -- User wants export to use existing name
-   | NoExport        -- This arg wasn't used at all
-   deriving Eq
-
-
-maybeToExport :: Maybe String -> Export
-maybeToExport (Just file) = ToPath file
-maybeToExport Nothing     = Existing
-
-
-data Options = Options
-   { optHelp :: Bool
-   , optEdit :: Edit
-   , optImport :: Maybe String
-   , optExport :: Export
-   , optVerbose :: Bool
-   }
-
-
-defaultOptions :: Options
-defaultOptions = Options
-   { optHelp = False
-   , optEdit = NotEditing
-   , optImport = Nothing
-   , optExport = NoExport
-   , optVerbose = False
-   }
-
-
-options :: [OptDescr (Options -> Options)]
-options =
-   [ Option ['h'] ["help"] 
-      (NoArg (\opts -> opts { optHelp = True } ))
-      "This help text"
-   , Option ['e'] ["edit-opf"]
-      (OptArg (\f opts -> opts { optEdit = maybeToEdit f }) "SUF")
-      "Edit a book's OPF XML data in a text editor. If an optional suffix is supplied, a backup of the book will be created with that suffix"
-   , Option ['x'] ["export"]
-      (OptArg (\f opts -> opts { optExport = maybeToExport f }) "FILE")
-      "Export the book's OPF XML metadata to a file. If no file name is given, the name of the file in the book will be used"
-   , Option ['i'] ["import"]
-      (ReqArg (\f opts -> opts { optImport = Just f }) "FILE")
-      "Import OPF metadata from the supplied XML file"
-   , Option ['v'] ["verbose"]
-      (NoArg (\opts -> opts { optVerbose = True } )) 
-      "Display all OPF package info, including manifest, spine and guide"
-   ]
-
-
-parseOpts :: [String] -> EM (Options, [String])
-parseOpts argv = 
-   case getOpt Permute options argv of
-      (o,n,[]  ) -> return (foldl (flip id) defaultOptions o, n)
-      (_,_,errs) -> throwError $ concat errs ++ usageText
-
-
-usageText :: String
-usageText = (usageInfo header options) ++ "\n" ++ footer
-   where
-      header = init $ unlines
-         [ "Usage: epubmeta [OPTIONS] EPUBFILE"
-         , "View or edit epub book metadata"
-         , ""
-         , "Note that 'edit' here means you get dumped into an editor with the XML document and are on your own."
-         , ""
-         , "Options:"
-         ]
-      footer = init $ unlines
-         [ "The -e feature will look for an editor in this order: the EDITOR environment variable, the VISUAL environment variable, vi"
-         , ""
-         , "For more information on the epub format:"
-         , "   epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm"
-         , "   epub3: http://www.idpf.org/epub/30/spec/epub30-publications.html"
-         , ""
-         , ""
-         , "Version " ++ (showVersion version) ++ "  Dino Morelli <dino@ui3.info>"
-         ]
diff --git a/src/EpubTools/EpubMeta/Util.hs b/src/EpubTools/EpubMeta/Util.hs
deleted file mode 100644
--- a/src/EpubTools/EpubMeta/Util.hs
+++ /dev/null
@@ -1,17 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubMeta.Util
-   ( EM , runEM
-   , liftIO
-   , throwError
-   )
-   where
-
-import Control.Monad.Except
-
-
-type EM a = (ExceptT String IO) a
-
-runEM :: EM a -> IO (Either String a)
-runEM = runExceptT
diff --git a/src/EpubTools/EpubName/Doc/Dsl.hs b/src/EpubTools/EpubName/Doc/Dsl.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Doc/Dsl.hs
+++ /dev/null
@@ -1,183 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubName.Doc.Dsl
-   ( docs
-   )
-   where
-
-
-docs :: String
-docs = unlines
-   [ "epubname rules documentation"
-   , "----------------------------"
-   , ""
-   , "Book naming is controlled by a set of rules loaded from a file "
-   , ""
-   , "At runtime, a rules file is searched for, first in the user's home directory and then in the installed files for epubname. The paths of these files specific to your system are listed in --help output."
-   , ""
-   , "The rules are checked, from first to last, for each file being renamed. The first rule that succeeds is used."
-   , ""
-   , ""
-   , "Rules have the following format:"
-   , ""
-   , "   label"
-   , "      authorMatch \"...\""
-   , "      subjectMatch \"...\""
-   , "      titlePat \"...\""
-   , "      name \"...\""
-   , ""
-   , "label is to identify what the rule is for to the user and also for verbose output (helpful for debugging rules)"
-   , ""
-   , "The indented fields are rule commands and consist of:"
-   , ""
-   , "   authorMatch - Optional. If present, it's a regexp pattern that must"
-   , "      match an author in the book's metadata. If it doesn't match, this"
-   , "      rule is rejected. This may be a partial match, \"bar\" will match if"
-   , "      any author looks like \"foo bar baz\""
-   , ""
-   , "   subjectMatch - Optional. If present, it's a regexp pattern that must"
-   , "      match a subject in the book's metadata. If it doesn't match, this"
-   , "      rule is rejected. This may be a partial match, \"bar\" will match if"
-   , "      any subject looks like \"foo bar baz\""
-   , ""
-   , "   titlePat - a regular expression for picking apart the title string in"
-   , "      the book's metadata. The pieces in capture groups ( ) are exposed "
-   , "      as numbered matches in the name construction field. If this match"
-   , "      fails, this rule is rejected."
-   , ""
-   , "   name - instructions for building up a new book filename, most often"
-   , "      using the parts extracted in titlePat. More on this below"
-   , ""
-   , ""
-   , "In the name command, there are several functions that can be used. These functions are delimited with parenthesis in the name command's expression."
-   , ""
-   , "   authors - Get formatted authors from the metadata. If there are any"
-   , "      authors, a hyphen will be placed after them in the resulting string."
-   , ""
-   , "      syntax: (authors)"
-   , ""
-   , "   INT - Retrieve the text of a specific numbered item from the title"
-   , "      pattern match results"
-   , ""
-   , "      syntax: (INT)"
-   , ""
-   , "   monthNum INDEX - Construct a numeric month from a variety of English"
-   , "      months strings. Including abbreviations and ranges"
-   , ""
-   , "      syntax: (monthNum INT)"
-   , ""
-   , "      examples:"
-   , "         \"January\"           -> \"01\""
-   , "         \"Feb\"               -> \"02\""
-   , "         \"Mar-Apr\"           -> \"03_04\""
-   , "         \"September/October\" -> \"09_10\""
-   , ""
-   , "   pad WIDTH INDEX - Pad a numeric value from the pattern match results"
-   , ""
-   , "      syntax: (pad INT INT)"
-   , ""
-   , "      examples:"
-   , "         2 \"3\"    -> \"03\""
-   , "         3 \"10\"   -> \"010\""
-   , ""
-   , "   publisher - Get the formatted publisher file-as string from the"
-   , "      metadata"
-   , ""
-   , "      syntax: (publisher)"
-   , ""
-   , "   scrub INDEX - Clean up an item from the pattern match results. A"
-   , "      large number of punctuation is removed and/or converted into"
-   , "      other characters, and the string is CamelCased. In the end, only"
-   , "      alphanumerics, - and _ are used."
-   , ""
-   , "      syntax: (scrub INT)"
-   , ""
-   , "   wordNum WIDTH INDEX - Construct a numeric string from an English word"
-   , "      for a number, with padding. Works on strings from \"One\" to \"Twenty\""
-   , ""
-   , "      syntax: (wordNum INT INT)"
-   , ""
-   , "      example:"
-   , "         2 \"One\"    -> \"01\""
-   , "         3 \"Twenty\" -> \"020\""
-   , ""
-   , "   year - Get the formatted publication year from the metadata"
-   , ""
-   , "      syntax: (year)"
-   , ""
-   , "   Anything else, not in parenthesis, is a string literal to be used at"
-   , "   that position in the resulting string."
-   , ""
-   , ""
-   , "This probably seems confusing, some examples may help."
-   , ""
-   , "This one is a fictitious magazine with issue and date info in the title:"
-   , ""
-   , "   original book metadata:"
-   , "      title: Adventurer's Weekly Issue 53, May 2008"
-   , ""
-   , "   epubname rule:"
-   , "      magAdventureWeekly"
-   , "         titlePat \"(Adventurer's Weekly) Issue ([0-9]+), ([^ ]+) ([0-9]{4})$\""
-   , "         name \"(scrub 1)Magazine(pad 3 2)_(4)-(monthNum 3)\""
-   , ""
-   , "   resulting title:"
-   , "      \"AdventurersWeeklyMagazine053_2008-05\""
-   , ""
-   , ""
-   , "Here's one using some more functions:"
-   , ""
-   , "   original book metadata:"
-   , "      title: Thrilling Journeys Three"
-   , "      creator"
-   , "         role: aut"
-   , "         text: The Thrilling Authors"
-   , ""
-   , "   epubname rule:"
-   , "      compThrillingJourneys"
-   , "         author \"Thrilling Authors\""
-   , "         titlePat \"(Thrilling Journeys) (.*)\""
-   , "         name \"(scrub 1)(wordNum 2 2)\""
-   , ""
-   , "   resulting title:"
-   , "      \"ThrillingJourneys03\""
-   , ""
-   , ""
-   , "And here's what happens when an ordinary (non-periodical) book is formatted:"
-   , ""
-   , "   original book metadata:"
-   , "      title: Springtime in the Village"
-   , "      creator"
-   , "         role: aut"
-   , "         file-as: Dandridge, Suzanne"
-   , "         text: Suzanne Dandridge"
-   , "      date"
-   , "         event: publication"
-   , "         text: 2012-10-09"
-   , ""
-   , "   epubname rule:"
-   , "      ordinary_book"
-   , "         titlePat \"(.*)\""
-   , "         name \"(authors)(scrub 1)(year)\""
-   , ""
-   , "   resulting title:"
-   , "      \"DandridgeSuzanne-SpringtimeInTheVillage_2012\""
-   , ""
-   , ""
-   , "The last rule in the built-in rules, ordinary_book is very important, and looks like this:"
-   , ""
-   , "   ordinary_book"
-   , "      titlePat \"(.*)\""
-   , "      name \"(authors)(scrub 1)(year)\""
-   , ""
-   , "This rule will match any book and is used to format all simple author-title situations. This rules is responsible for the basic epubname behavior and should always be present and always be last."
-   , ""
-   , ""
-   , "Preceding a rule label with ! will \"switch off\" the entire rule. It's a simple way to comment out a single rule in its entirety. example:"
-   , ""
-   , "   !compThrillingJourneys"
-   , "      author \"Thrilling Authors\""
-   , "      titlePat \"(Thrilling Journeys) (.*)\""
-   , "      name \"(scrub 1)(wordNum 2 2)\""
-   ]
diff --git a/src/EpubTools/EpubName/Doc/Rules.hs b/src/EpubTools/EpubName/Doc/Rules.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Doc/Rules.hs
+++ /dev/null
@@ -1,137 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubName.Doc.Rules
-   ( defaults
-   )
-   where
-
-
-defaults :: String
-defaults = unlines
-   [ "magAeon"
-   , "   titlePat \"^A[eE]on ([^ ]+)$\""
-   , "   name \"AeonMagazine(wordNum 2 1)\""
-   , ""
-   , "magApex"
-   , "   titlePat \"^(Apex)[^0-9]*([0-9]{1,3}).*\""
-   , "   name \"(scrub 1)Magazine(pad 3 2)\""
-   , ""
-   , "magAnalog"
-   , "   titlePat \"^(A[^ ]*).*, ([^ ]+) ([0-9]{4})$\""
-   , "   name \"(scrub 1)SF(3)-(monthNum 2)\""
-   , ""
-   , "magBcs"
-   , "   titlePat \"(Beneath Ceaseless.*) #([0-9]+).*\""
-   , "   name \"(scrub 1)_Issue(pad 3 2)\""
-   , ""
-   , "magBlackStatic"
-   , "   titlePat \"^(Black Static Horror Magazine)[^0-9]*([0-9]+)$\""
-   , "   name \"(scrub 1)(pad 2 2)\""
-   , ""
-   , "magChallengingDestiny"
-   , "   titlePat \"^(Challenging Destiny) #([0-9]+).*\""
-   , "   name \"(scrub 1)Magazine(pad 3 2)\""
-   , ""
-   , "magClarkesworld"
-   , "   titlePat \"^(Clarkesworld)[^0-9]*([0-9]+)$\""
-   , "   name \"(1)(pad 3 2)\""
-   , ""
-   , "magFantasyMag"
-   , "   titlePat \"^(Fantasy Magazine)[^0-9]+([0-9]+).*\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magEclipse_word"
-   , "   titlePat \"^(Eclipse) +([a-zA-Z]+)$\""
-   , "   name \"(1)(wordNum 2 2)\""
-   , ""
-   , "magEclipse_num"
-   , "   titlePat \"^(Eclipse) +([0-9]+):.*\""
-   , "   name \"(1)(pad 2 2)\""
-   , ""
-   , "magFsf"
-   , "   authorMatch \"Spilogale\""
-   , "   titlePat \".* ([^ ]+) ([0-9]{4})$\""
-   , "   name \"FantasyScienceFiction(2)-(monthNum 1)\""
-   , ""
-   , "magFutureOrbits"
-   , "   titlePat \"(Future Orbits) Issue ([0-9]+), ([^ ]+) ([0-9]{4})$\""
-   , "   name \"(scrub 1)Magazine(pad 2 2)_(4)-(monthNum 3)\""
-   , ""
-   , "magGud"
-   , "   authorMatch \"GUD Magazine Authors\""
-   , "   titlePat \".* Magazine Issue ([0-9]+) ::.*\""
-   , "   name \"GUDMagazine(pad 2 1)\""
-   , ""
-   , "magInterzone"
-   , "   titlePat \"^(Interzone|INTERZONE)[^0-9]+([0-9]+).*\""
-   , "   name \"(scrub 1)SFF(pad 3 2)\""
-   , ""
-   , "magLightspeed_date"
-   , "   titlePat \"(Lightspeed) Magazine, (.*) (.*)\""
-   , "   name \"(1)(3)-(monthNum 2)\""
-   , ""
-   , "magLightspeed_issue"
-   , "   titlePat \"(Lightspeed).* Issue ([^ ]+)(.*)\""
-   , "   name \"(1)(pad 3 2)\""
-   , ""
-   , "magLunaStationQuarterly"
-   , "   titlePat \"(Luna Station Quarterly) - Issue ([0-9]+)\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magNemesis"
-   , "   titlePat \"(Nemesis Mag)azine #([0-9]+).*\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magRageMachine"
-   , "   titlePat \"(Rage Machine.*)--([^ ]+) ([0-9]{4})$\""
-   , "   name \"(scrub 1)_(3)-(monthNum 2)\""
-   , ""
-   , "magSomethingWicked"
-   , "   titlePat \"^(Something Wicked)[^0-9]*([0-9]+)\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magWeirdTales"
-   , "   titlePat \"^(Weird Tales)[^0-9]*([0-9]+)$\""
-   , "   name \"(scrub 1)(pad 3 2)\""
-   , ""
-   , "magLocus"
-   , "   titlePat \"^(Locus), ([a-zA-Z]+) ([0-9]{4})$\""
-   , "   name \"(scrub 1)Magazine(3)-(monthNum 2)\""
-   , ""
-   , "magGenericWithIssue"
-   , "   titlePat \"(.* Magazine):? Issue ([0-9]+).*\""
-   , "   name \"(scrub 1)(pad 2 2)\""
-   , ""
-   , "magGenericSubjWithIssue"
-   , "   subjectMatch \"magazine\""
-   , "   titlePat \"(.*) Issue ([0-9]+).*\""
-   , "   name \"(scrub 1)Magazine(pad 2 2)\""
-   , ""
-   , "magGenericVolNo3"
-   , "   titlePat \"(.*)Vol(ume)? ([0-9]{1,2}),? (No|Num)[^0-9]+([0-9]{3}).*\""
-   , "   name \"(scrub 1)Vol(pad 2 3)No(pad 3 5)\""
-   , ""
-   , "magGenericVolNo2"
-   , "   titlePat \"(.*)Vol(ume)? ([0-9]{1,2}),? (No|Num)[^0-9]+([0-9]{1,2})[^0-9]?.*\""
-   , "   name \"(scrub 1)Vol(pad 2 3)No(pad 2 5)\""
-   , ""
-   , "nonficWomensInstituteLibrary"
-   , "   authorMatch \"Woman's Institute\""
-   , "   titlePat \"(.*)\""
-   , "   name \"(scrub 1)\""
-   , ""
-   , "anthology_date"
-   , "   subjectMatch \"anthology\""
-   , "   titlePat \"(.*) - ([^ ]+) ([0-9]{4})\""
-   , "   name \"(scrub 1)(3)-(monthNum 2)\""
-   , ""
-   , "anthology"
-   , "   subjectMatch \"anthology\""
-   , "   titlePat \"(.*)\""
-   , "   name \"(scrub 1)(year)\""
-   , ""
-   , "ordinary_book"
-   , "   titlePat \"(.*)\""
-   , "   name \"(authors)(scrub 1)(year)\""
-   ]
diff --git a/src/EpubTools/EpubName/Format/Author.hs b/src/EpubTools/EpubName/Format/Author.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Format/Author.hs
+++ /dev/null
@@ -1,120 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubName.Format.Author
-   ( extractAuthors
-   , authorMatches
-   )
-   where
-
-import Codec.Epub.Data.Metadata
-import Control.Monad
-import Data.List ( intercalate )
-import Data.Maybe ( isJust )
-import Prelude hiding ( last )
-import Text.Printf
-import Text.Regex
-
-import EpubTools.EpubName.Format.Util
-
-
-extractAuthors :: EN String
-extractAuthors = do
-   fmap (fmtAuthor . separateCombined . justAuthors) $ asks gMetadata
-   where
-      fmtAuthor []  = ""
-      fmtAuthor [c] = formatSingleAuthor c
-      fmtAuthor cs = (formatMultiAuthors cs) ++ "-"
-
-
-separateCombined :: [Creator] -> [Creator]
-separateCombined = concatMap separateOne
-
-
-separateOne :: Creator -> [Creator]
-separateOne c@(Creator r _ _ di) =
-   case (splitRegex (mkRegex " (&|and) ") di) of
-      -- If there was only one, leave the file-as alone!
-      [_] -> [c]
-      -- Otherwise, explode them into separate CreatorS
-      ss  -> map (Creator r Nothing Nothing) ss
-
-
-formatSingleAuthor :: Creator -> String
-
-formatSingleAuthor (Creator _ (Just fa) _ di) = 
-   if ((fa == di) && all (/= ',') fa)
-      then formatSingleAuthor $ Creator Nothing Nothing Nothing di
-      else authorSingle [fa]
-
-formatSingleAuthor (Creator _ _         _ di) = 
-   authorSingle . reverse . nameParts $ di
-
-
-nameParts :: String -> [String]
-nameParts s = maybe [] id $ foldl mplus Nothing matches
-   where
-      matches = map ($ s)
-         [ matchRegex (mkRegex "(.*), +([^ ]+)$") >=> return . reverse
-         , matchRegex (mkRegex "(.*) +([^ ]+)$")
-         , matchRegex (mkRegex "(.*)")
-         ]
-
-
-lastName' :: String -> String
-lastName' s = maybe "" head $ foldl mplus Nothing matches
-   where
-      matches = map ($ s)
-         [ matchRegex (mkRegex "(.*),.*")
-         , matchRegex (mkRegex ".* (.*)")
-         , matchRegex (mkRegex "(.*)")
-         ]
-
-lastName :: Creator -> String
-lastName (Creator _ (Just fa) _ _ ) = lastName' fa
-lastName (Creator _ _         _ di) = lastName' di
-
-
-formatMultiAuthors :: [Creator] -> String
-formatMultiAuthors = (intercalate "_") . (map lastName)
-
-
-justAuthors :: Metadata -> [Creator]
-justAuthors = (filter isAut) . metaCreators
-   where
-      isAut (Creator (Just "aut") _ _ _) = True
-      isAut (Creator Nothing      _ _ _) = True
-      isAut _                            = False
-
-
-{- A common simple formatter for many book authors
--}
-authorSingle :: [String] -> String
-authorSingle (last:rest:_) =
-   printf "%s%s-" (scrubString last) (scrubString rest)
-authorSingle [n]           =
-   printf "%s-" $ scrubString $ takeWhile (/= '(') n
-authorSingle _             = undefined
-
-
-{- Throws an error if no author matches the pattern
--}
-authorMatches :: String -> EN ()
-authorMatches re = do
-   let authorMatches' (Creator _ _ _ di) =
-         matchRegex (mkRegex re) di
-
-   md <- asks gMetadata
-   unless (any isJust $ map authorMatches' $ justAuthors md) $ throwError ""
-
-
-{- Author names with a postfix like II, III, Jr. or Sr.
-   FIXME: Is anyone calling this?
--}
-{-
-authorPostfix :: [String] -> String
-authorPostfix (rest:last:postfix:_) =
-   printf "%s%s%s-" (filterCommon last) (filterCommon rest)
-      (filterCommon postfix)
-authorPostfix _             = undefined
--}
diff --git a/src/EpubTools/EpubName/Format/Compile.hs b/src/EpubTools/EpubName/Format/Compile.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Format/Compile.hs
+++ /dev/null
@@ -1,174 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
-
-module EpubTools.EpubName.Format.Compile
-   where
-
-import Data.Maybe ( catMaybes )
-import Text.ParserCombinators.Parsec
-
-import EpubTools.EpubName.Format.Author
-import EpubTools.EpubName.Format.Format
-
-
-parseRules :: SourceName -> String -> Either ParseError [Formatter]
-parseRules name = runParser pFormatters () name
-
-
-pFormatters :: Parser [Formatter]
-pFormatters = fmap catMaybes $ many pFormatter
-
-
-pFormatter :: Parser (Maybe Formatter)
-pFormatter = do
-   l <- many1Till (noneOf " ") eol
-   a <- optionMaybe $ try $ pCommand "authorMatch"
-   s <- optionMaybe $ try $ pCommand "subjectMatch"
-   t <- pCommand "titlePat"
-   n <- pName
-   newline <|> return ' '
-   return $ case head l of
-      '!' -> Nothing
-      _   -> Just $ Formatter l
-         [ maybe (return ()) authorMatches a
-         , maybe (return ()) subjectMatches s
-         ]
-         (extractTitle t) n
-
-   
-pName :: Parser [ReplF]
-pName = do
-   pCommandPrefix "name"
-   fs <- pRepls
-   eol
-   return fs
-
-
-pCommand :: String -> Parser String
-pCommand c = do
-   pCommandPrefix c
-   char '"'
-   qs <- manyTill (noneOf "\"") (char '"')
-   eol
-   return qs
-
-
-pCommandPrefix :: String -> Parser ()
-pCommandPrefix c = do
-   many1 space
-   string c
-   spaces
-   return ()
-
-
-{-
-pRepls :: Parser [[String]]
-pRepls = do
-   char '"'
-   rs <- many pExpr
-   char '"'
-   return rs
--}
-pRepls :: Parser [ReplF]
-pRepls = do
-   char '"'
-   rs <- many pExpr
-   char '"'
-
-   let extension = literal ".epub"
-   return $ rs ++ [publisher, extension]
-
-
-{-
-pExpr :: Parser [String]
-pExpr = pRepl <|> pLiteral
--}
-pExpr :: Parser ReplF
-pExpr = pRepl <|> pLiteral
-
-
---pRepl :: Parser [String]
-pRepl :: Parser ReplF
-pRepl = do
-   char '('
-   spaces
-   x <- pCall <|> pIndex
-   spaces
-   char ')'
-   return x
-
-
-{-
-pCall :: Parser [String]
-pCall = do
-   fn <- pFName
-   spaces
-   args <- pArgList
-   return $ fn : args
--}
-pCall :: Parser ReplF
-pCall = do
-   fn <- pFName
-   spaces
-   args <- pArgList
-   mkReplF $ fn : args
-
-
-pFName :: Parser String
-pFName = do
-   first <- letter
-   rest <- many alphaNum
-   return $ first : rest
-
-
-pArgList :: Parser [String]
-pArgList = sepEndBy (many1 digit) (char ' ')
-
-
-{-
-pIndex :: Parser [String]
-pIndex = do
-   ds <- many1 digit
-   return [ds]
--}
-pIndex :: Parser ReplF
-pIndex = do
-   ds <- many1 digit
-   mkReplF ["index", ds]
-
-
-{-
-pLiteral :: Parser [String]
-pLiteral = do
-   l <- many1 (noneOf "()\"")
-   return [l]
--}
-pLiteral :: Parser ReplF
-pLiteral = do
-   l <- many1 (noneOf "()\"")
-   mkReplF ["literal", l]
-
-
-many1Till :: Parser a -> Parser end -> Parser [a]
-many1Till p end = do
-   h <- p
-   t <- manyTill p end
-   return $ h : t
-
-
-eol :: Parser Char
-eol = newline <|> (eof >> return '\n')
-
-
-mkReplF :: [String] -> Parser ReplF
-mkReplF ("authors" : []) = return authors
-mkReplF ("index" : i : []) = return $ index i
-mkReplF ("literal" : s : []) = return $ literal s
-mkReplF ("monthNum" : i : []) = return $ monthNum i
-mkReplF ("pad" : w : i : []) = return $ pad w i
-mkReplF ("scrub" : i : []) = return $ scrub i
-mkReplF ("wordNum" : w : i : []) = return $ wordNum w i
-mkReplF ("year" : []) = return year
-mkReplF l = unexpected $ "Bad function definition: " ++ (concat l)
diff --git a/src/EpubTools/EpubName/Format/Format.hs b/src/EpubTools/EpubName/Format/Format.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Format/Format.hs
+++ /dev/null
@@ -1,300 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{-# LANGUAGE FlexibleContexts #-}
-
-module EpubTools.EpubName.Format.Format
-   ( ReplF
-   , authors, index, literal, monthNum, pad, publisher, scrub, wordNum
-   , year
-   , Formatter (..)
-   , tryFormatting
-   , subjectMatches
-   , extractTitle
-   )
-   where
-
-import Codec.Epub.Data.Metadata
-import Control.Monad.Except
-import Data.List ( isPrefixOf )
-import Data.Maybe ( isJust, listToMaybe )
-import Text.Printf
-import Text.Regex
-
-import EpubTools.EpubName.Format.Author
-import EpubTools.EpubName.Format.PubYear
-import EpubTools.EpubName.Format.Util
-import EpubTools.EpubName.Opts
-
-
-type ReplF = [String] -> EN String
-
-
-data Formatter = Formatter
-   String         -- formatter label
-   [EN ()]        -- field pattern matchers
-   (EN [String])  -- title pattern parser
-   [ReplF]        -- name building actions
-
-
-{- A single, simple formatter that should handle most books with one or
-   more author.
--}
-{- Not using this at the moment, but I'm reluctant to remove it. Could
-   be useful someday.
-ordinaryBookFormatter :: Formatter
-ordinaryBookFormatter = Formatter
-   "ordinary_book"
-   (return ())
-   (extractTitle ".*")
-   [authors, scrub "1", year]
--}
-
-
-{- Wrapper functions to perform some basic operations in the Except
-   String monad
--}
-
--- general purpose read in the Except String monad
-readE :: (MonadError String m, Read a) => String -> String -> m a
-readE msg s = case reads s of
-   [(x, "")] -> return x
-   _         -> throwError msg
-
-
--- read an Int in the Except String monad
-readIntE :: MonadError String m => String -> m Int
-readIntE = readE "Not a number"
-
-
--- get list element at a specific index in the Except String monad
-elemE :: MonadError String m => [a] -> Int -> m a
-elemE l i
-   | i < length l = return $ l !! i
-   | otherwise    = throwError $ "Bad array index: " ++ show (i + 1)
-
-
-{- Instructions in the book naming DSL. These instructions are used from
-   the rules file notation, in the name "..." command
--}
-
--- Get formatted authors from the metadata
-authors :: ReplF
-authors _ = extractAuthors
-
-
--- Retrieve a specific item from the title pattern match results
-index :: String -> ReplF
-index sIndex matches = readIntE sIndex >>= return . subtract 1
-   >>= elemE matches
-
-
--- Insert a specific string literal into the resulting name string
-literal :: String -> ReplF
-literal s = return . const s
-
-
-{- Construct a numeric month from a variety of English months strings
-   Including abbreviations and ranges
--}
-monthNum :: String -> ReplF
-monthNum sIndex matches = index sIndex matches >>= monthStrToNumbers
-
-
--- Pad a numeric value from the pattern match results
-pad :: String -> String -> ReplF
-pad sWidth sIndex matches = do
-   width <- readIntE sWidth
-   value <- index sIndex matches >>= readIntE
-   return $ printf "%0*d" (width :: Int) (value :: Int)
-
-
--- Get the formatted publisher file-as string from the metadata
-publisher :: ReplF
-publisher _ = do
-   md <- asks gMetadata
-   fmap (extractPublisher md) $ asks $ optPublisher . gOpts
-
-
--- Clean up an item from the pattern match results
-scrub :: String -> ReplF
-scrub sIndex matches = fmap scrubString $ index sIndex matches
-
-
-{- Construct a numeric string from an English word for a number,
-   with padding
--}
-wordNum :: String -> String -> ReplF
-wordNum sWidth sIndex matches = do
-   value <- index sIndex matches >>= numStrToInt
-   width <- readIntE sWidth
-   return $ printf "%0*d" (width :: Int) (value :: Int)
-
-
--- Get the formatted publication year from the metadata
-year :: ReplF
-year _ = getPubYear
-
-
-{- Commands in the book naming DSL. These commands are used from the
-   rules file notation, in the rule blocks
--}
-
-{- This is used by subjectMatch to construct a subject matcher for a
-   specific rule. Throws an error if no subject matches the pattern.
--}
-subjectMatches :: String -> EN ()
-subjectMatches re = do
-   subjects <- asks $ metaSubjects . gMetadata
-   unless (any isJust $ map (matchRegex (mkRegex re)) subjects) $
-      throwError ""
-
-
-{- This is used by the titlePat command to construct a title pattern
-   matcher for a specific rule
--}
-extractTitle :: String -> EN [String]
-extractTitle re = do
-   md <- asks gMetadata
-
-   let titleMatch =
-         (matchRegex $ mkRegexWithOpts re False True) =<<
-         titleText `fmap` (listToMaybe $ metaTitles md)
-
-   maybe (throwError "") return titleMatch
-
-
-{- This is used by the name command to execute a series of the DSL
-   instructions above to create a new filename
--}
-name :: [String] -> [ReplF] -> EN String
-name matches replacers =
-   fmap concat $ sequence $ map (\a -> a matches) replacers
-
-
--- Try a specific formatter
-tryFormatter :: Formatter -> EN (String, FilePath)
-tryFormatter (Formatter label matchers titlePat nameBuilders) = do
-   sequence_ matchers
-   matches <- titlePat
-   newName <- name matches nameBuilders
-   return (label, sanitizeString newName)
-
-
-{- Try the entire list of formatters, one by one, in order, until one
-   succeeds or none do
--}
-tryFormatting :: (MonadIO m, MonadError String m) =>
-   Globals -> [Formatter] -> FilePath -> m (String, FilePath)
-tryFormatting gs fs oldPath =
-   either throwError return $ runEN gs $
-      foldr mplus
-         (throwError $ printf "%s [ERROR No formatter found]" oldPath) $
-         map tryFormatter fs
-
-
-extractPublisher :: Metadata -> Bool -> String
-extractPublisher _  False = ""
-extractPublisher md True  = maybe "" ('_' :)
-   (foldr mplus Nothing (map maybePub $ metaContributors md))
-
-   where
-      maybePub (Creator (Just "bkp") (Just fa) _ _ ) = Just fa
-      maybePub _                                       = Nothing
-
-
-{- Convert an English month name (with creative ranges and variations)
-   into number form
--}
-monthStrToNumbers :: String -> EN String
-monthStrToNumbers s
-   | isPrefixOf s "January"            = return "01"
-monthStrToNumbers "January-February"   = return "01_02"
-monthStrToNumbers "January/February"   = return "01_02"
-monthStrToNumbers "Jan-Feb"            = return "01_02"
-monthStrToNumbers "Jan/Feb"            = return "01_02"
-monthStrToNumbers s
-   | isPrefixOf s "February"           = return "02"
-monthStrToNumbers s
-   | isPrefixOf s "March"              = return "03"
-monthStrToNumbers "March-April"        = return "03_04"
-monthStrToNumbers "March/April"        = return "03_04"
-monthStrToNumbers "Mar-Apr"            = return "03_04"
-monthStrToNumbers "Mar/Apr"            = return "03_04"
-monthStrToNumbers s
-   | isPrefixOf s "April"              = return "04"
-monthStrToNumbers "April-May"          = return "04_05"
-monthStrToNumbers "April/May"          = return "04_05"
-monthStrToNumbers "Apr-May"            = return "04_05"
-monthStrToNumbers "Apr/May"            = return "04_05"
-monthStrToNumbers "May"                = return "05"
-monthStrToNumbers "May-June"           = return "05_06"
-monthStrToNumbers "May/June"           = return "05_06"
-monthStrToNumbers "May-Jun"            = return "05_06"
-monthStrToNumbers "May/Jun"            = return "05_06"
-monthStrToNumbers s
-   | isPrefixOf s "June"               = return "06"
-monthStrToNumbers "June/July"          = return "06_07"
-monthStrToNumbers "June-July"          = return "06_07"
-monthStrToNumbers "Jun-Jul"            = return "06_07"
-monthStrToNumbers "Jun/Jul"            = return "06_07"
-monthStrToNumbers s
-   | isPrefixOf s "July"               = return "07"
-monthStrToNumbers "July-August"        = return "07_08"
-monthStrToNumbers "July/August"        = return "07_08"
-monthStrToNumbers "Jul-Aug"            = return "07_08"
-monthStrToNumbers "Jul/Aug"            = return "07_08"
-monthStrToNumbers s
-   | isPrefixOf s "August"             = return "08"
-monthStrToNumbers "August-September"   = return "08_09"
-monthStrToNumbers "August/September"   = return "08_09"
-monthStrToNumbers "Aug-Sep"            = return "08_09"
-monthStrToNumbers "Aug/Sep"            = return "08_09"
-monthStrToNumbers s
-   | isPrefixOf s "September"          = return "09"
-monthStrToNumbers "September-October"  = return "09_10"
-monthStrToNumbers "September/October"  = return "09_10"
-monthStrToNumbers "Sep-Oct"            = return "09_10"
-monthStrToNumbers "Sep/Oct"            = return "09_10"
-monthStrToNumbers s
-   | isPrefixOf s "October"            = return "10"
-monthStrToNumbers "October-November"   = return "10_11"
-monthStrToNumbers "October/November"   = return "10_11"
-monthStrToNumbers "Oct-Nov"            = return "10_11"
-monthStrToNumbers "Oct/Nov"            = return "10_11"
-monthStrToNumbers s
-   | isPrefixOf s "November"           = return "11"
-monthStrToNumbers "November-December"  = return "11_12"
-monthStrToNumbers "November/December"  = return "11_12"
-monthStrToNumbers "Nov-Dec"  = return "11_12"
-monthStrToNumbers "Nov/Dec"  = return "11_12"
-monthStrToNumbers s
-   | isPrefixOf s "December"           = return "12"
-monthStrToNumbers s                    =
-   throwError $ printf "[ERROR monthNum %s]" s
-
-
-{- Convert an English word for a number into number form
--}
-numStrToInt :: String -> EN Int
-numStrToInt "One"       = return 1
-numStrToInt "Two"       = return 2
-numStrToInt "Three"     = return 3
-numStrToInt "Four"      = return 4
-numStrToInt "Five"      = return 5
-numStrToInt "Six"       = return 6
-numStrToInt "Seven"     = return 7
-numStrToInt "Eight"     = return 8
-numStrToInt "Nine"      = return 9
-numStrToInt "Ten"       = return 10
-numStrToInt "Eleven"    = return 11
-numStrToInt "Twelve"    = return 12
-numStrToInt "Thirteen"  = return 13
-numStrToInt "Fourteen"  = return 14
-numStrToInt "Fifteen"   = return 15
-numStrToInt "Sixteen"   = return 16
-numStrToInt "Seventeen" = return 17
-numStrToInt "Eighteen"  = return 18
-numStrToInt "Nineteen"  = return 19
-numStrToInt "Twenty"    = return 20
-numStrToInt s           = throwError $ printf "[ERROR wordNum %s]" s
diff --git a/src/EpubTools/EpubName/Format/PubYear.hs b/src/EpubTools/EpubName/Format/PubYear.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Format/PubYear.hs
+++ /dev/null
@@ -1,61 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubName.Format.PubYear
-   ( extractYear
-   , getPubYear
-   )
-   where
-
-import Codec.Epub.Data.Metadata
-import Codec.Epub.Data.Package
-import Control.Monad
-import Text.Regex
-
-import EpubTools.EpubName.Opts
-import EpubTools.EpubName.Format.Util
-
-
-{- Look for publication date, return "" if none found
--}
-getPubYear :: EN String
-getPubYear = do
-   yearHandling <- asks $ optPubYear . gOpts
-   md <- asks gMetadata
-   version <- asks $ pkgVersion . gPackage
-
-   case (version, yearHandling) of
-      ('3' : _ , Publication) -> getPubYear' md $ [getFirstDate]
-      ('3' : _ , AnyDate    ) -> getPubYear' md $ [getFirstDate]
-      ('2' : _ , Publication) -> getPubYear' md pubAttrs
-      ('2' : _ , AnyDate    ) -> getPubYear' md $
-         pubAttrs ++ [getFirstDate]
-      (_       , _          ) -> return ""
-
-   where
-      getPubYear' md = return . maybe "" ('_' :) . foldr mplus Nothing .
-         map (\f -> f (metaDates md))
-
-      pubAttrs =
-         [ getDateWithAttr "original-publication"
-         , getDateWithAttr "publication"
-         ]
-
-
-getDateWithAttr :: String -> [Date] -> Maybe String
-getDateWithAttr attrVal mds = foldr mplus Nothing $ map getPublication' mds
-   where
-      getPublication' (Date (Just av) d)
-         | av == attrVal = extractYear d
-      getPublication' _  = Nothing
-
-
-getFirstDate :: [Date] -> Maybe String
-getFirstDate ((Date _ d) : _) = extractYear d
-getFirstDate _                = Nothing
-
-
-extractYear :: String -> Maybe String
-extractYear s = case matchRegex (mkRegex "(^| )([0-9]{4})") s of
-   Just (_ : y : []) -> Just y
-   _                 -> Nothing
diff --git a/src/EpubTools/EpubName/Format/Util.hs b/src/EpubTools/EpubName/Format/Util.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Format/Util.hs
+++ /dev/null
@@ -1,91 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubName.Format.Util
-   ( Globals (..)
-   , EN , runEN
-   , throwError
-   , asks
-   , scrubString
-   , sanitizeString
-   )
-   where
-
-import Codec.Epub.Data.Metadata
-import Codec.Epub.Data.Package
-import Control.Monad.Except
-import Control.Monad.Identity
-import Control.Monad.Reader
-import Data.Char
-import Data.List ( foldl' )
-import Text.Regex
-
-import EpubTools.EpubName.Opts
-
-
-data Globals = Globals
-   { gOpts :: Options
-   , gPackage :: Package
-   , gMetadata :: Metadata
-   }
-
-type EN a = ReaderT Globals (ExceptT String Identity) a
-
-runEN :: Globals -> EN a -> Either String a
-runEN env ev = runIdentity (runExceptT (runReaderT ev env))
-
-
-{- Convenience function to make a regex replacer for a given pattern 
-   and replacement string. Helps by doing the 'flip' of str and rpl
-   so you can partial eval.
--}
-repl :: String -> String -> String -> String
-repl re rpl str = subRegex (mkRegex re) str rpl
-
-
-{- Transforms a string like this:
-      "the quick brown McCheeseburger" -> "TheQuickBrownMccheeseburger"
--}
-camelCase :: String -> String
-camelCase s = concat $ map capFirst $ words s
-   where
-      capFirst s'@('I':'I':_) = map toUpper s'
-      capFirst s'@('I':'V':_) = map toUpper s'
-      capFirst s'@('I':'X':_) = map toUpper s'
-      capFirst (first:rest) = (toUpper first) : (map toLower rest)
-      capFirst _ = undefined
-
-
-{- A set of common string filters that apply to any and all parts
-   of every single string we process in this project.
--}
-aggressiveFilters :: [(String -> String)]
-aggressiveFilters =
-   [ repl "[',\\?();#’\\*!]"  ""
-   , repl "]"                 ""    -- Tricky to put this in [ ]
-   , repl "[./]"              " "
-   , repl ":"                 "_"
-   , repl "[\\[\\\n]"         "_ "
-   , repl "-"                 "- "
-   , filter (/= '"')
-   -- Decided that I like the article included in titles
-   --, repl "^The "             ""
-   , repl "&"                 " And "
-   , camelCase
-   ]
-
-
-{- Utility function to apply the above filters to a string, giving you
-   back the transformed string
--}
-scrubString :: String -> String
-scrubString s = foldl' (flip id) s aggressiveFilters
-
-
-{- Utility function to apply a very basic filter to make a legal filename,
-   giving you back the transformed string. This is used on everything
-   at the very end, just before the new filename is returned from the
-   formatting machinery.
--}
-sanitizeString :: String -> String
-sanitizeString s = foldl' (flip id) s [ repl "/" " " ]
diff --git a/src/EpubTools/EpubName/Main.hs b/src/EpubTools/EpubName/Main.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Main.hs
+++ /dev/null
@@ -1,65 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{-# LANGUAGE FlexibleContexts #-}
-
-module EpubTools.EpubName.Main
-   ( initialize
-   )
-   where
-
-import Control.Monad.Except
-import System.Directory ( doesFileExist )
-import System.Exit
-import Text.Printf
-
-import qualified EpubTools.EpubName.Doc.Rules as Rules
-import EpubTools.EpubName.Format.Compile
-import EpubTools.EpubName.Format.Format
-import EpubTools.EpubName.Opts
-import EpubTools.EpubName.Util
-
-
-initialize :: (MonadError ExitCode m, MonadIO m) =>
-   Options -> m [Formatter]
-initialize opts = (locateRules $ optRulesPaths opts)
-   >>= (loadFormatters $ optVerbose opts)
-
-
-locateRules :: (MonadError ExitCode m, MonadIO m) =>
-   [FilePath] -> m (String, String)
-locateRules paths = do
-   mbResult <- liftIO $ foldl (liftM2 mplus) (return Nothing)
-      $ map mbExists paths
-   maybe (return ("built-in", Rules.defaults))
-      loadRules mbResult
-
-   where
-      loadRules p = do
-         contents <- liftIO $ readFile p
-         return (p, contents)
-
-
-mbExists :: FilePath -> IO (Maybe FilePath)
-mbExists p = do
-   e <- doesFileExist p
-   if e
-      then return $ Just p
-      else return Nothing
-
-
-loadFormatters :: (MonadError ExitCode m, MonadIO m) =>
-   (Maybe Int) -> (String, String) -> m [Formatter]
-loadFormatters verbosity (name, contents) = case parseRules name contents of
-   Left err  -> do
-      _ <- liftIO $ printf "%s:\n%s" name (show err)
-      throwError exitInitFailure
-   Right fs  -> do
-      liftIO $ showRulesSource verbosity name
-      return fs
-
-
-showRulesSource :: (Monad m, PrintfType (m ()), PrintfArg t, Num a, Eq a) =>
-   Maybe a -> t -> m ()
-showRulesSource (Just 1) name = printf "Rules loaded from: %s\n" name
-showRulesSource _        _    = return ()
diff --git a/src/EpubTools/EpubName/Opts.hs b/src/EpubTools/EpubName/Opts.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Opts.hs
+++ /dev/null
@@ -1,225 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-{-# LANGUAGE FlexibleContexts #-}
-
-module EpubTools.EpubName.Opts
-   ( PubYear (..), Options (..)
-   , defaultOptions
-   , parseOpts, usageText
-   )
-   where
-
-import Control.Monad.Except
-import Data.Maybe
-import Data.Version ( showVersion )
-import Paths_epub_tools ( version )
-import System.Console.GetOpt
-import System.Environment
-import System.Exit
-import System.FilePath
-
-import EpubTools.EpubName.Util
-
-
-data PubYear
-   = Publication  -- Default
-      -- epub3: use first date of any kind
-      -- epub2: use date 'original-publication' or event='publication'
-   | AnyDate      -- Failing above, use any date found
-   | NoDate       -- Don't do publication date at all
-
-
-data Options = Options
-   { optDumpRules    :: Bool
-   , optHelp         :: Bool
-   , optHelpRules    :: Bool
-   , optInteractive  :: Bool
-   , optNoAction     :: Bool
-   , optPublisher    :: Bool
-   , optRulesPaths   :: [FilePath]
-   , optTargetDir    :: FilePath
-   , optVerbose      :: Maybe Int
-   , optPubYear      :: PubYear
-   }
-
-
-defaultRulesFile :: FilePath
-defaultRulesFile = "default.rules"
-
-
-userRulesPath :: IO (Maybe FilePath)
-userRulesPath = foldl (liftM2 mplus) (return Nothing) $ map mkdir
-   [ ("HOME", ".epubtools")      -- UNIX-like
-   , ("APPDATA", "epubtools")    -- Windows
-   ]
-
-   where
-      mkdir (var, subdir) = do
-         homeDir <- lookupEnv var
-         return $ (</> subdir </> defaultRulesFile) `fmap` homeDir
-
-
-defaultOptions :: IO Options
-defaultOptions = do
-   urp <- userRulesPath
-
-   return Options
-      { optDumpRules       = False
-      , optHelp            = False
-      , optHelpRules       = False
-      , optInteractive     = False
-      , optNoAction        = False
-      , optPublisher       = False
-      , optRulesPaths      = maybeToList urp
-      , optTargetDir       = "."
-      , optVerbose         = Nothing
-      , optPubYear         = Publication
-      }
-
-
-options :: [OptDescr (Options -> Options)]
-options =
-   [ Option ['d'] ["any-date"]
-      (NoArg (\opts -> opts { optPubYear = AnyDate } )) 
-      "If no publication year found, use first date element of any kind"
-   , Option ['D'] ["no-date"]
-      (NoArg (\opts -> opts { optPubYear = NoDate } )) 
-      "Suppress inclusion of original publication year"
-   , Option [] ["dump-rules"] 
-      (NoArg (\opts -> opts { optDumpRules = True } ))
-      "Output built-in rules definitions, use to make your own"
-   , Option ['h'] ["help"] 
-      (NoArg (\opts -> opts { optHelp = True } ))
-      "This help text"
-   , Option [] ["help-rules"] 
-      (NoArg (\opts -> opts { optHelpRules = True } ))
-      "Help on the naming rules domain specific language"
-   , Option ['i'] ["interactive"] 
-      (NoArg (\opts -> opts { optInteractive = True } ))
-      "Prompt user interactively"
-   , Option ['n'] ["no-action"]
-      (NoArg (\opts -> opts { optNoAction = True } )) 
-      "Display what would be done, but do nothing"
-   , Option ['p'] ["publisher"]
-      (NoArg (\opts -> opts { optPublisher = True } )) 
-      "Include book publisher if present. See below"
-   , Option ['r'] ["rules"] (ReqArg prependRulesPath "FILE")
-      "Specify a rules file for naming the books"
-   , Option ['t'] ["target-dir"]
-      ( ReqArg
-         (\p opts -> opts { optTargetDir = p } )
-         "DIR")
-      "Target directory for successfully renamed books. Default: ."
-   , Option ['v'] ["verbose"]
-      ( OptArg
-         ((\n opts -> opts { optVerbose = Just (read n)}) . fromMaybe "1")
-         "LEVEL")
-      "Verbosity level: 1, 2"
-   ]
-
-
-prependRulesPath :: FilePath -> Options -> Options
-prependRulesPath p opts = opts { optRulesPaths = newlist }
-   where
-      newlist = p : optRulesPaths opts
-
-
-parseOpts :: (MonadError ExitCode m, MonadIO m) =>
-   [String] -> m (Options, [String])
-parseOpts argv = do
-   dos <- liftIO defaultOptions
-   case getOpt Permute options argv of
-      (o,n,[]  ) -> return (foldl (flip id) dos o, n)
-      (_,_,errs) -> do
-         liftIO $ do
-            ut <- usageText
-            putStrLn $ concat errs ++ ut
-         throwError exitInitFailure
-
-
-usageText :: IO String
-usageText = return $ (usageInfo header options) ++ "\n" ++ footer
-   where
-      header = init $ unlines
-         [ "USAGE: epubname [OPTIONS] FILES"
-         , "Rename epub book files with clear names based on their metadata"
-         , ""
-         , "OPTIONS:"
-         , ""
-         ]
-      footer = init $ unlines
-         [ "VERBOSITY LEVELS:"
-         , ""
-         , "   1 - Output which formatter processed the file"
-         , "   2 - Output the OPF Package and Metadata info"
-         , ""
-         , "EXIT CODES:"
-         , ""
-         , "   0 - success"
-         , "   1 - bad options"
-         , "   2 - failed to process one or more of the files given"
-         , ""
-         , "SIMPLE NAMING:"
-         , ""
-         , "Book names are constructed by examining parts of the OPF Package metadata such as the title, creators, contributors and dates."
-         , ""
-         , "Strings from the OPF metadata fields are stripped of punctuation, CamelCased and stripped of spaces. Resulting file names look like this:"
-         , ""
-         , "For books with a single author:"
-         , "   LastFirst[Middle]-TitleText[_year][_publisher].epub"
-         , ""
-         , "For books with multiple authors:"
-         , "   Last1_Last2[_Last3...]-TitleText[_year][_publisher].epub"
-         , ""
-         , "For books that have no clear authors, such as compilations:"
-         , "   TitleText[_year][_publisher].epub"
-         , ""
-         , "Only creator tags with either a role attribute of 'aut' or no role at all are considered authors. If a file-as attribute is present, this will be the preferred string. If not, the program tries to do some intelligent parsing of the name."
-         , ""
-         , "PUBLICATION DATE:"
-         , ""
-         , "This software will attempt to locate a publication date to use at the end of the filename. This can be suppressed with the --no-date switch. How the date is found depends on the document epub version:"
-         , ""
-         , "epub2: metadata/date elements like the examples below will be looked for in this order:"
-         , "  <dc:date opf:event='original-publication'>2013-09-17</dc:date>"
-         , "  <dc:date opf:event='publication'>2013-09-17T12:00:00Z</dc:date>"
-         , ""
-         , "The --any-date switch will force the use of the first date found in epub2 documents."
-         , ""
-         , "epub3: the single allowed (but optional) metadata/date element will be used and would look like this:"
-         , "    <dc:date>2013-09-17T12:00:00Z</dc:date>"
-         , ""
-         , "My understanding is it doesn't have to be a complete date string, this software will fish the year out of whatever it can find."
-         , ""
-         , "PUBLISHER:"
-         , ""
-         , "I wanted to provide a way to have multiple copies of the same book produced by different publishers and name them sort-of unambiguously. I came up with the idea of expecting a contributor tag with role attribute of 'bkp' (so far, this is fairly normal). And then use a file-as attribute on that tag to contain a small string to be used in the filename. The idea here is short and sweet for the file-as."
-         , ""
-         , "MORE COMPLICATED NAMING: MAGAZINES AND COLLECTIONS"
-         , ""
-         , "Magazines are kind of a sticky problem in that it's often desireable to have edition and/or date info in the filename. There's a lot of chaos out there with titling the epub editions of magazines. The solution in this software is to do some pattern matching on multiple fields in the magazine's metadata combined with custom naming rules for specific magazines."
-         , ""
-         , "This software ships with a set of rules to properly name over 20 magazines and compilations that are commonly purchased by the developer. These default rules can be extended to add new publications, and used as an example."
-         , ""
-         , "Another case is anthologies, which is to say: collections of works by multiple authors but not necessarily in periodical form. There is an anthology rule in the default rules that's set to look for any <dc:subject>anthology</dc:subject> tag in the metadata. In this case, the authors will be ignored and the title along with a publication year will be used (see above re: dc:date)"
-         , ""
-         , "epubname will search the following locations for a rules file, in this order:"
-         , ""
-         , "   Path specified in a --rules switch"
-         , "   $HOME/.epubtools/default.rules  -- or:"
-         , "   %APPDATA%\\epubtools\\default.rules  -- in Windows"
-         , "   Built-in rules, a comprehensive stock set of naming rules"
-         , ""
-         , "Use the built-in rules as a model for a custom file. See --dump-rules above"
-         , ""
-         , "Please see --help-rules for more information on the syntax of the DSL in the rules file."
-         , ""
-         , ""
-         , "For more information on the epub format:"
-         , "   epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm"
-         , "   epub3: http://www.idpf.org/epub/30/spec/epub30-publications.html"
-         , ""
-         , ""
-         , "Version " ++ (showVersion version) ++ "  Dino Morelli <dino@ui3.info>"
-         ]
diff --git a/src/EpubTools/EpubName/Prompt.hs b/src/EpubTools/EpubName/Prompt.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Prompt.hs
+++ /dev/null
@@ -1,48 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubName.Prompt
-   ( PromptResult (..)
-   , prompt
-   , continue
-   )
-   where
-
-import Data.Char ( toLower )
-
-
-data PromptResult
-   = Yes
-   | No
-   | Quit
-   deriving Eq
-
-
-prompt :: IO PromptResult
-prompt = do
-   putStr "Rename this file? [Ynq], or ? for more options: "
-   c <- getChar
-   putStr "\n"
-
-   case (toLower c) of
-      'y'  -> return Yes
-      ' '  -> return Yes
-      '\n' -> return Yes
-      'n'  -> return No
-      'q'  -> return Quit
-      '?'  -> more >> prompt
-      _    -> putStrLn "Unknown choice" >> prompt
-
-
-continue :: PromptResult -> Bool
-continue Quit = False
-continue _    = True
-
-
-more :: IO ()
-more = putStr . unlines $
-   [ "y: rename this file"
-   , "n: don't rename this file"
-   , "q: cancel now, don't rename any files beyond this point"
-   , "<space> or <enter>: accept the default (which is capitalized)"
-   ]
diff --git a/src/EpubTools/EpubName/Util.hs b/src/EpubTools/EpubName/Util.hs
deleted file mode 100644
--- a/src/EpubTools/EpubName/Util.hs
+++ /dev/null
@@ -1,12 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubName.Util
-   where
-
-import System.Exit
-
-
-exitInitFailure, exitProcessingFailure :: ExitCode
-exitInitFailure = ExitFailure 1
-exitProcessingFailure = ExitFailure 128
diff --git a/src/EpubTools/EpubZip/Opts.hs b/src/EpubTools/EpubZip/Opts.hs
deleted file mode 100644
--- a/src/EpubTools/EpubZip/Opts.hs
+++ /dev/null
@@ -1,76 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.EpubZip.Opts
-   ( Options (..)
-   , parseOpts, usageText
-   )
-   where
-
-import Data.Version ( showVersion )
-import Paths_epub_tools ( version )
-import System.Console.GetOpt
-
-
-data Options = Options
-   { optHelp :: Bool
-   , optOverwrite :: Bool
-   }
-
-
-defaultOptions :: Options
-defaultOptions = Options
-   { optHelp = False
-   , optOverwrite = False
-   }
-
-
-options :: [OptDescr (Options -> Options)]
-options =
-   [ Option ['h'] ["help"] 
-      (NoArg (\opts -> opts { optHelp = True } ))
-      "This help text"
-   , Option ['o'] ["overwrite"] 
-      (NoArg (\opts -> opts { optOverwrite = True } ))
-      "Force overwrite if dest file exists"
-   ]
-
-
-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: epubzip [OPTIONS] DESTDIR"
-         , "       epubzip [OPTIONS] DESTDIR/EPUBFILE"
-         , "Construct an epub zip archive from files in the current directory"
-         , ""
-         , "Options:"
-         ]
-      footer = init $ unlines
-         [ "If run with DESTDIR alone, epubzip will try to construct a name from the OPF package data for this book (see epubname). If run with DESTDIR/EPUBFILE, epubzip will use that name for the destination file."
-         , ""
-         , "You may have noticed that there is no epubunzip utility. Truth is, epubs are just zip files and you barely need epubzip either if you have the normal zip/unzip utilities installed. While not as fancy with file naming and leaving out dotfiles, this works for zipping:"
-         , ""
-         , "   $ cd DIR"
-         , "   $ zip -Xr9D ../EPUBFILE mimetype *"
-         , ""
-         , "And for unzipping, it's really just as easy:"
-         , ""
-         , "   $ mkdir TEMPDIR"
-         , "   $ cd TEMPDIR"
-         , "   $ unzip EPUBFILE"
-         , ""
-         , "For more information on the epub format:"
-         , "   epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm"
-         , "   epub3: http://www.idpf.org/epub/30/spec/epub30-publications.html"
-         , ""
-         , ""
-         , "Version " ++ (showVersion version) ++ "  Dino Morelli <dino@ui3.info>"
-         ]
diff --git a/src/EpubTools/epubmeta.hs b/src/EpubTools/epubmeta.hs
deleted file mode 100644
--- a/src/EpubTools/epubmeta.hs
+++ /dev/null
@@ -1,33 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-import Data.Maybe
-import System.Environment ( getArgs )
-import System.Exit
-
-import EpubTools.EpubMeta.Display
-import EpubTools.EpubMeta.Edit
-import EpubTools.EpubMeta.Export
-import EpubTools.EpubMeta.Import
-import EpubTools.EpubMeta.Opts
-import EpubTools.EpubMeta.Util
-
-
-dispatch :: Options -> [FilePath] -> EM ()
-dispatch opts (f:[]) | optEdit opts /= NotEditing = edit opts f
-dispatch opts (f:[]) | isJust . optImport $ opts  = importOpf opts f
-dispatch opts (f:[]) | optExport opts /= NoExport = exportOpf opts f
-dispatch opts (f:[])                              = display opts f
-dispatch _    _                                   = throwError usageText
-
-
-main :: IO ()
-main = do
-   result <- runEM $
-      liftIO getArgs >>= parseOpts >>= uncurry dispatch
-   either exitFail (const (exitWith ExitSuccess)) result
-
-   where
-      exitFail msg = do
-         putStrLn msg
-         exitWith $ ExitFailure 1
diff --git a/src/EpubTools/epubname.hs b/src/EpubTools/epubname.hs
deleted file mode 100644
--- a/src/EpubTools/epubname.hs
+++ /dev/null
@@ -1,164 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
--- This is for GHC 7.8/7.10 compatibility with the
--- Control.Applicative import below
-{-# OPTIONS_GHC -fno-warn-unused-imports #-}
-
-{-# LANGUAGE FlexibleContexts #-}
-
-import Codec.Epub
-import Codec.Epub.Data.Metadata
-import Codec.Epub.Data.Package
-import Control.Applicative
-import Control.Monad
-import Control.Monad.Except
-import System.Directory ( doesDirectoryExist, doesFileExist, renameFile )
-import System.Environment ( getArgs )
-import System.FilePath
-import System.Exit
-import System.IO ( BufferMode ( NoBuffering )
-                 , hSetBuffering, stdin, stdout, stderr 
-                 )
-import Text.Printf
-
-import qualified EpubTools.EpubName.Doc.Dsl as Dsl
-import qualified EpubTools.EpubName.Doc.Rules as Rules
-import EpubTools.EpubName.Format.Format ( Formatter (..), tryFormatting )
-import EpubTools.EpubName.Format.Util
-import EpubTools.EpubName.Main
-import EpubTools.EpubName.Opts ( Options (..), parseOpts, usageText )
-import EpubTools.EpubName.Prompt ( PromptResult (..), prompt, continue )
-import EpubTools.EpubName.Util
-
-
-{- Construct additional verbose output
--}
-formatF :: (String, Package, Metadata) -> String
-formatF (fmtUsed, _, _) = printf "\n   formatter: %s" fmtUsed
-
-formatFM :: (String, Package, Metadata) -> String
-formatFM (fmtUsed, pkg, md) =
-   printf "\n   formatter: %s\n%s\n%s" fmtUsed
-      (format pkg) (format md)
-
-
-{- Format and display output for a book that was processed
--}
-displayResults :: MonadIO m => Options
-   -> FilePath -> FilePath -> String -> Package -> Metadata -> m ()
-displayResults opts oldPath newPath fmtUsed pkg md =
-   liftIO $ printf "%s -> %s%s\n" oldPath newPath
-      (additional (optVerbose opts) (fmtUsed, pkg, md))
-   where
-      additional Nothing  = const ""
-      additional (Just 1) = formatF
-      additional _        = formatFM
-
-
-{- Process an individual epub book file
--}
-processBook :: Options -> [Formatter] -> [FilePath] -> Bool -> Bool
-   -> IO Bool
-
-processBook _    _          []              _     priRes =
-   return priRes
-
-processBook _    _          _               False priRes =
-   return priRes
-
-processBook opts formatters (oldPath:paths) _     priRes = do
-   result <- runExceptT $ do
-      {- Parse the book's metadata
-         The reason for the nested runErrorT block is, if there is
-         failure during parsing, we need to mark the result up with the
-         file path. Failures here will otherwise get lost in the output
-         when multiple books are processed at once.
-      -}
-      epm <- runExceptT $ do
-         xml <- getPkgXmlFromZip oldPath
-         (,) <$> getPackage xml <*> getMetadata xml
-      (pkg, md) <- either
-         ( \msg -> throwError
-            $ printf "ERROR: File %s: %s" oldPath msg
-         ) return epm
-
-      (fmtUsed, shortPath) <-
-         tryFormatting (Globals opts pkg md) formatters oldPath
-
-      let newPath = optTargetDir opts </> shortPath
-
-      fileExists <- liftIO $ doesFileExist newPath
-      when fileExists $ throwError $ 
-         printf "File %s already exists. No change." newPath
-
-      displayResults opts oldPath newPath fmtUsed pkg md
-
-      promptResult <- liftIO $ case (optInteractive opts) of
-         True  -> prompt
-         False -> return Yes
-
-      when ((promptResult == Yes) && (optNoAction opts == False)) $
-         liftIO $ renameFile oldPath newPath
-
-      return $ continue promptResult
-
-   (thisRes, cont) <- either (\errmsg -> do
-      putStrLn errmsg
-      return (False, True)
-      )
-      (\c -> return (True, c))
-      result
-
-   let newRes = case (priRes, thisRes) of
-         (False, _) -> False
-         (_    , r) -> r
-
-   processBook opts formatters paths cont newRes
-
-
-main :: IO ()
-main = do
-   -- No buffering, it messes with the order of output
-   mapM_ (flip hSetBuffering NoBuffering) [ stdout, stderr, stdin ]
-
-   either exitWith exitWith =<< (runExceptT $ do
-      -- Parse command-line arguments
-      (opts, paths) <- (liftIO getArgs) >>= parseOpts
-
-      -- User asked for rules help, this is a special termination case
-      when (optHelpRules opts) $ do
-         liftIO $ putStrLn Dsl.docs
-         throwError ExitSuccess
-
-      -- User asked for a dump of the built-in rules,
-      -- this is a special termination case
-      when (optDumpRules opts) $ do
-         liftIO $ putStr Rules.defaults
-         throwError ExitSuccess
-
-      -- User asked for help, this is a special termination case
-      when ((optHelp opts) || (null paths)) $ do
-         liftIO $ usageText >>= putStrLn
-         throwError ExitSuccess
-
-      let targetDir = optTargetDir opts
-      targetDirExists <- liftIO $ doesDirectoryExist targetDir
-      unless targetDirExists $ do
-         _ <- liftIO $ printf
-            "ERROR: Target directory doesn't exist: %s\n" targetDir
-         throwError exitInitFailure
-
-      -- Locate the rules file, load it and parse into a list of
-      -- formatters
-      fs <- initialize opts
-
-      when (optNoAction opts) $ liftIO
-         $ putStrLn "No-action specified"
-
-      -- Perform the formatting operation on the books
-      code <- liftIO $ processBook opts fs paths True True
-      case code of
-         True  -> return ExitSuccess
-         False -> throwError exitProcessingFailure
-      )
diff --git a/src/EpubTools/epubzip.hs b/src/EpubTools/epubzip.hs
deleted file mode 100644
--- a/src/EpubTools/epubzip.hs
+++ /dev/null
@@ -1,79 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-import Codec.Epub
-import Control.Monad
-import Control.Monad.Except
-import System.Directory
-import System.Environment
-import System.Exit
-import System.FilePath
-import System.IO ( BufferMode ( NoBuffering )
-                 , hSetBuffering, stdout, stderr 
-                 )
-import Text.Printf
-
-import EpubTools.EpubName.Format.Format ( tryFormatting )
-import EpubTools.EpubName.Format.Util
-import EpubTools.EpubName.Main
-import qualified EpubTools.EpubName.Opts as EN
-import EpubTools.EpubZip.Opts
-
-
-main :: IO ()
-main = do
-   -- No buffering, it messes with the order of output
-   mapM_ (flip hSetBuffering NoBuffering) [ stdout, stderr ]
-
-   either exitWith exitWith =<< (runExceptT $ do
-      -- Parse command-line arguments
-      (opts, paths) <- liftIO $ getArgs >>= parseOpts
-
-      -- User asked for help, this is a special termination case
-      when ((optHelp opts) || (null paths)) $ do
-         liftIO $ putStrLn usageText
-         throwError ExitSuccess
-
-      let inputPath = head paths
-      isDir <- liftIO $ doesDirectoryExist inputPath
-
-      en <- if isDir
-            then do
-               dos <- liftIO EN.defaultOptions
-               fs <- initialize dos
-               runExceptT $ do
-                  xml <- snd `fmap` getPkgPathXmlFromDir "."
-                  pkg <- getPackage xml
-                  metadata <- getMetadata xml
-
-                  (_, newPath) <- tryFormatting
-                     (Globals dos pkg metadata) fs
-                     "CURRENT DIRECTORY"
-
-                  return $ inputPath </> newPath
-            else return . Right $ inputPath
-
-      case en of
-         Right zipPath -> do
-            exists <- liftIO $ doesFileExist zipPath
-
-            case ((optOverwrite opts), exists) of
-               (False, True) -> do
-                  _ <- liftIO $ printf
-                     "File %s exists, use --overwrite to force\n"
-                     zipPath
-                  throwError $ ExitFailure 1
-               (True,  True) -> liftIO $ removeFile zipPath
-               (_   ,  _   ) -> return ()
-
-            _ <- liftIO $ do
-               archive <- mkEpubArchive "."
-               writeArchive zipPath archive
-               printf "Book created: %s\n" zipPath
-
-            return ExitSuccess
-
-         Left errorMsg -> do
-            liftIO $ putStrLn errorMsg
-            throwError $ ExitFailure 1
-      )
diff --git a/src/app/EpubTools/EpubMeta/Display.hs b/src/app/EpubTools/EpubMeta/Display.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubMeta/Display.hs
@@ -0,0 +1,26 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubMeta.Display
+   where
+
+import Codec.Epub
+import Control.Monad ( when )
+
+import EpubTools.EpubMeta.Opts
+import EpubTools.EpubMeta.Util
+
+
+display :: Options -> FilePath -> EM ()
+display opts zipPath = do
+   xml <- getPkgXmlFromZip zipPath
+
+   pkg <- format `fmap` getPackage xml
+   meta <- format `fmap` getMetadata xml
+   liftIO $ mapM_ putStr [pkg, meta]
+
+   when (optVerbose opts) $ do
+      mf <- format `fmap` getManifest xml
+      spine <- format `fmap` getSpine xml
+      guide <- format `fmap` getGuide xml
+      liftIO $ mapM_ putStr [mf, spine, guide]
diff --git a/src/app/EpubTools/EpubMeta/Edit.hs b/src/app/EpubTools/EpubMeta/Edit.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubMeta/Edit.hs
@@ -0,0 +1,83 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module EpubTools.EpubMeta.Edit
+   ( edit )
+   where
+
+import Control.Exception
+import Control.Monad
+import System.Directory
+import System.FilePath
+import System.Environment
+import System.Exit
+import System.Process ( system )
+import Text.Printf
+
+import EpubTools.EpubMeta.Export
+import EpubTools.EpubMeta.Import
+import EpubTools.EpubMeta.Opts
+import EpubTools.EpubMeta.Util
+
+
+{- Turn an (IO a) action into a (Maybe a) with a value of Nothing
+   if an IO exception is raised
+-}
+ioMaybe :: IO a -> IO (Maybe a)
+ioMaybe action = do
+   result :: (Either SomeException a) <- try action
+   either (const . return $ Nothing) (return . Just) result
+
+
+{- Look through the user's environment looking for their preferred 
+   editor. Use /usr/bin/vi as a default if none found
+-}
+findEditor :: EM FilePath
+findEditor = do
+   mbEditor <- liftIO $ foldr (liftM2 mplus) (findExecutable "vi")
+      [ ioMaybe $ getEnv "EDITOR"
+      , ioMaybe $ getEnv "VISUAL"
+      ]
+
+   maybe (throwError "epubmeta: ERROR: Could not find a suitable editor in your EDITOR or VISUAL environment variables, and could not find the vi binary. Fix this situation or use epubmeta export/import.") return mbEditor
+
+
+edit :: Options -> FilePath -> EM ()
+edit opts = edit' $ optEdit opts
+
+
+edit' :: Edit -> FilePath -> EM ()
+
+edit' NoBackup zipPath = do
+   -- Make a temp path for the OPF document
+   tempDir <- liftIO $ getTemporaryDirectory
+   let opfPath = tempDir </> "epubmeta-temp-content.opf"
+
+   -- Dump the OPF document to this path
+   exportOpf (defaultOptions { optExport = ToPath opfPath }) zipPath
+
+   -- Open this file in the user's editor and wait for it
+   editor <- findEditor
+   ec <- liftIO $ system $ printf "%s %s" editor opfPath
+
+   case ec of
+      ExitSuccess -> do
+         -- Modify the book with the now-edited OPF data
+         importOpf (defaultOptions { optImport = Just opfPath }) zipPath
+
+         -- Delete the temp file
+         liftIO $ removeFile opfPath
+
+      ExitFailure _ -> do
+         -- Delete the temp file
+         liftIO $ removeFile opfPath
+
+         throwError "epubmeta: ERROR: Something went wrong during editing, your file has not been changed."
+
+edit' (Backup suffix) zipPath = do
+   liftIO $ copyFile zipPath (zipPath ++ suffix)
+   edit' NoBackup zipPath
+
+edit' NotEditing _ = undefined
diff --git a/src/app/EpubTools/EpubMeta/Export.hs b/src/app/EpubTools/EpubMeta/Export.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubMeta/Export.hs
@@ -0,0 +1,35 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubMeta.Export
+   ( exportOpf )
+   where
+
+import Codec.Epub.IO
+import System.FilePath
+
+import EpubTools.EpubMeta.Opts
+import EpubTools.EpubMeta.Util
+
+
+write :: FilePath -> String -> EM ()
+write path contents = liftIO $ do
+   writeFile path contents
+   putStrLn $ "OPF XML saved as: " ++ path
+
+
+exportOpf :: Options -> FilePath -> EM ()
+exportOpf opts = export' (optExport opts)
+
+
+export' :: Export -> FilePath -> EM ()
+
+export' Existing zipPath = do
+   (fullPath, contents) <- getPkgPathXmlFromZip zipPath
+   write (takeFileName fullPath) contents
+
+export' (ToPath path) zipPath = do
+   contents <- getPkgXmlFromZip zipPath
+   write path contents
+
+export' NoExport _ = undefined
diff --git a/src/app/EpubTools/EpubMeta/Import.hs b/src/app/EpubTools/EpubMeta/Import.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubMeta/Import.hs
@@ -0,0 +1,55 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubMeta.Import
+   ( importOpf )
+   where
+
+import Codec.Archive.Zip ( Entry (..), addEntryToArchive, findEntryByPath
+  , readEntry, toArchive )
+import Codec.Epub.IO ( getPkgPathXmlFromBS, writeArchive )
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BL
+import Data.List ( foldl' )
+import Data.Maybe ( fromJust )
+import System.Directory ( removeFile )
+
+import EpubTools.EpubMeta.Opts
+import EpubTools.EpubMeta.Util
+
+
+importOpf :: Options -> FilePath -> EM ()
+importOpf opts zipPath = do
+  -- The modified file we want to insert into the book
+  let pathToNewOpf = fromJust . optImport $ opts
+
+  -- Make new file into a Zip Entry
+  tempEntry <- liftIO $ readEntry [] pathToNewOpf
+
+  -- Read the zip archive in, we'll need it in several steps
+  strictBytes <- liftIO $ BS.readFile zipPath
+
+  -- The path in the book where new file must go
+  (pathToOldFile, _) <- getPkgPathXmlFromBS strictBytes
+
+  -- Adjust the entry's path
+  let newOpfEntry = tempEntry { eRelativePath = pathToOldFile }
+
+  -- Bytes of existing archive already loaded, make an Archive from that
+  let oldArchive = toArchive . BL.fromChunks $ [strictBytes]
+
+  -- Make an entry from the existing mimetype file
+  mimeEntry <- maybe
+    (throwError "Something went wrong: This epub has no mimetype, which shouldn't be possible. Original file unchanged.")
+    return $ findEntryByPath "mimetype" oldArchive
+
+  -- Add the two entries back to the archive in this order to preserve the
+  -- mimetype as the first one
+  let newArchive = foldl' (flip id) oldArchive
+        [addEntryToArchive newOpfEntry, addEntryToArchive mimeEntry]
+
+  -- We're ready to write the new zip archive, delete the old one
+  liftIO $ removeFile zipPath
+
+  -- Write the new archive out
+  liftIO $ writeArchive zipPath newArchive
diff --git a/src/app/EpubTools/EpubMeta/Opts.hs b/src/app/EpubTools/EpubMeta/Opts.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubMeta/Opts.hs
@@ -0,0 +1,111 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubMeta.Opts
+   ( Edit (..)
+   , Export (..)
+   , Options (..), defaultOptions
+   , parseOpts, usageText
+   )
+   where
+
+import Data.Version ( showVersion )
+import Paths_epub_tools ( version )
+import System.Console.GetOpt
+
+import EpubTools.EpubMeta.Util
+
+
+-- There are three states for this, representing with a new type
+data Edit
+   = Backup String
+   | NoBackup
+   | NotEditing
+   deriving Eq
+
+
+maybeToEdit :: Maybe String -> Edit
+maybeToEdit (Just suffix) = Backup suffix
+maybeToEdit Nothing       = NoBackup
+
+
+-- There are three states for this, representing with a new type
+data Export
+   = ToPath String   -- User specified a file to export to
+   | Existing        -- User wants export to use existing name
+   | NoExport        -- This arg wasn't used at all
+   deriving Eq
+
+
+maybeToExport :: Maybe String -> Export
+maybeToExport (Just file) = ToPath file
+maybeToExport Nothing     = Existing
+
+
+data Options = Options
+   { optHelp :: Bool
+   , optEdit :: Edit
+   , optImport :: Maybe String
+   , optExport :: Export
+   , optVerbose :: Bool
+   }
+
+
+defaultOptions :: Options
+defaultOptions = Options
+   { optHelp = False
+   , optEdit = NotEditing
+   , optImport = Nothing
+   , optExport = NoExport
+   , optVerbose = False
+   }
+
+
+options :: [OptDescr (Options -> Options)]
+options =
+   [ Option ['h'] ["help"] 
+      (NoArg (\opts -> opts { optHelp = True } ))
+      "This help text"
+   , Option ['e'] ["edit-opf"]
+      (OptArg (\f opts -> opts { optEdit = maybeToEdit f }) "SUF")
+      "Edit a book's OPF XML data in a text editor. If an optional suffix is supplied, a backup of the book will be created with that suffix"
+   , Option ['x'] ["export"]
+      (OptArg (\f opts -> opts { optExport = maybeToExport f }) "FILE")
+      "Export the book's OPF XML metadata to a file. If no file name is given, the name of the file in the book will be used"
+   , Option ['i'] ["import"]
+      (ReqArg (\f opts -> opts { optImport = Just f }) "FILE")
+      "Import OPF metadata from the supplied XML file"
+   , Option ['v'] ["verbose"]
+      (NoArg (\opts -> opts { optVerbose = True } )) 
+      "Display all OPF package info, including manifest, spine and guide"
+   ]
+
+
+parseOpts :: [String] -> EM (Options, [String])
+parseOpts argv = 
+   case getOpt Permute options argv of
+      (o,n,[]  ) -> return (foldl (flip id) defaultOptions o, n)
+      (_,_,errs) -> throwError $ concat errs ++ usageText
+
+
+usageText :: String
+usageText = (usageInfo header options) ++ "\n" ++ footer
+   where
+      header = init $ unlines
+         [ "Usage: epubmeta [OPTIONS] EPUBFILE"
+         , "View or edit epub book metadata"
+         , ""
+         , "Note that 'edit' here means you get dumped into an editor with the XML document and are on your own."
+         , ""
+         , "Options:"
+         ]
+      footer = init $ unlines
+         [ "The -e feature will look for an editor in this order: the EDITOR environment variable, the VISUAL environment variable, vi"
+         , ""
+         , "For more information on the epub format:"
+         , "   epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm"
+         , "   epub3: http://www.idpf.org/epub/30/spec/epub30-publications.html"
+         , ""
+         , ""
+         , "Version " ++ (showVersion version) ++ "  Dino Morelli <dino@ui3.info>"
+         ]
diff --git a/src/app/EpubTools/EpubMeta/Util.hs b/src/app/EpubTools/EpubMeta/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubMeta/Util.hs
@@ -0,0 +1,17 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubMeta.Util
+   ( EM , runEM
+   , liftIO
+   , throwError
+   )
+   where
+
+import Control.Monad.Except
+
+
+type EM a = (ExceptT String IO) a
+
+runEM :: EM a -> IO (Either String a)
+runEM = runExceptT
diff --git a/src/app/EpubTools/EpubName/Doc/Dsl.hs b/src/app/EpubTools/EpubName/Doc/Dsl.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Doc/Dsl.hs
@@ -0,0 +1,183 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubName.Doc.Dsl
+   ( docs
+   )
+   where
+
+
+docs :: String
+docs = unlines
+   [ "epubname rules documentation"
+   , "----------------------------"
+   , ""
+   , "Book naming is controlled by a set of rules loaded from a file "
+   , ""
+   , "At runtime, a rules file is searched for, first in the user's home directory and then in the installed files for epubname. The paths of these files specific to your system are listed in --help output."
+   , ""
+   , "The rules are checked, from first to last, for each file being renamed. The first rule that succeeds is used."
+   , ""
+   , ""
+   , "Rules have the following format:"
+   , ""
+   , "   label"
+   , "      authorMatch \"...\""
+   , "      subjectMatch \"...\""
+   , "      titlePat \"...\""
+   , "      name \"...\""
+   , ""
+   , "label is to identify what the rule is for to the user and also for verbose output (helpful for debugging rules)"
+   , ""
+   , "The indented fields are rule commands and consist of:"
+   , ""
+   , "   authorMatch - Optional. If present, it's a regexp pattern that must"
+   , "      match an author in the book's metadata. If it doesn't match, this"
+   , "      rule is rejected. This may be a partial match, \"bar\" will match if"
+   , "      any author looks like \"foo bar baz\""
+   , ""
+   , "   subjectMatch - Optional. If present, it's a regexp pattern that must"
+   , "      match a subject in the book's metadata. If it doesn't match, this"
+   , "      rule is rejected. This may be a partial match, \"bar\" will match if"
+   , "      any subject looks like \"foo bar baz\""
+   , ""
+   , "   titlePat - a regular expression for picking apart the title string in"
+   , "      the book's metadata. The pieces in capture groups ( ) are exposed "
+   , "      as numbered matches in the name construction field. If this match"
+   , "      fails, this rule is rejected."
+   , ""
+   , "   name - instructions for building up a new book filename, most often"
+   , "      using the parts extracted in titlePat. More on this below"
+   , ""
+   , ""
+   , "In the name command, there are several functions that can be used. These functions are delimited with parenthesis in the name command's expression."
+   , ""
+   , "   authors - Get formatted authors from the metadata. If there are any"
+   , "      authors, a hyphen will be placed after them in the resulting string."
+   , ""
+   , "      syntax: (authors)"
+   , ""
+   , "   INT - Retrieve the text of a specific numbered item from the title"
+   , "      pattern match results"
+   , ""
+   , "      syntax: (INT)"
+   , ""
+   , "   monthNum INDEX - Construct a numeric month from a variety of English"
+   , "      months strings. Including abbreviations and ranges"
+   , ""
+   , "      syntax: (monthNum INT)"
+   , ""
+   , "      examples:"
+   , "         \"January\"           -> \"01\""
+   , "         \"Feb\"               -> \"02\""
+   , "         \"Mar-Apr\"           -> \"03_04\""
+   , "         \"September/October\" -> \"09_10\""
+   , ""
+   , "   pad WIDTH INDEX - Pad a numeric value from the pattern match results"
+   , ""
+   , "      syntax: (pad INT INT)"
+   , ""
+   , "      examples:"
+   , "         2 \"3\"    -> \"03\""
+   , "         3 \"10\"   -> \"010\""
+   , ""
+   , "   publisher - Get the formatted publisher file-as string from the"
+   , "      metadata"
+   , ""
+   , "      syntax: (publisher)"
+   , ""
+   , "   scrub INDEX - Clean up an item from the pattern match results. A"
+   , "      large number of punctuation is removed and/or converted into"
+   , "      other characters, and the string is CamelCased. In the end, only"
+   , "      alphanumerics, - and _ are used."
+   , ""
+   , "      syntax: (scrub INT)"
+   , ""
+   , "   wordNum WIDTH INDEX - Construct a numeric string from an English word"
+   , "      for a number, with padding. Works on strings from \"One\" to \"Twenty\""
+   , ""
+   , "      syntax: (wordNum INT INT)"
+   , ""
+   , "      example:"
+   , "         2 \"One\"    -> \"01\""
+   , "         3 \"Twenty\" -> \"020\""
+   , ""
+   , "   year - Get the formatted publication year from the metadata"
+   , ""
+   , "      syntax: (year)"
+   , ""
+   , "   Anything else, not in parenthesis, is a string literal to be used at"
+   , "   that position in the resulting string."
+   , ""
+   , ""
+   , "This probably seems confusing, some examples may help."
+   , ""
+   , "This one is a fictitious magazine with issue and date info in the title:"
+   , ""
+   , "   original book metadata:"
+   , "      title: Adventurer's Weekly Issue 53, May 2008"
+   , ""
+   , "   epubname rule:"
+   , "      magAdventureWeekly"
+   , "         titlePat \"(Adventurer's Weekly) Issue ([0-9]+), ([^ ]+) ([0-9]{4})$\""
+   , "         name \"(scrub 1)Magazine(pad 3 2)_(4)-(monthNum 3)\""
+   , ""
+   , "   resulting title:"
+   , "      \"AdventurersWeeklyMagazine053_2008-05\""
+   , ""
+   , ""
+   , "Here's one using some more functions:"
+   , ""
+   , "   original book metadata:"
+   , "      title: Thrilling Journeys Three"
+   , "      creator"
+   , "         role: aut"
+   , "         text: The Thrilling Authors"
+   , ""
+   , "   epubname rule:"
+   , "      compThrillingJourneys"
+   , "         author \"Thrilling Authors\""
+   , "         titlePat \"(Thrilling Journeys) (.*)\""
+   , "         name \"(scrub 1)(wordNum 2 2)\""
+   , ""
+   , "   resulting title:"
+   , "      \"ThrillingJourneys03\""
+   , ""
+   , ""
+   , "And here's what happens when an ordinary (non-periodical) book is formatted:"
+   , ""
+   , "   original book metadata:"
+   , "      title: Springtime in the Village"
+   , "      creator"
+   , "         role: aut"
+   , "         file-as: Dandridge, Suzanne"
+   , "         text: Suzanne Dandridge"
+   , "      date"
+   , "         event: publication"
+   , "         text: 2012-10-09"
+   , ""
+   , "   epubname rule:"
+   , "      ordinary_book"
+   , "         titlePat \"(.*)\""
+   , "         name \"(authors)(scrub 1)(year)\""
+   , ""
+   , "   resulting title:"
+   , "      \"DandridgeSuzanne-SpringtimeInTheVillage_2012\""
+   , ""
+   , ""
+   , "The last rule in the built-in rules, ordinary_book is very important, and looks like this:"
+   , ""
+   , "   ordinary_book"
+   , "      titlePat \"(.*)\""
+   , "      name \"(authors)(scrub 1)(year)\""
+   , ""
+   , "This rule will match any book and is used to format all simple author-title situations. This rules is responsible for the basic epubname behavior and should always be present and always be last."
+   , ""
+   , ""
+   , "Preceding a rule label with ! will \"switch off\" the entire rule. It's a simple way to comment out a single rule in its entirety. example:"
+   , ""
+   , "   !compThrillingJourneys"
+   , "      author \"Thrilling Authors\""
+   , "      titlePat \"(Thrilling Journeys) (.*)\""
+   , "      name \"(scrub 1)(wordNum 2 2)\""
+   ]
diff --git a/src/app/EpubTools/EpubName/Doc/Rules.hs b/src/app/EpubTools/EpubName/Doc/Rules.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Doc/Rules.hs
@@ -0,0 +1,137 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubName.Doc.Rules
+   ( defaults
+   )
+   where
+
+
+defaults :: String
+defaults = unlines
+   [ "magAeon"
+   , "   titlePat \"^A[eE]on ([^ ]+)$\""
+   , "   name \"AeonMagazine(wordNum 2 1)\""
+   , ""
+   , "magApex"
+   , "   titlePat \"^(Apex)[^0-9]*([0-9]{1,3}).*\""
+   , "   name \"(scrub 1)Magazine(pad 3 2)\""
+   , ""
+   , "magAnalog"
+   , "   titlePat \"^(A[^ ]*).*, ([^ ]+) ([0-9]{4})$\""
+   , "   name \"(scrub 1)SF(3)-(monthNum 2)\""
+   , ""
+   , "magBcs"
+   , "   titlePat \"(Beneath Ceaseless.*) #([0-9]+).*\""
+   , "   name \"(scrub 1)_Issue(pad 3 2)\""
+   , ""
+   , "magBlackStatic"
+   , "   titlePat \"^(Black Static Horror Magazine)[^0-9]*([0-9]+)$\""
+   , "   name \"(scrub 1)(pad 2 2)\""
+   , ""
+   , "magChallengingDestiny"
+   , "   titlePat \"^(Challenging Destiny) #([0-9]+).*\""
+   , "   name \"(scrub 1)Magazine(pad 3 2)\""
+   , ""
+   , "magClarkesworld"
+   , "   titlePat \"^(Clarkesworld)[^0-9]*([0-9]+)$\""
+   , "   name \"(1)(pad 3 2)\""
+   , ""
+   , "magFantasyMag"
+   , "   titlePat \"^(Fantasy Magazine)[^0-9]+([0-9]+).*\""
+   , "   name \"(scrub 1)(pad 3 2)\""
+   , ""
+   , "magEclipse_word"
+   , "   titlePat \"^(Eclipse) +([a-zA-Z]+)$\""
+   , "   name \"(1)(wordNum 2 2)\""
+   , ""
+   , "magEclipse_num"
+   , "   titlePat \"^(Eclipse) +([0-9]+):.*\""
+   , "   name \"(1)(pad 2 2)\""
+   , ""
+   , "magFsf"
+   , "   authorMatch \"Spilogale\""
+   , "   titlePat \".* ([^ ]+) ([0-9]{4})$\""
+   , "   name \"FantasyScienceFiction(2)-(monthNum 1)\""
+   , ""
+   , "magFutureOrbits"
+   , "   titlePat \"(Future Orbits) Issue ([0-9]+), ([^ ]+) ([0-9]{4})$\""
+   , "   name \"(scrub 1)Magazine(pad 2 2)_(4)-(monthNum 3)\""
+   , ""
+   , "magGud"
+   , "   authorMatch \"GUD Magazine Authors\""
+   , "   titlePat \".* Magazine Issue ([0-9]+) ::.*\""
+   , "   name \"GUDMagazine(pad 2 1)\""
+   , ""
+   , "magInterzone"
+   , "   titlePat \"^(Interzone|INTERZONE)[^0-9]+([0-9]+).*\""
+   , "   name \"(scrub 1)SFF(pad 3 2)\""
+   , ""
+   , "magLightspeed_date"
+   , "   titlePat \"(Lightspeed) Magazine, (.*) (.*)\""
+   , "   name \"(1)(3)-(monthNum 2)\""
+   , ""
+   , "magLightspeed_issue"
+   , "   titlePat \"(Lightspeed).* Issue ([^ ]+)(.*)\""
+   , "   name \"(1)(pad 3 2)\""
+   , ""
+   , "magLunaStationQuarterly"
+   , "   titlePat \"(Luna Station Quarterly) - Issue ([0-9]+)\""
+   , "   name \"(scrub 1)(pad 3 2)\""
+   , ""
+   , "magNemesis"
+   , "   titlePat \"(Nemesis Mag)azine #([0-9]+).*\""
+   , "   name \"(scrub 1)(pad 3 2)\""
+   , ""
+   , "magRageMachine"
+   , "   titlePat \"(Rage Machine.*)--([^ ]+) ([0-9]{4})$\""
+   , "   name \"(scrub 1)_(3)-(monthNum 2)\""
+   , ""
+   , "magSomethingWicked"
+   , "   titlePat \"^(Something Wicked)[^0-9]*([0-9]+)\""
+   , "   name \"(scrub 1)(pad 3 2)\""
+   , ""
+   , "magWeirdTales"
+   , "   titlePat \"^(Weird Tales)[^0-9]*([0-9]+)$\""
+   , "   name \"(scrub 1)(pad 3 2)\""
+   , ""
+   , "magLocus"
+   , "   titlePat \"^(Locus), ([a-zA-Z]+) ([0-9]{4})$\""
+   , "   name \"(scrub 1)Magazine(3)-(monthNum 2)\""
+   , ""
+   , "magGenericWithIssue"
+   , "   titlePat \"(.* Magazine):? Issue ([0-9]+).*\""
+   , "   name \"(scrub 1)(pad 2 2)\""
+   , ""
+   , "magGenericSubjWithIssue"
+   , "   subjectMatch \"magazine\""
+   , "   titlePat \"(.*) Issue ([0-9]+).*\""
+   , "   name \"(scrub 1)Magazine(pad 2 2)\""
+   , ""
+   , "magGenericVolNo3"
+   , "   titlePat \"(.*)Vol(ume)? ([0-9]{1,2}),? (No|Num)[^0-9]+([0-9]{3}).*\""
+   , "   name \"(scrub 1)Vol(pad 2 3)No(pad 3 5)\""
+   , ""
+   , "magGenericVolNo2"
+   , "   titlePat \"(.*)Vol(ume)? ([0-9]{1,2}),? (No|Num)[^0-9]+([0-9]{1,2})[^0-9]?.*\""
+   , "   name \"(scrub 1)Vol(pad 2 3)No(pad 2 5)\""
+   , ""
+   , "nonficWomensInstituteLibrary"
+   , "   authorMatch \"Woman's Institute\""
+   , "   titlePat \"(.*)\""
+   , "   name \"(scrub 1)\""
+   , ""
+   , "anthology_date"
+   , "   subjectMatch \"anthology\""
+   , "   titlePat \"(.*) - ([^ ]+) ([0-9]{4})\""
+   , "   name \"(scrub 1)(3)-(monthNum 2)\""
+   , ""
+   , "anthology"
+   , "   subjectMatch \"anthology\""
+   , "   titlePat \"(.*)\""
+   , "   name \"(scrub 1)(year)\""
+   , ""
+   , "ordinary_book"
+   , "   titlePat \"(.*)\""
+   , "   name \"(authors)(scrub 1)(year)\""
+   ]
diff --git a/src/app/EpubTools/EpubName/Format/Author.hs b/src/app/EpubTools/EpubName/Format/Author.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Format/Author.hs
@@ -0,0 +1,120 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubName.Format.Author
+   ( extractAuthors
+   , authorMatches
+   )
+   where
+
+import Codec.Epub.Data.Metadata
+import Control.Monad
+import Data.List ( intercalate )
+import Data.Maybe ( isJust )
+import Prelude hiding ( last )
+import Text.Printf
+import Text.Regex
+
+import EpubTools.EpubName.Format.Util
+
+
+extractAuthors :: EN String
+extractAuthors = do
+   fmap (fmtAuthor . separateCombined . justAuthors) $ asks gMetadata
+   where
+      fmtAuthor []  = ""
+      fmtAuthor [c] = formatSingleAuthor c
+      fmtAuthor cs = (formatMultiAuthors cs) ++ "-"
+
+
+separateCombined :: [Creator] -> [Creator]
+separateCombined = concatMap separateOne
+
+
+separateOne :: Creator -> [Creator]
+separateOne c@(Creator r _ _ di) =
+   case (splitRegex (mkRegex " (&|and) ") di) of
+      -- If there was only one, leave the file-as alone!
+      [_] -> [c]
+      -- Otherwise, explode them into separate CreatorS
+      ss  -> map (Creator r Nothing Nothing) ss
+
+
+formatSingleAuthor :: Creator -> String
+
+formatSingleAuthor (Creator _ (Just fa) _ di) = 
+   if ((fa == di) && all (/= ',') fa)
+      then formatSingleAuthor $ Creator Nothing Nothing Nothing di
+      else authorSingle [fa]
+
+formatSingleAuthor (Creator _ _         _ di) = 
+   authorSingle . reverse . nameParts $ di
+
+
+nameParts :: String -> [String]
+nameParts s = maybe [] id $ foldl mplus Nothing matches
+   where
+      matches = map ($ s)
+         [ matchRegex (mkRegex "(.*), +([^ ]+)$") >=> return . reverse
+         , matchRegex (mkRegex "(.*) +([^ ]+)$")
+         , matchRegex (mkRegex "(.*)")
+         ]
+
+
+lastName' :: String -> String
+lastName' s = maybe "" head $ foldl mplus Nothing matches
+   where
+      matches = map ($ s)
+         [ matchRegex (mkRegex "(.*),.*")
+         , matchRegex (mkRegex ".* (.*)")
+         , matchRegex (mkRegex "(.*)")
+         ]
+
+lastName :: Creator -> String
+lastName (Creator _ (Just fa) _ _ ) = lastName' fa
+lastName (Creator _ _         _ di) = lastName' di
+
+
+formatMultiAuthors :: [Creator] -> String
+formatMultiAuthors = (intercalate "_") . (map lastName)
+
+
+justAuthors :: Metadata -> [Creator]
+justAuthors = (filter isAut) . metaCreators
+   where
+      isAut (Creator (Just "aut") _ _ _) = True
+      isAut (Creator Nothing      _ _ _) = True
+      isAut _                            = False
+
+
+{- A common simple formatter for many book authors
+-}
+authorSingle :: [String] -> String
+authorSingle (last:rest:_) =
+   printf "%s%s-" (scrubString last) (scrubString rest)
+authorSingle [n]           =
+   printf "%s-" $ scrubString $ takeWhile (/= '(') n
+authorSingle _             = undefined
+
+
+{- Throws an error if no author matches the pattern
+-}
+authorMatches :: String -> EN ()
+authorMatches re = do
+   let authorMatches' (Creator _ _ _ di) =
+         matchRegex (mkRegex re) di
+
+   md <- asks gMetadata
+   unless (any isJust $ map authorMatches' $ justAuthors md) $ throwError ""
+
+
+{- Author names with a postfix like II, III, Jr. or Sr.
+   FIXME: Is anyone calling this?
+-}
+{-
+authorPostfix :: [String] -> String
+authorPostfix (rest:last:postfix:_) =
+   printf "%s%s%s-" (filterCommon last) (filterCommon rest)
+      (filterCommon postfix)
+authorPostfix _             = undefined
+-}
diff --git a/src/app/EpubTools/EpubName/Format/Compile.hs b/src/app/EpubTools/EpubName/Format/Compile.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Format/Compile.hs
@@ -0,0 +1,174 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}
+
+module EpubTools.EpubName.Format.Compile
+   where
+
+import Data.Maybe ( catMaybes )
+import Text.ParserCombinators.Parsec
+
+import EpubTools.EpubName.Format.Author
+import EpubTools.EpubName.Format.Format
+
+
+parseRules :: SourceName -> String -> Either ParseError [Formatter]
+parseRules name = runParser pFormatters () name
+
+
+pFormatters :: Parser [Formatter]
+pFormatters = fmap catMaybes $ many pFormatter
+
+
+pFormatter :: Parser (Maybe Formatter)
+pFormatter = do
+   l <- many1Till (noneOf " ") eol
+   a <- optionMaybe $ try $ pCommand "authorMatch"
+   s <- optionMaybe $ try $ pCommand "subjectMatch"
+   t <- pCommand "titlePat"
+   n <- pName
+   newline <|> return ' '
+   return $ case head l of
+      '!' -> Nothing
+      _   -> Just $ Formatter l
+         [ maybe (return ()) authorMatches a
+         , maybe (return ()) subjectMatches s
+         ]
+         (extractTitle t) n
+
+   
+pName :: Parser [ReplF]
+pName = do
+   pCommandPrefix "name"
+   fs <- pRepls
+   eol
+   return fs
+
+
+pCommand :: String -> Parser String
+pCommand c = do
+   pCommandPrefix c
+   char '"'
+   qs <- manyTill (noneOf "\"") (char '"')
+   eol
+   return qs
+
+
+pCommandPrefix :: String -> Parser ()
+pCommandPrefix c = do
+   many1 space
+   string c
+   spaces
+   return ()
+
+
+{-
+pRepls :: Parser [[String]]
+pRepls = do
+   char '"'
+   rs <- many pExpr
+   char '"'
+   return rs
+-}
+pRepls :: Parser [ReplF]
+pRepls = do
+   char '"'
+   rs <- many pExpr
+   char '"'
+
+   let extension = literal ".epub"
+   return $ rs ++ [publisher, extension]
+
+
+{-
+pExpr :: Parser [String]
+pExpr = pRepl <|> pLiteral
+-}
+pExpr :: Parser ReplF
+pExpr = pRepl <|> pLiteral
+
+
+--pRepl :: Parser [String]
+pRepl :: Parser ReplF
+pRepl = do
+   char '('
+   spaces
+   x <- pCall <|> pIndex
+   spaces
+   char ')'
+   return x
+
+
+{-
+pCall :: Parser [String]
+pCall = do
+   fn <- pFName
+   spaces
+   args <- pArgList
+   return $ fn : args
+-}
+pCall :: Parser ReplF
+pCall = do
+   fn <- pFName
+   spaces
+   args <- pArgList
+   mkReplF $ fn : args
+
+
+pFName :: Parser String
+pFName = do
+   first <- letter
+   rest <- many alphaNum
+   return $ first : rest
+
+
+pArgList :: Parser [String]
+pArgList = sepEndBy (many1 digit) (char ' ')
+
+
+{-
+pIndex :: Parser [String]
+pIndex = do
+   ds <- many1 digit
+   return [ds]
+-}
+pIndex :: Parser ReplF
+pIndex = do
+   ds <- many1 digit
+   mkReplF ["index", ds]
+
+
+{-
+pLiteral :: Parser [String]
+pLiteral = do
+   l <- many1 (noneOf "()\"")
+   return [l]
+-}
+pLiteral :: Parser ReplF
+pLiteral = do
+   l <- many1 (noneOf "()\"")
+   mkReplF ["literal", l]
+
+
+many1Till :: Parser a -> Parser end -> Parser [a]
+many1Till p end = do
+   h <- p
+   t <- manyTill p end
+   return $ h : t
+
+
+eol :: Parser Char
+eol = newline <|> (eof >> return '\n')
+
+
+mkReplF :: [String] -> Parser ReplF
+mkReplF ("authors" : []) = return authors
+mkReplF ("index" : i : []) = return $ index i
+mkReplF ("literal" : s : []) = return $ literal s
+mkReplF ("monthNum" : i : []) = return $ monthNum i
+mkReplF ("pad" : w : i : []) = return $ pad w i
+mkReplF ("scrub" : i : []) = return $ scrub i
+mkReplF ("wordNum" : w : i : []) = return $ wordNum w i
+mkReplF ("year" : []) = return year
+mkReplF l = unexpected $ "Bad function definition: " ++ (concat l)
diff --git a/src/app/EpubTools/EpubName/Format/Format.hs b/src/app/EpubTools/EpubName/Format/Format.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Format/Format.hs
@@ -0,0 +1,300 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+module EpubTools.EpubName.Format.Format
+   ( ReplF
+   , authors, index, literal, monthNum, pad, publisher, scrub, wordNum
+   , year
+   , Formatter (..)
+   , tryFormatting
+   , subjectMatches
+   , extractTitle
+   )
+   where
+
+import Codec.Epub.Data.Metadata
+import Control.Monad.Except
+import Data.List ( isPrefixOf )
+import Data.Maybe ( isJust, listToMaybe )
+import Text.Printf
+import Text.Regex
+
+import EpubTools.EpubName.Format.Author
+import EpubTools.EpubName.Format.PubYear
+import EpubTools.EpubName.Format.Util
+import EpubTools.EpubName.Opts
+
+
+type ReplF = [String] -> EN String
+
+
+data Formatter = Formatter
+   String         -- formatter label
+   [EN ()]        -- field pattern matchers
+   (EN [String])  -- title pattern parser
+   [ReplF]        -- name building actions
+
+
+{- A single, simple formatter that should handle most books with one or
+   more author.
+-}
+{- Not using this at the moment, but I'm reluctant to remove it. Could
+   be useful someday.
+ordinaryBookFormatter :: Formatter
+ordinaryBookFormatter = Formatter
+   "ordinary_book"
+   (return ())
+   (extractTitle ".*")
+   [authors, scrub "1", year]
+-}
+
+
+{- Wrapper functions to perform some basic operations in the Except
+   String monad
+-}
+
+-- general purpose read in the Except String monad
+readE :: (MonadError String m, Read a) => String -> String -> m a
+readE msg s = case reads s of
+   [(x, "")] -> return x
+   _         -> throwError msg
+
+
+-- read an Int in the Except String monad
+readIntE :: MonadError String m => String -> m Int
+readIntE = readE "Not a number"
+
+
+-- get list element at a specific index in the Except String monad
+elemE :: MonadError String m => [a] -> Int -> m a
+elemE l i
+   | i < length l = return $ l !! i
+   | otherwise    = throwError $ "Bad array index: " ++ show (i + 1)
+
+
+{- Instructions in the book naming DSL. These instructions are used from
+   the rules file notation, in the name "..." command
+-}
+
+-- Get formatted authors from the metadata
+authors :: ReplF
+authors _ = extractAuthors
+
+
+-- Retrieve a specific item from the title pattern match results
+index :: String -> ReplF
+index sIndex matches = readIntE sIndex >>= return . subtract 1
+   >>= elemE matches
+
+
+-- Insert a specific string literal into the resulting name string
+literal :: String -> ReplF
+literal s = return . const s
+
+
+{- Construct a numeric month from a variety of English months strings
+   Including abbreviations and ranges
+-}
+monthNum :: String -> ReplF
+monthNum sIndex matches = index sIndex matches >>= monthStrToNumbers
+
+
+-- Pad a numeric value from the pattern match results
+pad :: String -> String -> ReplF
+pad sWidth sIndex matches = do
+   width <- readIntE sWidth
+   value <- index sIndex matches >>= readIntE
+   return $ printf "%0*d" (width :: Int) (value :: Int)
+
+
+-- Get the formatted publisher file-as string from the metadata
+publisher :: ReplF
+publisher _ = do
+   md <- asks gMetadata
+   fmap (extractPublisher md) $ asks $ optPublisher . gOpts
+
+
+-- Clean up an item from the pattern match results
+scrub :: String -> ReplF
+scrub sIndex matches = fmap scrubString $ index sIndex matches
+
+
+{- Construct a numeric string from an English word for a number,
+   with padding
+-}
+wordNum :: String -> String -> ReplF
+wordNum sWidth sIndex matches = do
+   value <- index sIndex matches >>= numStrToInt
+   width <- readIntE sWidth
+   return $ printf "%0*d" (width :: Int) (value :: Int)
+
+
+-- Get the formatted publication year from the metadata
+year :: ReplF
+year _ = getPubYear
+
+
+{- Commands in the book naming DSL. These commands are used from the
+   rules file notation, in the rule blocks
+-}
+
+{- This is used by subjectMatch to construct a subject matcher for a
+   specific rule. Throws an error if no subject matches the pattern.
+-}
+subjectMatches :: String -> EN ()
+subjectMatches re = do
+   subjects <- asks $ metaSubjects . gMetadata
+   unless (any isJust $ map (matchRegex (mkRegex re)) subjects) $
+      throwError ""
+
+
+{- This is used by the titlePat command to construct a title pattern
+   matcher for a specific rule
+-}
+extractTitle :: String -> EN [String]
+extractTitle re = do
+   md <- asks gMetadata
+
+   let titleMatch =
+         (matchRegex $ mkRegexWithOpts re False True) =<<
+         titleText `fmap` (listToMaybe $ metaTitles md)
+
+   maybe (throwError "") return titleMatch
+
+
+{- This is used by the name command to execute a series of the DSL
+   instructions above to create a new filename
+-}
+name :: [String] -> [ReplF] -> EN String
+name matches replacers =
+   fmap concat $ sequence $ map (\a -> a matches) replacers
+
+
+-- Try a specific formatter
+tryFormatter :: Formatter -> EN (String, FilePath)
+tryFormatter (Formatter label matchers titlePat nameBuilders) = do
+   sequence_ matchers
+   matches <- titlePat
+   newName <- name matches nameBuilders
+   return (label, sanitizeString newName)
+
+
+{- Try the entire list of formatters, one by one, in order, until one
+   succeeds or none do
+-}
+tryFormatting :: (MonadIO m, MonadError String m) =>
+   Globals -> [Formatter] -> FilePath -> m (String, FilePath)
+tryFormatting gs fs oldPath =
+   either throwError return $ runEN gs $
+      foldr mplus
+         (throwError $ printf "%s [ERROR No formatter found]" oldPath) $
+         map tryFormatter fs
+
+
+extractPublisher :: Metadata -> Bool -> String
+extractPublisher _  False = ""
+extractPublisher md True  = maybe "" ('_' :)
+   (foldr mplus Nothing (map maybePub $ metaContributors md))
+
+   where
+      maybePub (Creator (Just "bkp") (Just fa) _ _ ) = Just fa
+      maybePub _                                       = Nothing
+
+
+{- Convert an English month name (with creative ranges and variations)
+   into number form
+-}
+monthStrToNumbers :: String -> EN String
+monthStrToNumbers s
+   | isPrefixOf s "January"            = return "01"
+monthStrToNumbers "January-February"   = return "01_02"
+monthStrToNumbers "January/February"   = return "01_02"
+monthStrToNumbers "Jan-Feb"            = return "01_02"
+monthStrToNumbers "Jan/Feb"            = return "01_02"
+monthStrToNumbers s
+   | isPrefixOf s "February"           = return "02"
+monthStrToNumbers s
+   | isPrefixOf s "March"              = return "03"
+monthStrToNumbers "March-April"        = return "03_04"
+monthStrToNumbers "March/April"        = return "03_04"
+monthStrToNumbers "Mar-Apr"            = return "03_04"
+monthStrToNumbers "Mar/Apr"            = return "03_04"
+monthStrToNumbers s
+   | isPrefixOf s "April"              = return "04"
+monthStrToNumbers "April-May"          = return "04_05"
+monthStrToNumbers "April/May"          = return "04_05"
+monthStrToNumbers "Apr-May"            = return "04_05"
+monthStrToNumbers "Apr/May"            = return "04_05"
+monthStrToNumbers "May"                = return "05"
+monthStrToNumbers "May-June"           = return "05_06"
+monthStrToNumbers "May/June"           = return "05_06"
+monthStrToNumbers "May-Jun"            = return "05_06"
+monthStrToNumbers "May/Jun"            = return "05_06"
+monthStrToNumbers s
+   | isPrefixOf s "June"               = return "06"
+monthStrToNumbers "June/July"          = return "06_07"
+monthStrToNumbers "June-July"          = return "06_07"
+monthStrToNumbers "Jun-Jul"            = return "06_07"
+monthStrToNumbers "Jun/Jul"            = return "06_07"
+monthStrToNumbers s
+   | isPrefixOf s "July"               = return "07"
+monthStrToNumbers "July-August"        = return "07_08"
+monthStrToNumbers "July/August"        = return "07_08"
+monthStrToNumbers "Jul-Aug"            = return "07_08"
+monthStrToNumbers "Jul/Aug"            = return "07_08"
+monthStrToNumbers s
+   | isPrefixOf s "August"             = return "08"
+monthStrToNumbers "August-September"   = return "08_09"
+monthStrToNumbers "August/September"   = return "08_09"
+monthStrToNumbers "Aug-Sep"            = return "08_09"
+monthStrToNumbers "Aug/Sep"            = return "08_09"
+monthStrToNumbers s
+   | isPrefixOf s "September"          = return "09"
+monthStrToNumbers "September-October"  = return "09_10"
+monthStrToNumbers "September/October"  = return "09_10"
+monthStrToNumbers "Sep-Oct"            = return "09_10"
+monthStrToNumbers "Sep/Oct"            = return "09_10"
+monthStrToNumbers s
+   | isPrefixOf s "October"            = return "10"
+monthStrToNumbers "October-November"   = return "10_11"
+monthStrToNumbers "October/November"   = return "10_11"
+monthStrToNumbers "Oct-Nov"            = return "10_11"
+monthStrToNumbers "Oct/Nov"            = return "10_11"
+monthStrToNumbers s
+   | isPrefixOf s "November"           = return "11"
+monthStrToNumbers "November-December"  = return "11_12"
+monthStrToNumbers "November/December"  = return "11_12"
+monthStrToNumbers "Nov-Dec"  = return "11_12"
+monthStrToNumbers "Nov/Dec"  = return "11_12"
+monthStrToNumbers s
+   | isPrefixOf s "December"           = return "12"
+monthStrToNumbers s                    =
+   throwError $ printf "[ERROR monthNum %s]" s
+
+
+{- Convert an English word for a number into number form
+-}
+numStrToInt :: String -> EN Int
+numStrToInt "One"       = return 1
+numStrToInt "Two"       = return 2
+numStrToInt "Three"     = return 3
+numStrToInt "Four"      = return 4
+numStrToInt "Five"      = return 5
+numStrToInt "Six"       = return 6
+numStrToInt "Seven"     = return 7
+numStrToInt "Eight"     = return 8
+numStrToInt "Nine"      = return 9
+numStrToInt "Ten"       = return 10
+numStrToInt "Eleven"    = return 11
+numStrToInt "Twelve"    = return 12
+numStrToInt "Thirteen"  = return 13
+numStrToInt "Fourteen"  = return 14
+numStrToInt "Fifteen"   = return 15
+numStrToInt "Sixteen"   = return 16
+numStrToInt "Seventeen" = return 17
+numStrToInt "Eighteen"  = return 18
+numStrToInt "Nineteen"  = return 19
+numStrToInt "Twenty"    = return 20
+numStrToInt s           = throwError $ printf "[ERROR wordNum %s]" s
diff --git a/src/app/EpubTools/EpubName/Format/PubYear.hs b/src/app/EpubTools/EpubName/Format/PubYear.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Format/PubYear.hs
@@ -0,0 +1,39 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubName.Format.PubYear
+   ( extractYear
+   , getPubYear
+   )
+   where
+
+import Codec.Epub.Data.Metadata ( DateValue (..), DateEvent (Created, Date,
+  Epub, Issued, Modified), Metadata (metaDates) )
+import Control.Monad ( (<=<) )
+import qualified Data.Map.Strict as Map
+import Data.Monoid ( First (..), getFirst )
+import Text.Regex ( matchRegex, mkRegex )
+
+import EpubTools.EpubName.Opts
+import EpubTools.EpubName.Format.Util
+
+
+{- Look for publication date, return "" if none found
+-}
+getPubYear :: EN String
+getPubYear = do
+  yearHandling <- asks $ optPubYear . gOpts
+  let events = case yearHandling of
+        AnyDate -> [Issued, Created, Date, Epub, Modified]
+        NoModified -> [Issued, Created, Date, Epub]
+        NoDate -> []
+  datesMap <- asks (metaDates . gMetadata)
+  let fs = map (flip Map.lookup $ datesMap) events
+  pure . maybe "" ('_' :) . (extractYear <=< getFirst) . mconcat . map First $ fs
+
+
+extractYear :: DateValue -> Maybe String
+extractYear (DateValue dateString) =
+  case matchRegex (mkRegex "(^| )([0-9]{4})") dateString of
+    Just (_ : y : []) -> Just y
+    _                 -> Nothing
diff --git a/src/app/EpubTools/EpubName/Format/Util.hs b/src/app/EpubTools/EpubName/Format/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Format/Util.hs
@@ -0,0 +1,91 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubName.Format.Util
+   ( Globals (..)
+   , EN , runEN
+   , throwError
+   , asks
+   , scrubString
+   , sanitizeString
+   )
+   where
+
+import Codec.Epub.Data.Metadata
+import Codec.Epub.Data.Package
+import Control.Monad.Except
+import Control.Monad.Identity
+import Control.Monad.Reader
+import Data.Char
+import Data.List ( foldl' )
+import Text.Regex
+
+import EpubTools.EpubName.Opts
+
+
+data Globals = Globals
+   { gOpts :: Options
+   , gPackage :: Package
+   , gMetadata :: Metadata
+   }
+
+type EN a = ReaderT Globals (ExceptT String Identity) a
+
+runEN :: Globals -> EN a -> Either String a
+runEN env ev = runIdentity (runExceptT (runReaderT ev env))
+
+
+{- Convenience function to make a regex replacer for a given pattern 
+   and replacement string. Helps by doing the 'flip' of str and rpl
+   so you can partial eval.
+-}
+repl :: String -> String -> String -> String
+repl re rpl str = subRegex (mkRegex re) str rpl
+
+
+{- Transforms a string like this:
+      "the quick brown McCheeseburger" -> "TheQuickBrownMccheeseburger"
+-}
+camelCase :: String -> String
+camelCase s = concat $ map capFirst $ words s
+   where
+      capFirst s'@('I':'I':_) = map toUpper s'
+      capFirst s'@('I':'V':_) = map toUpper s'
+      capFirst s'@('I':'X':_) = map toUpper s'
+      capFirst (first:rest) = (toUpper first) : (map toLower rest)
+      capFirst _ = undefined
+
+
+{- A set of common string filters that apply to any and all parts
+   of every single string we process in this project.
+-}
+aggressiveFilters :: [(String -> String)]
+aggressiveFilters =
+   [ repl "[',\\?();#’\\*!]"  ""
+   , repl "]"                 ""    -- Tricky to put this in [ ]
+   , repl "[./]"              " "
+   , repl ":"                 "_"
+   , repl "[\\[\\\n]"         "_ "
+   , repl "-"                 "- "
+   , filter (/= '"')
+   -- Decided that I like the article included in titles
+   --, repl "^The "             ""
+   , repl "&"                 " And "
+   , camelCase
+   ]
+
+
+{- Utility function to apply the above filters to a string, giving you
+   back the transformed string
+-}
+scrubString :: String -> String
+scrubString s = foldl' (flip id) s aggressiveFilters
+
+
+{- Utility function to apply a very basic filter to make a legal filename,
+   giving you back the transformed string. This is used on everything
+   at the very end, just before the new filename is returned from the
+   formatting machinery.
+-}
+sanitizeString :: String -> String
+sanitizeString s = foldl' (flip id) s [ repl "/" " " ]
diff --git a/src/app/EpubTools/EpubName/Main.hs b/src/app/EpubTools/EpubName/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Main.hs
@@ -0,0 +1,65 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+module EpubTools.EpubName.Main
+   ( initialize
+   )
+   where
+
+import Control.Monad.Except
+import System.Directory ( doesFileExist )
+import System.Exit
+import Text.Printf
+
+import qualified EpubTools.EpubName.Doc.Rules as Rules
+import EpubTools.EpubName.Format.Compile
+import EpubTools.EpubName.Format.Format
+import EpubTools.EpubName.Opts
+import EpubTools.EpubName.Util
+
+
+initialize :: (MonadError ExitCode m, MonadIO m) =>
+   Options -> m [Formatter]
+initialize opts = (locateRules $ optRulesPaths opts)
+   >>= (loadFormatters $ optVerbose opts)
+
+
+locateRules :: (MonadError ExitCode m, MonadIO m) =>
+   [FilePath] -> m (String, String)
+locateRules paths = do
+   mbResult <- liftIO $ foldl (liftM2 mplus) (return Nothing)
+      $ map mbExists paths
+   maybe (return ("built-in", Rules.defaults))
+      loadRules mbResult
+
+   where
+      loadRules p = do
+         contents <- liftIO $ readFile p
+         return (p, contents)
+
+
+mbExists :: FilePath -> IO (Maybe FilePath)
+mbExists p = do
+   e <- doesFileExist p
+   if e
+      then return $ Just p
+      else return Nothing
+
+
+loadFormatters :: (MonadError ExitCode m, MonadIO m) =>
+   (Maybe Int) -> (String, String) -> m [Formatter]
+loadFormatters verbosity (name, contents) = case parseRules name contents of
+   Left err  -> do
+      _ <- liftIO $ printf "%s:\n%s" name (show err)
+      throwError exitInitFailure
+   Right fs  -> do
+      liftIO $ showRulesSource verbosity name
+      return fs
+
+
+showRulesSource :: (Monad m, PrintfType (m ()), PrintfArg t, Num a, Eq a) =>
+   Maybe a -> t -> m ()
+showRulesSource (Just 1) name = printf "Rules loaded from: %s\n" name
+showRulesSource _        _    = return ()
diff --git a/src/app/EpubTools/EpubName/Opts.hs b/src/app/EpubTools/EpubName/Opts.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Opts.hs
@@ -0,0 +1,228 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+{-# LANGUAGE FlexibleContexts #-}
+
+module EpubTools.EpubName.Opts
+   ( PubYear (..), Options (..)
+   , defaultOptions
+   , parseOpts, usageText
+   )
+   where
+
+import Control.Monad.Except
+import Data.Maybe
+import Data.Version ( showVersion )
+import Paths_epub_tools ( version )
+import System.Console.GetOpt
+import System.Environment
+import System.Exit
+import System.FilePath
+
+import EpubTools.EpubName.Util
+
+
+data PubYear
+   = AnyDate      -- Default
+      -- Use any date found in this order: Issued, Created, Epub (date element), Modified
+   | NoModified   -- Don't use Modified date: Issued, Created, Epub (date element)
+   | NoDate       -- Don't do publication date at all
+
+
+data Options = Options
+   { optDumpRules    :: Bool
+   , optHelp         :: Bool
+   , optHelpRules    :: Bool
+   , optInteractive  :: Bool
+   , optNoAction     :: Bool
+   , optPublisher    :: Bool
+   , optRulesPaths   :: [FilePath]
+   , optTargetDir    :: FilePath
+   , optVerbose      :: Maybe Int
+   , optPubYear      :: PubYear
+   }
+
+
+defaultRulesFile :: FilePath
+defaultRulesFile = "default.rules"
+
+
+userRulesPath :: IO (Maybe FilePath)
+userRulesPath = foldl (liftM2 mplus) (return Nothing) $ map mkdir
+   [ ("HOME", ".epubtools")      -- UNIX-like
+   , ("APPDATA", "epubtools")    -- Windows
+   ]
+
+   where
+      mkdir (var, subdir) = do
+         homeDir <- lookupEnv var
+         return $ (</> subdir </> defaultRulesFile) `fmap` homeDir
+
+
+defaultOptions :: IO Options
+defaultOptions = do
+   urp <- userRulesPath
+
+   return Options
+      { optDumpRules       = False
+      , optHelp            = False
+      , optHelpRules       = False
+      , optInteractive     = False
+      , optNoAction        = False
+      , optPublisher       = False
+      , optRulesPaths      = maybeToList urp
+      , optTargetDir       = "."
+      , optVerbose         = Nothing
+      , optPubYear         = AnyDate
+      }
+
+
+options :: [OptDescr (Options -> Options)]
+options =
+   [ Option ['M'] ["no-modified-date"]
+      (NoArg (\opts -> opts { optPubYear = NoModified } ))
+      "Don't use modified date for publication year"
+   , Option ['D'] ["no-date"]
+      (NoArg (\opts -> opts { optPubYear = NoDate } )) 
+      "Suppress inclusion of any publication year"
+   , Option [] ["dump-rules"] 
+      (NoArg (\opts -> opts { optDumpRules = True } ))
+      "Output built-in rules definitions, use to make your own"
+   , Option ['h'] ["help"] 
+      (NoArg (\opts -> opts { optHelp = True } ))
+      "This help text"
+   , Option [] ["help-rules"] 
+      (NoArg (\opts -> opts { optHelpRules = True } ))
+      "Help on the naming rules domain specific language"
+   , Option ['i'] ["interactive"] 
+      (NoArg (\opts -> opts { optInteractive = True } ))
+      "Prompt user interactively"
+   , Option ['n'] ["no-action"]
+      (NoArg (\opts -> opts { optNoAction = True } )) 
+      "Display what would be done, but do nothing"
+   , Option ['p'] ["publisher"]
+      (NoArg (\opts -> opts { optPublisher = True } )) 
+      "Include book publisher if present. See below"
+   , Option ['r'] ["rules"] (ReqArg prependRulesPath "FILE")
+      "Specify a rules file for naming the books"
+   , Option ['t'] ["target-dir"]
+      ( ReqArg
+         (\p opts -> opts { optTargetDir = p } )
+         "DIR")
+      "Target directory for successfully renamed books. Default: ."
+   , Option ['v'] ["verbose"]
+      ( OptArg
+         ((\n opts -> opts { optVerbose = Just (read n)}) . fromMaybe "1")
+         "LEVEL")
+      "Verbosity level: 1, 2"
+   ]
+
+
+prependRulesPath :: FilePath -> Options -> Options
+prependRulesPath p opts = opts { optRulesPaths = newlist }
+   where
+      newlist = p : optRulesPaths opts
+
+
+parseOpts :: (MonadError ExitCode m, MonadIO m) =>
+   [String] -> m (Options, [String])
+parseOpts argv = do
+   dos <- liftIO defaultOptions
+   case getOpt Permute options argv of
+      (o,n,[]  ) -> return (foldl (flip id) dos o, n)
+      (_,_,errs) -> do
+         liftIO $ do
+            ut <- usageText
+            putStrLn $ concat errs ++ ut
+         throwError exitInitFailure
+
+
+usageText :: IO String
+usageText = return $ (usageInfo header options) ++ "\n" ++ footer
+   where
+      header = init $ unlines
+         [ "USAGE: epubname [OPTIONS] FILES"
+         , "Rename epub book files with clear names based on their metadata"
+         , ""
+         , "OPTIONS:"
+         , ""
+         ]
+      footer = init $ unlines
+         [ "VERBOSITY LEVELS:"
+         , ""
+         , "   1 - Output which formatter processed the file"
+         , "   2 - Output the OPF Package and Metadata info"
+         , ""
+         , "EXIT CODES:"
+         , ""
+         , "   0 - success"
+         , "   1 - bad options"
+         , "   2 - failed to process one or more of the files given"
+         , ""
+         , "SIMPLE NAMING:"
+         , ""
+         , "Book names are constructed by examining parts of the OPF Package metadata such as the title, creators, contributors and dates."
+         , ""
+         , "Strings from the OPF metadata fields are stripped of punctuation, CamelCased and stripped of spaces. Resulting file names look like this:"
+         , ""
+         , "For books with a single author:"
+         , "   LastFirst[Middle]-TitleText[_year][_publisher].epub"
+         , ""
+         , "For books with multiple authors:"
+         , "   Last1_Last2[_Last3...]-TitleText[_year][_publisher].epub"
+         , ""
+         , "For books that have no clear authors, such as compilations:"
+         , "   TitleText[_year][_publisher].epub"
+         , ""
+         , "Only creator tags with either a role attribute of 'aut' or no role at all are considered authors. If a file-as attribute is present, this will be the preferred string. If not, the program tries to do some intelligent parsing of the name."
+         , ""
+         , "PUBLICATION DATE:"
+         , ""
+         , "This software will attempt to locate a publication date to use at the end of the filename. This can be suppressed with the --no-date switch. How the date is found depends on the document epub version:"
+         , ""
+         , "epub2: These date values are looked for in this order"
+         , "  issued    <dc:date opf:event='original-publication'>2013-09-17</dc:date>"
+         , "  created   <dc:date opf:event='publication'>2013-09-17T12:00:00Z</dc:date>"
+         , "  epub      <dc:date>2013-09-17</dc:date>"
+         , "  modified  <meta property='dcterms:modified'>2013-09-17</meta>"
+         , ""
+         , "epub3: These date values are looked for in this order"
+         , "  issued    <meta property='dcterms:issued'>2013-09-17</meta>"
+         , "  created   <meta property='dcterms:created'>2013-09-17T12:00:00Z</created>"
+         , "  date      <meta property='dcterms:date'>2013-09-18</meta>"
+         , "  epub      <dc:date>2013-09-17T12:00:00Z</dc:date>"
+         , "  modified  <meta property='dcterms:modified'>2013-09-17</meta>"
+         , ""
+         , "This software will fish the year out of whatever string it can find. Use the examples above as a guide for adding/editing these values yourself."
+         , ""
+         , "PUBLISHER:"
+         , ""
+         , "I wanted to provide a way to have multiple copies of the same book produced by different publishers and name them sort-of unambiguously. I came up with the idea of expecting a contributor tag with role attribute of 'bkp' (so far, this is fairly normal). And then use a file-as attribute on that tag to contain a small string to be used in the filename. The idea here is short and sweet for the file-as."
+         , ""
+         , "MORE COMPLICATED NAMING: MAGAZINES AND COLLECTIONS"
+         , ""
+         , "Magazines are kind of a sticky problem in that it's often desireable to have edition and/or date info in the filename. There's a lot of chaos out there with titling the epub editions of magazines. The solution in this software is to do some pattern matching on multiple fields in the magazine's metadata combined with custom naming rules for specific magazines."
+         , ""
+         , "This software ships with a set of rules to properly name over 20 magazines and compilations that are commonly purchased by the developer. These default rules can be extended to add new publications, and used as an example."
+         , ""
+         , "Another case is anthologies, which is to say: collections of works by multiple authors but not necessarily in periodical form. There is an anthology rule in the default rules that's set to look for any <dc:subject>anthology</dc:subject> tag in the metadata. In this case, the authors will be ignored and the title along with a publication year will be used (see above re: dc:date)"
+         , ""
+         , "epubname will search the following locations for a rules file, in this order:"
+         , ""
+         , "   Path specified in a --rules switch"
+         , "   $HOME/.epubtools/default.rules  -- or:"
+         , "   %APPDATA%\\epubtools\\default.rules  -- in Windows"
+         , "   Built-in rules, a comprehensive stock set of naming rules"
+         , ""
+         , "Use the built-in rules as a model for a custom file. See --dump-rules above"
+         , ""
+         , "Please see --help-rules for more information on the syntax of the DSL in the rules file."
+         , ""
+         , ""
+         , "For more information on the epub format:"
+         , "   epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm"
+         , "   epub3: https://www.w3.org/TR/epub/"
+         , ""
+         , ""
+         , "Version " ++ (showVersion version) ++ "  Dino Morelli <dino@ui3.info>"
+         ]
diff --git a/src/app/EpubTools/EpubName/Prompt.hs b/src/app/EpubTools/EpubName/Prompt.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Prompt.hs
@@ -0,0 +1,48 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubName.Prompt
+   ( PromptResult (..)
+   , prompt
+   , continue
+   )
+   where
+
+import Data.Char ( toLower )
+
+
+data PromptResult
+   = Yes
+   | No
+   | Quit
+   deriving Eq
+
+
+prompt :: IO PromptResult
+prompt = do
+   putStr "Rename this file? [Ynq], or ? for more options: "
+   c <- getChar
+   putStr "\n"
+
+   case (toLower c) of
+      'y'  -> return Yes
+      ' '  -> return Yes
+      '\n' -> return Yes
+      'n'  -> return No
+      'q'  -> return Quit
+      '?'  -> more >> prompt
+      _    -> putStrLn "Unknown choice" >> prompt
+
+
+continue :: PromptResult -> Bool
+continue Quit = False
+continue _    = True
+
+
+more :: IO ()
+more = putStr . unlines $
+   [ "y: rename this file"
+   , "n: don't rename this file"
+   , "q: cancel now, don't rename any files beyond this point"
+   , "<space> or <enter>: accept the default (which is capitalized)"
+   ]
diff --git a/src/app/EpubTools/EpubName/Util.hs b/src/app/EpubTools/EpubName/Util.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubName/Util.hs
@@ -0,0 +1,12 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubName.Util
+   where
+
+import System.Exit
+
+
+exitInitFailure, exitProcessingFailure :: ExitCode
+exitInitFailure = ExitFailure 1
+exitProcessingFailure = ExitFailure 128
diff --git a/src/app/EpubTools/EpubZip/Opts.hs b/src/app/EpubTools/EpubZip/Opts.hs
new file mode 100644
--- /dev/null
+++ b/src/app/EpubTools/EpubZip/Opts.hs
@@ -0,0 +1,76 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.EpubZip.Opts
+   ( Options (..)
+   , parseOpts, usageText
+   )
+   where
+
+import Data.Version ( showVersion )
+import Paths_epub_tools ( version )
+import System.Console.GetOpt
+
+
+data Options = Options
+   { optHelp :: Bool
+   , optOverwrite :: Bool
+   }
+
+
+defaultOptions :: Options
+defaultOptions = Options
+   { optHelp = False
+   , optOverwrite = False
+   }
+
+
+options :: [OptDescr (Options -> Options)]
+options =
+   [ Option ['h'] ["help"] 
+      (NoArg (\opts -> opts { optHelp = True } ))
+      "This help text"
+   , Option ['o'] ["overwrite"] 
+      (NoArg (\opts -> opts { optOverwrite = True } ))
+      "Force overwrite if dest file exists"
+   ]
+
+
+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: epubzip [OPTIONS] DESTDIR"
+         , "       epubzip [OPTIONS] DESTDIR/EPUBFILE"
+         , "Construct an epub zip archive from files in the current directory"
+         , ""
+         , "Options:"
+         ]
+      footer = init $ unlines
+         [ "If run with DESTDIR alone, epubzip will try to construct a name from the OPF package data for this book (see epubname). If run with DESTDIR/EPUBFILE, epubzip will use that name for the destination file."
+         , ""
+         , "You may have noticed that there is no epubunzip utility. Truth is, epubs are just zip files and you barely need epubzip either if you have the normal zip/unzip utilities installed. While not as fancy with file naming and leaving out dotfiles, this works for zipping:"
+         , ""
+         , "   $ cd DIR"
+         , "   $ zip -Xr9D ../EPUBFILE mimetype *"
+         , ""
+         , "And for unzipping, it's really just as easy:"
+         , ""
+         , "   $ mkdir TEMPDIR"
+         , "   $ cd TEMPDIR"
+         , "   $ unzip EPUBFILE"
+         , ""
+         , "For more information on the epub format:"
+         , "   epub2: http://idpf.org/epub/20/spec/OPF_2.0.1_draft.htm"
+         , "   epub3: http://www.idpf.org/epub/30/spec/epub30-publications.html"
+         , ""
+         , ""
+         , "Version " ++ (showVersion version) ++ "  Dino Morelli <dino@ui3.info>"
+         ]
diff --git a/src/app/epubmeta.hs b/src/app/epubmeta.hs
new file mode 100644
--- /dev/null
+++ b/src/app/epubmeta.hs
@@ -0,0 +1,33 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+import Data.Maybe
+import System.Environment ( getArgs )
+import System.Exit
+
+import EpubTools.EpubMeta.Display
+import EpubTools.EpubMeta.Edit
+import EpubTools.EpubMeta.Export
+import EpubTools.EpubMeta.Import
+import EpubTools.EpubMeta.Opts
+import EpubTools.EpubMeta.Util
+
+
+dispatch :: Options -> [FilePath] -> EM ()
+dispatch opts (f:[]) | optEdit opts /= NotEditing = edit opts f
+dispatch opts (f:[]) | isJust . optImport $ opts  = importOpf opts f
+dispatch opts (f:[]) | optExport opts /= NoExport = exportOpf opts f
+dispatch opts (f:[])                              = display opts f
+dispatch _    _                                   = throwError usageText
+
+
+main :: IO ()
+main = do
+   result <- runEM $
+      liftIO getArgs >>= parseOpts >>= uncurry dispatch
+   either exitFail (const (exitWith ExitSuccess)) result
+
+   where
+      exitFail msg = do
+         putStrLn msg
+         exitWith $ ExitFailure 1
diff --git a/src/app/epubname.hs b/src/app/epubname.hs
new file mode 100644
--- /dev/null
+++ b/src/app/epubname.hs
@@ -0,0 +1,164 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+-- This is for GHC 7.8/7.10 compatibility with the
+-- Control.Applicative import below
+{-# OPTIONS_GHC -fno-warn-unused-imports #-}
+
+{-# LANGUAGE FlexibleContexts #-}
+
+import Codec.Epub
+import Codec.Epub.Data.Metadata
+import Codec.Epub.Data.Package
+import Control.Applicative
+import Control.Monad
+import Control.Monad.Except
+import System.Directory ( doesDirectoryExist, doesFileExist, renameFile )
+import System.Environment ( getArgs )
+import System.FilePath
+import System.Exit
+import System.IO ( BufferMode ( NoBuffering )
+                 , hSetBuffering, stdin, stdout, stderr 
+                 )
+import Text.Printf
+
+import qualified EpubTools.EpubName.Doc.Dsl as Dsl
+import qualified EpubTools.EpubName.Doc.Rules as Rules
+import EpubTools.EpubName.Format.Format ( Formatter (..), tryFormatting )
+import EpubTools.EpubName.Format.Util
+import EpubTools.EpubName.Main
+import EpubTools.EpubName.Opts ( Options (..), parseOpts, usageText )
+import EpubTools.EpubName.Prompt ( PromptResult (..), prompt, continue )
+import EpubTools.EpubName.Util
+
+
+{- Construct additional verbose output
+-}
+formatF :: (String, Package, Metadata) -> String
+formatF (fmtUsed, _, _) = printf "\n   formatter: %s" fmtUsed
+
+formatFM :: (String, Package, Metadata) -> String
+formatFM (fmtUsed, pkg, md) =
+   printf "\n   formatter: %s\n%s\n%s" fmtUsed
+      (format pkg) (format md)
+
+
+{- Format and display output for a book that was processed
+-}
+displayResults :: MonadIO m => Options
+   -> FilePath -> FilePath -> String -> Package -> Metadata -> m ()
+displayResults opts oldPath newPath fmtUsed pkg md =
+   liftIO $ printf "%s -> %s%s\n" oldPath newPath
+      (additional (optVerbose opts) (fmtUsed, pkg, md))
+   where
+      additional Nothing  = const ""
+      additional (Just 1) = formatF
+      additional _        = formatFM
+
+
+{- Process an individual epub book file
+-}
+processBook :: Options -> [Formatter] -> [FilePath] -> Bool -> Bool
+   -> IO Bool
+
+processBook _    _          []              _     priRes =
+   return priRes
+
+processBook _    _          _               False priRes =
+   return priRes
+
+processBook opts formatters (oldPath:paths) _     priRes = do
+   result <- runExceptT $ do
+      {- Parse the book's metadata
+         The reason for the nested runErrorT block is, if there is
+         failure during parsing, we need to mark the result up with the
+         file path. Failures here will otherwise get lost in the output
+         when multiple books are processed at once.
+      -}
+      epm <- runExceptT $ do
+         xml <- getPkgXmlFromZip oldPath
+         (,) <$> getPackage xml <*> getMetadata xml
+      (pkg, md) <- either
+         ( \msg -> throwError
+            $ printf "ERROR: File %s: %s" oldPath msg
+         ) return epm
+
+      (fmtUsed, shortPath) <-
+         tryFormatting (Globals opts pkg md) formatters oldPath
+
+      let newPath = optTargetDir opts </> shortPath
+
+      fileExists <- liftIO $ doesFileExist newPath
+      when fileExists $ throwError $ 
+         printf "File %s already exists. No change." newPath
+
+      displayResults opts oldPath newPath fmtUsed pkg md
+
+      promptResult <- liftIO $ case (optInteractive opts) of
+         True  -> prompt
+         False -> return Yes
+
+      when ((promptResult == Yes) && (optNoAction opts == False)) $
+         liftIO $ renameFile oldPath newPath
+
+      return $ continue promptResult
+
+   (thisRes, cont) <- either (\errmsg -> do
+      putStrLn errmsg
+      return (False, True)
+      )
+      (\c -> return (True, c))
+      result
+
+   let newRes = case (priRes, thisRes) of
+         (False, _) -> False
+         (_    , r) -> r
+
+   processBook opts formatters paths cont newRes
+
+
+main :: IO ()
+main = do
+   -- No buffering, it messes with the order of output
+   mapM_ (flip hSetBuffering NoBuffering) [ stdout, stderr, stdin ]
+
+   either exitWith exitWith =<< (runExceptT $ do
+      -- Parse command-line arguments
+      (opts, paths) <- (liftIO getArgs) >>= parseOpts
+
+      -- User asked for rules help, this is a special termination case
+      when (optHelpRules opts) $ do
+         liftIO $ putStrLn Dsl.docs
+         throwError ExitSuccess
+
+      -- User asked for a dump of the built-in rules,
+      -- this is a special termination case
+      when (optDumpRules opts) $ do
+         liftIO $ putStr Rules.defaults
+         throwError ExitSuccess
+
+      -- User asked for help, this is a special termination case
+      when ((optHelp opts) || (null paths)) $ do
+         liftIO $ usageText >>= putStrLn
+         throwError ExitSuccess
+
+      let targetDir = optTargetDir opts
+      targetDirExists <- liftIO $ doesDirectoryExist targetDir
+      unless targetDirExists $ do
+         _ <- liftIO $ printf
+            "ERROR: Target directory doesn't exist: %s\n" targetDir
+         throwError exitInitFailure
+
+      -- Locate the rules file, load it and parse into a list of
+      -- formatters
+      fs <- initialize opts
+
+      when (optNoAction opts) $ liftIO
+         $ putStrLn "No-action specified"
+
+      -- Perform the formatting operation on the books
+      code <- liftIO $ processBook opts fs paths True True
+      case code of
+         True  -> return ExitSuccess
+         False -> throwError exitProcessingFailure
+      )
diff --git a/src/app/epubzip.hs b/src/app/epubzip.hs
new file mode 100644
--- /dev/null
+++ b/src/app/epubzip.hs
@@ -0,0 +1,79 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+import Codec.Epub
+import Control.Monad
+import Control.Monad.Except
+import System.Directory
+import System.Environment
+import System.Exit
+import System.FilePath
+import System.IO ( BufferMode ( NoBuffering )
+                 , hSetBuffering, stdout, stderr 
+                 )
+import Text.Printf
+
+import EpubTools.EpubName.Format.Format ( tryFormatting )
+import EpubTools.EpubName.Format.Util
+import EpubTools.EpubName.Main
+import qualified EpubTools.EpubName.Opts as EN
+import EpubTools.EpubZip.Opts
+
+
+main :: IO ()
+main = do
+   -- No buffering, it messes with the order of output
+   mapM_ (flip hSetBuffering NoBuffering) [ stdout, stderr ]
+
+   either exitWith exitWith =<< (runExceptT $ do
+      -- Parse command-line arguments
+      (opts, paths) <- liftIO $ getArgs >>= parseOpts
+
+      -- User asked for help, this is a special termination case
+      when ((optHelp opts) || (null paths)) $ do
+         liftIO $ putStrLn usageText
+         throwError ExitSuccess
+
+      let inputPath = head paths
+      isDir <- liftIO $ doesDirectoryExist inputPath
+
+      en <- if isDir
+            then do
+               dos <- liftIO EN.defaultOptions
+               fs <- initialize dos
+               runExceptT $ do
+                  xml <- snd `fmap` getPkgPathXmlFromDir "."
+                  pkg <- getPackage xml
+                  metadata <- getMetadata xml
+
+                  (_, newPath) <- tryFormatting
+                     (Globals dos pkg metadata) fs
+                     "CURRENT DIRECTORY"
+
+                  return $ inputPath </> newPath
+            else return . Right $ inputPath
+
+      case en of
+         Right zipPath -> do
+            exists <- liftIO $ doesFileExist zipPath
+
+            case ((optOverwrite opts), exists) of
+               (False, True) -> do
+                  _ <- liftIO $ printf
+                     "File %s exists, use --overwrite to force\n"
+                     zipPath
+                  throwError $ ExitFailure 1
+               (True,  True) -> liftIO $ removeFile zipPath
+               (_   ,  _   ) -> return ()
+
+            _ <- liftIO $ do
+               archive <- mkEpubArchive "."
+               writeArchive zipPath archive
+               printf "Book created: %s\n" zipPath
+
+            return ExitSuccess
+
+         Left errorMsg -> do
+            liftIO $ putStrLn errorMsg
+            throwError $ ExitFailure 1
+      )
diff --git a/src/tests/EpubTools/Test/EpubName/Format.hs b/src/tests/EpubTools/Test/EpubName/Format.hs
new file mode 100644
--- /dev/null
+++ b/src/tests/EpubTools/Test/EpubName/Format.hs
@@ -0,0 +1,1657 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.Test.EpubName.Format
+   ( formatTests
+   )
+   where
+
+import Codec.Epub.Data.Metadata
+import Codec.Epub.Data.Package
+import Control.Monad.Except
+import qualified Data.Map.Strict as Map
+import Test.HUnit ( Test (..), assertEqual )
+import Test.HUnit.Base ( Assertion )
+
+import EpubTools.EpubName.Format.Format
+import EpubTools.EpubName.Format.Util
+import EpubTools.EpubName.Opts
+
+
+formatTests :: Options -> [Formatter] -> Test
+formatTests opts fs = TestLabel "Format" $ TestList $
+   map (\f -> f ((Globals opts (Package "3.3" "") emptyMetadata), fs))
+      [ testAuthorMinimal
+      , testAuthorOneName
+      , testAuthorRole
+      , testAuthorFileas
+      , testAuthorFileasParens
+      , testAuthorFull
+      , testAuthorMiddle
+      , testMultiAutCreators
+      , testMultiAutOneString
+      , testNoAuthor
+      , testColon
+      , testCreatorsNoAuthor
+      , testCreatorsNoAuthorPubDate
+      , testCreatorLastFirst
+      , testTitleCaps
+      , testTitleBracket
+      , testTitleNone
+      , testTitleMultiline
+      , testTitleHyphen
+      , testTitleHyphenDates
+      , testTitleRomanNum
+      , testAllPunctuation
+      , testPubYearNoDatesPresent
+      , testPubYearAnyIssued
+      , testPubYearAnyCreated
+      , testPubYearAnyDate
+      , testPubYearAnyEpub
+      , testPubYearAnyModified
+      , testPubYearAnyAllPresent
+      , testPubYearNoModified
+      , testPubYearNoDate
+      , testMagAeon
+      , testMagAEon
+      , testMagApexPound
+      , testMagApexIssue
+      , testMagApexLong
+      , testChallengingDestinyShort
+      , testChallengingDestinyLong
+      , testAnalogSingle
+      , testAnalogDouble
+      , testAsimovs
+      , testFantasyMagazine
+      , testFsfShort
+      , testFsfShortComma
+      , testFsfLong
+      , testFsfAmpersand
+      , testFsfAmpersandSpaces
+      , testFsfVeryLong
+      , testMagFutureOrbits
+      , testGudShort
+      , testGudLong
+      , testGudVeryLong
+      , testInterzone
+      , testInterzoneCaps
+      , testInterzoneOldLong
+      , testInterzoneOldShort
+      , testNemesisShort
+      , testNemesisLong
+      , testMagSomethingWicked
+      , testMagSomethingWickedMonth
+      , testSFBestOf
+      , testBestSF
+      , testYearsBest
+      , testMagBlackStatic
+      , testRageMachineMag
+      , testEclipseMagWord
+      , testEclipseMagNum
+      , testBcs
+      , testBkpFileAs
+      , testBkpText
+      , testBkpMissing
+      , testMagUniverse
+      , testMagClarkesworld
+      , testLightspeedDate
+      , testLightspeedMagIssue
+      , testLightspeedIssue
+      , testLightspeedMagIssueDate
+      , testMagWeirdTales
+      , testMagGalaxysEdge
+      , testMagPlasmaFreq
+      , testMagPlasmaFreqMonth
+      , testMagPunchinello1800s
+      , testMagGenericVolNo3
+      , testMagSubjWithIssue
+      , testWomensInstituteLib
+      , testAnthologyDate
+      , testAnthology
+      , testMagLunaStation
+      ]
+
+
+assertNewName :: Globals -> [Formatter] -> String
+   -> (String, String) -> Assertion
+assertNewName globals fs desc expected = do
+   result <- runExceptT $ tryFormatting globals fs ""
+   let actual = either (\em -> ("NO FORMATTER", em)) id result
+   assertEqual desc expected actual
+
+
+testAuthorMinimal :: (Globals, [Formatter]) -> Test
+testAuthorMinimal (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "minimal author" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing
+            "Herman Melville"]
+         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "MelvilleHerman-MobyDick.epub"
+         )
+
+
+testAuthorOneName :: (Globals, [Formatter]) -> Test
+testAuthorOneName (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "author is a single word" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing
+            "Melville"]
+         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "Melville-MobyDick.epub"
+         )
+
+
+testAuthorRole :: (Globals, [Formatter]) -> Test
+testAuthorRole (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "author with role" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing
+            Nothing "Herman Melville"]
+         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "MelvilleHerman-MobyDick.epub"
+         )
+
+
+testAuthorFileas :: (Globals, [Formatter]) -> Test
+testAuthorFileas (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "author with file-as" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing
+            (Just "Melville, Herman")
+            Nothing
+            "Herman Melville"]
+         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "MelvilleHerman-MobyDick.epub"
+         )
+
+
+testAuthorFileasParens :: (Globals, [Formatter]) -> Test
+testAuthorFileasParens (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "author with file-as and pesky parenthesized full names" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing
+            (Just "Fletcher, J. S. (Joseph Smith)")
+            Nothing
+            "J. S. Fletcher"]
+         , metaTitles = [Title Nothing Nothing Nothing "The Middle of Things"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "FletcherJS-TheMiddleOfThings.epub"
+         )
+
+
+testAuthorFull :: (Globals, [Formatter]) -> Test
+testAuthorFull (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "author fully filled out" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut")
+            (Just "Melville, Herman")
+            Nothing
+            "Herman Melville"]
+         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "MelvilleHerman-MobyDick.epub"
+         )
+
+
+testAuthorMiddle :: (Globals, [Formatter]) -> Test
+testAuthorMiddle (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "author with middle-name" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut")
+            (Just "Wallace, David Foster")
+            Nothing
+            "David Foster Wallace"]
+         , metaTitles = [Title Nothing Nothing Nothing "Infinite Jest"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "WallaceDavidFoster-InfiniteJest.epub"
+         )
+
+
+testMultiAutCreators :: (Globals, [Formatter]) -> Test
+testMultiAutCreators (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "several authors" expected
+   where
+      meta = emptyMetadata
+         { metaCreators =
+            [ Creator (Just "aut") Nothing
+               Nothing "James Patrick Kelly"
+            , Creator (Just "aut") (Just "Kessel, John")
+               Nothing "John Kessel"
+            , Creator (Just "aut") Nothing
+               Nothing "Jonathan Lethem"
+            ]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Ninety Percent of Everything"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "Kelly_Kessel_Lethem-NinetyPercentOfEverything.epub"
+         )
+
+
+testMultiAutOneString :: (Globals, [Formatter]) -> Test
+testMultiAutOneString (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "more than one author separated by & and/or and" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing
+            "Yvonne Q. Anderson & Eva Tunglewacker and Jefferson Milner"]
+         , metaTitles = [Title Nothing Nothing Nothing "Big Trouble"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "Anderson_Tunglewacker_Milner-BigTrouble.epub"
+         )
+
+
+testNoAuthor :: (Globals, [Formatter]) -> Test
+testNoAuthor (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "no creator(s) at all" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = []
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Some Collection of Fine Stories, Volume 1"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "SomeCollectionOfFineStoriesVolume1.epub"
+         )
+
+
+testCreatorsNoAuthor :: (Globals, [Formatter]) -> Test
+testCreatorsNoAuthor (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "creators, but no author(s) at all" expected
+   where
+      meta = emptyMetadata
+         { metaCreators =
+            [ Creator (Just "edt") Nothing
+               Nothing "Graham Spindlewest"
+            , Creator (Just "ill") Nothing
+               Nothing "Eva Tunglewacker"
+            ]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Some Collection of Fine Stories, Volume 1"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "SomeCollectionOfFineStoriesVolume1.epub"
+         )
+
+
+testColon :: (Globals, [Formatter]) -> Test
+testColon (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "colon becomes underscore" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing
+            "Ed Howdershelt"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "Book 1: 3rd World Products, Inc."]
+         }
+      expected =
+         ( "ordinary_book"
+         , "HowdersheltEd-Book1_3rdWorldProductsInc.epub"
+         )
+
+
+testCreatorsNoAuthorPubDate :: (Globals, [Formatter]) -> Test
+testCreatorsNoAuthorPubDate (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "creators, but no author(s) at all, with pub date" expected
+   where
+      meta = emptyMetadata
+         { metaCreators =
+            [ Creator (Just "edt") Nothing
+               Nothing "Graham Spindlewest"
+            , Creator (Just "ill") Nothing
+               Nothing "Eva Tunglewacker"
+            ]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Some Collection of Fine Stories, Volume 1"]
+         , metaDates = Map.fromList [(Created, DateValue "2008")]
+         }
+      expected =
+         ( "ordinary_book"
+         , "SomeCollectionOfFineStoriesVolume1_2008.epub"
+         )
+
+
+testCreatorLastFirst :: (Globals, [Formatter]) -> Test
+testCreatorLastFirst (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "creator text (not file-as) is last-name-first" expected
+   where
+      meta = emptyMetadata
+         { metaCreators =
+            [ Creator Nothing Nothing
+               Nothing "Spindlewest, Graham"
+            ]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "A Midnight's Horror Story"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "SpindlewestGraham-AMidnightsHorrorStory.epub"
+         )
+
+
+testTitleCaps :: (Globals, [Formatter]) -> Test
+testTitleCaps (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "title all caps" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Greg Bear"]
+         , metaTitles = [Title Nothing Nothing Nothing "EON"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "BearGreg-Eon.epub"
+         )
+
+
+testTitleBracket :: (Globals, [Formatter]) -> Test
+testTitleBracket (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "title with brackets" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Mercedes Lackey"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "SKitty [Shipscat series #1]"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "LackeyMercedes-Skitty_ShipscatSeries1.epub"
+         )
+
+
+testTitleNone :: (Globals, [Formatter]) -> Test
+testTitleNone (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "missing title" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Nobody McCrankypants"]
+         }
+      expected =
+         ( "NO FORMATTER"
+         , " [ERROR No formatter found]"
+         )
+
+
+testTitleMultiline :: (Globals, [Formatter]) -> Test
+testTitleMultiline (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "multiline title" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator
+            Nothing
+            (Just "Castleman, Virginia Carter")
+            Nothing
+            "Virginia Carter Castleman"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Pocahontas.\nA Poem"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "CastlemanVirginiaCarter-Pocahontas_APoem.epub"
+         )
+
+
+testTitleHyphen :: (Globals, [Formatter]) -> Test
+testTitleHyphen (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "title with hyphen" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator
+            Nothing
+            (Just "Spinoza, Benedictus de")
+            Nothing
+            "Benedictus de Spinoza"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Theologico-Political Treatise"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "SpinozaBenedictusDe-Theologico-PoliticalTreatise.epub"
+         )
+
+
+testTitleHyphenDates :: (Globals, [Formatter]) -> Test
+testTitleHyphenDates (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "title with hyphen separating dates" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator
+            Nothing
+            (Just "Anonymous")
+            Nothing
+            "Anonymous"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Queen Victoria\nStory of Her Life and Reign, 1819-1901"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "Anonymous-QueenVictoria_StoryOfHerLifeAndReign1819-1901.epub"
+         )
+
+testTitleRomanNum :: (Globals, [Formatter]) -> Test
+testTitleRomanNum (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "title with Roman numerals" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator
+            Nothing
+            (Just "Hauptmann, Gerhart")
+            Nothing
+            "Gerhart Hauptmann"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "The Dramatic Works of Gerhart Hauptmann\nVolume III"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "HauptmannGerhart-TheDramaticWorksOfGerhartHauptmann_VolumeIII.epub"
+         )
+
+
+testAllPunctuation :: (Globals, [Formatter]) -> Test
+testAllPunctuation (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "big test of all punctuation" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Dino Morelli"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "The *crazy*: Sand-box. Of Smedley's discontent/loathing, fear & Malnourishment? (Maybe not!); [Part #2]"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "MorelliDino-TheCrazy_Sand-BoxOfSmedleysDiscontentLoathingFearAndMalnourishmentMaybeNot_Part2.epub"
+         )
+
+
+testPubYearNoDatesPresent :: (Globals, [Formatter]) -> Test
+testPubYearNoDatesPresent (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta} fs
+      "book with no dates at all" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "JonesJim-ATimelessStory.epub"
+         )
+
+
+testPubYearAnyIssued :: (Globals, [Formatter]) -> Test
+testPubYearAnyIssued (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "book with only issued date" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+         , metaDates = Map.fromList [(Issued, DateValue "2003")]
+         }
+      expected =
+         ( "ordinary_book"
+         , "JonesJim-ATimelessStory_2003.epub"
+         )
+
+
+testPubYearAnyCreated :: (Globals, [Formatter]) -> Test
+testPubYearAnyCreated (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "book with only created date" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+         , metaDates = Map.fromList [(Created, DateValue "2003")]
+         }
+      expected =
+         ( "ordinary_book"
+         , "JonesJim-ATimelessStory_2003.epub"
+         )
+
+
+testPubYearAnyDate :: (Globals, [Formatter]) -> Test
+testPubYearAnyDate (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "book with only created date" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+         , metaDates = Map.fromList [(Date, DateValue "2003")]
+         }
+      expected =
+         ( "ordinary_book"
+         , "JonesJim-ATimelessStory_2003.epub"
+         )
+
+
+testPubYearAnyEpub :: (Globals, [Formatter]) -> Test
+testPubYearAnyEpub (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "book with only the simple (date element) date" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+         , metaDates = Map.fromList [(Epub, DateValue "2003")]
+         }
+      expected =
+         ( "ordinary_book"
+         , "JonesJim-ATimelessStory_2003.epub"
+         )
+
+
+testPubYearAnyModified :: (Globals, [Formatter]) -> Test
+testPubYearAnyModified (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "book with only modified date" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+         , metaDates = Map.fromList [(Modified, DateValue "2003")]
+         }
+      expected =
+         ( "ordinary_book"
+         , "JonesJim-ATimelessStory_2003.epub"
+         )
+
+
+testPubYearAnyAllPresent :: (Globals, [Formatter]) -> Test
+testPubYearAnyAllPresent (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "book with all dates" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+         , metaDates = Map.fromList
+            [ (Epub, DateValue "2001")
+            , (Date, DateValue "2005")
+            , (Modified, DateValue "2004")
+            , (Created, DateValue "2003")
+            , (Issued, DateValue "2002")
+            ]
+         }
+      expected =
+         ( "ordinary_book"
+         , "JonesJim-ATimelessStory_2002.epub"
+         )
+
+
+testPubYearNoModified :: (Globals, [Formatter]) -> Test
+testPubYearNoModified (gs, fs) = TestCase $
+   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+      "book with only modified date, --no-modified-date switch" expected
+   where
+      testOpts = (gOpts gs) { optPubYear = NoModified }
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+         , metaDates = Map.fromList [ (Modified, DateValue "2004") ]
+         }
+      expected =
+         ( "ordinary_book"
+         , "JonesJim-ATimelessStory.epub"
+         )
+
+
+testPubYearNoDate :: (Globals, [Formatter]) -> Test
+testPubYearNoDate (gs, fs) = TestCase $
+   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+      "epub book with lots of dates but we don't want"
+      expected
+   where
+      testOpts = (gOpts gs) { optPubYear = NoDate }
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
+         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
+         , metaDates = Map.fromList
+            [ (Epub, DateValue "2001")
+            , (Date, DateValue "2005")
+            , (Modified, DateValue "2004")
+            , (Created, DateValue "2003")
+            , (Issued, DateValue "2002")
+            ]
+         }
+      expected =
+         ( "ordinary_book"
+         , "JonesJim-ATimelessStory.epub"
+         )
+
+
+testMagAeon :: (Globals, [Formatter]) -> Test
+testMagAeon (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "Aeon magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "Aeon Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing "Aeon Eight"]
+         }
+      expected =
+         ( "magAeon"
+         , "AeonMagazine08.epub"
+         )
+
+
+testMagAEon :: (Globals, [Formatter]) -> Test
+testMagAEon (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "AEon magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing "AEon Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing "Aeon Thirteen"]
+         }
+      expected =
+         ( "magAeon"
+         , "AeonMagazine13.epub"
+         )
+
+
+testMagApexPound :: (Globals, [Formatter]) -> Test
+testMagApexPound (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Apex Magazine, pound sign and issue number" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing
+            Nothing "Apex Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Apex Magazine #16"]
+         }
+      expected =
+         ( "magApex"
+         , "ApexMagazine016.epub"
+         )
+
+
+testMagApexIssue :: (Globals, [Formatter]) -> Test
+testMagApexIssue (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Apex Magazine, Issue number" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing
+            Nothing "Apex Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Apex Magazine Issue 17"]
+         }
+      expected =
+         ( "magApex"
+         , "ApexMagazine017.epub"
+         )
+
+
+testMagApexLong :: (Globals, [Formatter]) -> Test
+testMagApexLong (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Apex Magazine, title with month year" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing
+            Nothing "Apex Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Apex Magazine: Issue 101, October 2017"]
+         }
+      expected =
+         ( "magApex"
+         , "ApexMagazine101.epub"
+         )
+
+
+testChallengingDestinyShort :: (Globals, [Formatter]) -> Test
+testChallengingDestinyShort (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Challenging Destiny Magazine, short title" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Crystalline Sphere Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Challenging Destiny #23"]
+         }
+      expected =
+         ( "magChallengingDestiny"
+         , "ChallengingDestinyMagazine023.epub"
+         )
+
+
+testChallengingDestinyLong :: (Globals, [Formatter]) -> Test
+testChallengingDestinyLong (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Challenging Destiny Magazine, long title" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "Crystalline Sphere Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "Challenging Destiny #24: August 2007"]
+         }
+      expected =
+         ( "magChallengingDestiny"
+         , "ChallengingDestinyMagazine024.epub"
+         )
+
+
+testAnalogSingle :: (Globals, [Formatter]) -> Test
+testAnalogSingle (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "Analog, single month" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Dell Magazine Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Analog SFF, November 2010"]
+         }
+      expected =
+         ( "magAnalog"
+         , "AnalogSF2010-11.epub"
+         )
+
+
+testAnalogDouble :: (Globals, [Formatter]) -> Test
+testAnalogDouble (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "Analog, double month" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Dell Magazine Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Analog SFF, July-August 2003"]
+         }
+      expected =
+         ( "magAnalog"
+         , "AnalogSF2003-07_08.epub"
+         )
+
+
+testAsimovs :: (Globals, [Formatter]) -> Test
+testAsimovs (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "Asimovs" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Dell Magazine Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Asimov's SF, August 2003"]
+         }
+      expected =
+         ( "magAnalog"
+         , "AsimovsSF2003-08.epub"
+         )
+
+testFantasyMagazine :: (Globals, [Formatter]) -> Test
+testFantasyMagazine (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "Fantasy Magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators =
+            [ Creator (Just "aut") (Just "Howard, Kat & Tanzer, Molly & Beagle, Peter S. & Howard, Jonathan L. & Valentine, Genevieve & Vaughn, Carrie & Pilinovsky, Helen") Nothing "Kat Howard"
+            , Creator (Just "aut") Nothing Nothing "Molly Tanzer"
+            , Creator (Just "aut") Nothing Nothing "Peter S. Beagle"
+            , Creator (Just "aut") Nothing Nothing "Jonathan L. Howard"
+            , Creator (Just "aut") Nothing Nothing "Genevieve Valentine"
+            , Creator (Just "aut") Nothing Nothing "Carrie Vaughn"
+            , Creator (Just "aut") Nothing Nothing "Helen Pilinovsky"
+            ]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Fantasy Magazine Issue 49"]
+         }
+      expected =
+         ( "magFantasyMag"
+         , "FantasyMagazine049.epub"
+         )
+
+
+testFsfShort :: (Globals, [Formatter]) -> Test
+testFsfShort (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "FSF Magazine, short" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Spilogale Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing "FSF Oct/Nov 2003"]
+         }
+      expected =
+         ( "magFsf"
+         , "FantasyScienceFiction2003-10_11.epub"
+         )
+
+
+testFsfShortComma :: (Globals, [Formatter]) -> Test
+testFsfShortComma (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "FSF Magazine, short, comma" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Spilogale Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing "FSF, April 2008"]
+         }
+      expected =
+         ( "magFsf"
+         , "FantasyScienceFiction2008-04.epub"
+         )
+
+
+testFsfLong :: (Globals, [Formatter]) -> Test
+testFsfLong (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "FSF Magazine, long" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "Spilogale Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "FSF Magazine, April 2006"]
+         }
+      expected =
+         ( "magFsf"
+         , "FantasyScienceFiction2006-04.epub"
+         )
+
+
+testFsfAmpersand :: (Globals, [Formatter]) -> Test
+testFsfAmpersand (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "FSF Magazine, ampersand" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Spilogale"]
+         , metaTitles = [Title Nothing Nothing Nothing "F&SF, Apr 2004"]
+         }
+      expected =
+         ( "magFsf"
+         , "FantasyScienceFiction2004-04.epub"
+         )
+
+
+testFsfAmpersandSpaces :: (Globals, [Formatter]) -> Test
+testFsfAmpersandSpaces (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "FSF Magazine, ampersand, spaces" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Spilogale"]
+         , metaTitles = [Title Nothing Nothing Nothing "F & SF Dec 2003"]
+         }
+      expected =
+         ( "magFsf"
+         , "FantasyScienceFiction2003-12.epub"
+         )
+
+
+testFsfVeryLong :: (Globals, [Formatter]) -> Test
+testFsfVeryLong (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "FSF Magazine, very long name" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing
+            Nothing "Spilogale Inc."]
+         , metaTitles = [Title Nothing Nothing Nothing "Fantasy & Science Fiction, November/December 2017"]
+         }
+      expected =
+         ( "magFsf"
+         , "FantasyScienceFiction2017-11_12.epub"
+         )
+
+
+testMagFutureOrbits :: (Globals, [Formatter]) -> Test
+testMagFutureOrbits (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "testMagFutureOrbits" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "Vander Neut Publications, L.L.C."]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "Future Orbits Issue 5, June/July 2002"]
+         }
+      expected =
+         ( "magFutureOrbits"
+         , "FutureOrbitsMagazine05_2002-06_07.epub"
+         )
+
+
+testGudShort :: (Globals, [Formatter]) -> Test
+testGudShort (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Gud Magazine, short" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "GUD Magazine Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "GUD Magazine Issue 0 :: Spring 2007"]
+         }
+      expected =
+         ( "magGud"
+         , "GUDMagazine00.epub"
+         )
+
+
+testGudLong :: (Globals, [Formatter]) -> Test
+testGudLong (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Gud Magazine, long" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "GUD Magazine Authors, Jeff Somers, Jeremy Shipp"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "GUD Magazine Issue 2 :: Spring 2008"]
+         }
+      expected =
+         ( "magGud"
+         , "GUDMagazine02.epub"
+         )
+
+
+testGudVeryLong :: (Globals, [Formatter]) -> Test
+testGudVeryLong (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Gud Magazine, very long" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "GUD Magazine Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "Greatest Uncommon Denominator Magazine Issue 4 :: Spring 2009"]
+         }
+      expected =
+         ( "magGud"
+         , "GUDMagazine04.epub"
+         )
+
+
+testInterzone :: (Globals, [Formatter]) -> Test
+testInterzone (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Interzone Magazine, Smashwords style" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "TTA Press"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "Interzone 233 Mar - Apr 2011"]
+         }
+      expected =
+         ( "magInterzone"
+         , "InterzoneSFF233.epub"
+         )
+
+
+testInterzoneCaps :: (Globals, [Formatter]) -> Test
+testInterzoneCaps (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Interzone Magazine, started in 2014" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing (Just "Interzone 249")
+            Nothing "TTA Press"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "INTERZONE #249 SCIENCE FICTION & FANTASY MAGAZINE (NOVâ\x80\x93DEC 2013)"]
+         }
+      expected =
+         ( "magInterzone"
+         , "InterzoneSFF249.epub"
+         )
+
+
+testInterzoneOldLong :: (Globals, [Formatter]) -> Test
+testInterzoneOldLong (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Interzone Magazine, old style, long" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "TTA Press Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "Interzone Science Fiction and Fantasy Magazine #216"]
+         }
+      expected =
+         ( "magInterzone"
+         , "InterzoneSFF216.epub"
+         )
+
+
+testInterzoneOldShort :: (Globals, [Formatter]) -> Test
+testInterzoneOldShort (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Interzone Magazine, old style, short" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "TTA Press Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing 
+            "Interzone SFF #212"]
+         }
+      expected =
+         ( "magInterzone"
+         , "InterzoneSFF212.epub"
+         )
+
+
+testNemesisShort :: (Globals, [Formatter]) -> Test
+testNemesisShort (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Nemesis Magazine, short" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "Stephen Adams"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Nemesis Magazine #2"]
+         }
+      expected =
+         ( "magNemesis"
+         , "NemesisMag002.epub"
+         )
+
+
+testNemesisLong :: (Globals, [Formatter]) -> Test
+testNemesisLong (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Nemesis Magazine, long" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "Stephen Adams"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Nemesis Magazine #7: Featuring Victory Rose in Death Stalks the Ruins"]
+         }
+      expected =
+         ( "magNemesis"
+         , "NemesisMag007.epub"
+         )
+
+
+testMagSomethingWicked :: (Globals, [Formatter]) -> Test
+testMagSomethingWicked (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Something Wicked Magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing Nothing
+            "Something Wicked Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Something Wicked SF and Horror Magazine #5"]
+         }
+      expected =
+         ( "magSomethingWicked"
+         , "SomethingWicked005.epub"
+         )
+
+
+testMagSomethingWickedMonth :: (Globals, [Formatter]) -> Test
+testMagSomethingWickedMonth (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Something Wicked Magazine, month in title" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut")
+            (Just "Authors, Something Wicked")
+            Nothing
+            "Something Wicked Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Something Wicked #14 (October 2011)"]
+         }
+      expected =
+         ( "magSomethingWicked"
+         , "SomethingWicked014.epub"
+         )
+
+
+testSFBestOf :: (Globals, [Formatter]) -> Test
+testSFBestOf (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Science Fiction: The Best of the Year" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "Rich Horton"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Science Fiction: The Best of the Year, 2007 Edition"]
+         , metaSubjects = ["anthology"]
+         }
+      expected =
+         ( "anthology"
+         , "ScienceFiction_TheBestOfTheYear2007Edition.epub"
+         )
+
+
+testBestSF :: (Globals, [Formatter]) -> Test
+testBestSF (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "The Best Science Fiction and Fantasy of the Year" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut")
+            (Just "Strahan, Jonathan")
+            Nothing
+            "Jonathan Strahan"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "The Best Science Fiction and Fantasy of the Year: Volume 2"]
+         , metaSubjects = ["anthology"]
+         }
+      expected =
+         ( "anthology"
+         , "TheBestScienceFictionAndFantasyOfTheYear_Volume2.epub"
+         )
+
+
+testYearsBest :: (Globals, [Formatter]) -> Test
+testYearsBest (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "The Year's Best SF" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing Nothing
+            "Rich Horton, Michael Swanwick, Karen Joy Fowler"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "The Year's Best Science Fiction: 2008 Edition"]
+         , metaSubjects = ["anthology"]
+         }
+      expected =
+         ( "anthology"
+         , "TheYearsBestScienceFiction_2008Edition.epub"
+         )
+
+
+testMagBlackStatic :: (Globals, [Formatter]) -> Test
+testMagBlackStatic (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Black Static Magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing Nothing 
+            Nothing "TTA Press Authors"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Black Static Horror Magazine #5"]
+         }
+      expected =
+         ( "magBlackStatic"
+         , "BlackStaticHorrorMagazine05.epub"
+         )
+
+
+testRageMachineMag :: (Globals, [Formatter]) -> Test
+testRageMachineMag (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Rage Machine Magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "edt") Nothing 
+            Nothing "G. W. Thomas"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Rage Machine Magazine #1--December 2005"]
+         }
+      expected =
+         ( "magRageMachine"
+         , "RageMachineMagazine1_2005-12.epub"
+         )
+
+
+testEclipseMagWord :: (Globals, [Formatter]) -> Test
+testEclipseMagWord (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Eclipse Magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut")
+            (Just "Strahan, Jonathan")
+            Nothing
+            "Jonathan Strahan"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Eclipse One"]
+         }
+      expected =
+         ( "magEclipse_word"
+         , "Eclipse01.epub"
+         )
+
+
+testEclipseMagNum :: (Globals, [Formatter]) -> Test
+testEclipseMagNum (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Eclipse Magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut")
+            (Just "Strahan, Jonathan")
+            Nothing
+            "Jonathan Strahan"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Eclipse 4: New Science Fiction and Fantasy"]
+         }
+      expected =
+         ( "magEclipse_num"
+         , "Eclipse04.epub"
+         )
+
+
+testBcs :: (Globals, [Formatter]) -> Test
+testBcs (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Beneath Ceaseless Skies Magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = 
+            [ Creator (Just "aut")
+               (Just "Tidwell, Erin A.")
+               Nothing
+               "Hoover, Kenneth Mark"
+            , Creator (Just "aut")
+               (Just "Tidwell, Erin A.")
+               Nothing
+               "Tidwell, Erin A."
+            ]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Beneath Ceaseless Skies #32"]
+         }
+      expected =
+         ( "magBcs"
+         , "BeneathCeaselessSkies_Issue032.epub"
+         )
+
+
+testBkpFileAs :: (Globals, [Formatter]) -> Test
+testBkpFileAs (gs, fs) = TestCase $
+   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+      "book publisher suffix requested and present in file-as"
+      expected
+   where
+      testOpts = (gOpts gs) { optPublisher = True }
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing
+            Nothing "Herman Melville"]
+         , metaContributors = [Creator (Just "bkp") (Just "acme") 
+            Nothing "Acme Publishing, Inc."]
+         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "MelvilleHerman-MobyDick_acme.epub"
+         )
+
+
+testBkpText :: (Globals, [Formatter]) -> Test
+testBkpText (gs, fs) = TestCase $
+   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+      "book publisher suffix requested and present in text"
+      expected
+   where
+      testOpts = (gOpts gs) { optPublisher = True }
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing
+            Nothing "Herman Melville"]
+         , metaContributors = [Creator (Just "bkp") Nothing
+            Nothing "Acme Publishing, Inc."]
+         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "MelvilleHerman-MobyDick.epub"
+         )
+
+
+testBkpMissing :: (Globals, [Formatter]) -> Test
+testBkpMissing (gs, fs) = TestCase $
+   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+      "book publisher suffix requested and not present"
+      expected
+   where
+      testOpts = (gOpts gs) { optPublisher = True }
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing
+            Nothing "Herman Melville"]
+         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
+         }
+      expected =
+         ( "ordinary_book"
+         , "MelvilleHerman-MobyDick.epub"
+         )
+
+
+testMagUniverse :: (Globals, [Formatter]) -> Test
+testMagUniverse (gs, fs) = TestCase $
+   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+      "Jim Baen's Universe Magazine" expected
+   where
+      testOpts = (gOpts gs) { optPublisher = True }
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing
+            Nothing "Jim Baen's Universe"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Jim Baen's Universe-Vol 4 Num 6"]
+         }
+      expected =
+         ( "magGenericVolNo2"
+         , "JimBaensUniverse-Vol04No06.epub"
+         )
+
+
+testMagClarkesworld :: (Globals, [Formatter]) -> Test
+testMagClarkesworld (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Clarkesworld Magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut")
+            (Just "Kowal, Mary Robinette") Nothing
+            "Mary Robinette Kowal"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Clarkesworld Magazine - Issue 21"]
+         }
+      expected =
+         ( "magClarkesworld"
+         , "Clarkesworld021.epub"
+         )
+
+
+testLightspeedDate :: (Globals, [Formatter]) -> Test
+testLightspeedDate (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Lightspeed Magazine, date in title" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") Nothing Nothing
+            "Edited by John Joseph Adams"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Lightspeed Magazine, June 2010"]
+         }
+      expected =
+         ( "magLightspeed_date"
+         , "Lightspeed2010-06.epub"
+         )
+
+
+testLightspeedMagIssue :: (Globals, [Formatter]) -> Test
+testLightspeedMagIssue (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Lightspeed Magazine, issue number in title" expected
+   where
+      meta = emptyMetadata
+         { metaCreators = [Creator (Just "aut") (Just "Magazine, Lightspeed & Clark, Maggie & Valentine, Genevieve & Baxter, Stephen & Okorafor, Nnedi & Wilison, Daniel H. & Reed, Robert & Sedia, Ekaterina") Nothing "Lightspeed Magazine"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Lightspeed Magazine Issue 10"]
+         }
+      expected =
+         ( "magLightspeed_issue"
+         , "Lightspeed010.epub"
+         )
+
+
+testLightspeedIssue :: (Globals, [Formatter]) -> Test
+testLightspeedIssue (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Lightspeed Magazine, issue number in title" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing
+            "Lightspeed Issue 33"]
+         }
+      expected =
+         ( "magLightspeed_issue"
+         , "Lightspeed033.epub"
+         )
+
+
+testLightspeedMagIssueDate :: (Globals, [Formatter]) -> Test
+testLightspeedMagIssueDate (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Lightspeed Magazine, both issue and date in title" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing
+            "Lightspeed Magazine, Issue 62 (July 2015)"]
+         }
+      expected =
+         ( "magLightspeed_issue"
+         , "Lightspeed062.epub"
+         )
+
+
+testMagWeirdTales :: (Globals, [Formatter]) -> Test
+testMagWeirdTales (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Weird Tales magazine" expected
+   where
+      meta = emptyMetadata
+         { metaCreators =
+            [ Creator (Just "aut") Nothing Nothing "VanderMeer"
+            , Creator (Just "aut") Nothing Nothing "Ann"
+            , Creator (Just "aut") Nothing Nothing "Spinrad"
+            , Creator (Just "aut") Nothing Nothing "Norman"
+            ]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Weird Tales #350"]
+         }
+      expected =
+         ( "magWeirdTales"
+         , "WeirdTales350.epub"
+         )
+
+
+testMagGalaxysEdge :: (Globals, [Formatter]) -> Test
+testMagGalaxysEdge (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Galaxy's Edge magazine" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing
+            "Galaxy's Edge Magazine: Issue 1 March 2013"]
+         }
+      expected =
+         ( "magGenericWithIssue"
+         , "GalaxysEdgeMagazine01.epub"
+         )
+
+
+testMagPlasmaFreq :: (Globals, [Formatter]) -> Test
+testMagPlasmaFreq (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Plasma Frequency magazine" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing
+            "Plasma Frequency Magazine Issue 1"]
+         }
+      expected =
+         ( "magGenericWithIssue"
+         , "PlasmaFrequencyMagazine01.epub"
+         )
+
+
+testMagPlasmaFreqMonth :: (Globals, [Formatter]) -> Test
+testMagPlasmaFreqMonth (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Plasma Frequency magazine, incl month/year in title" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing
+            "Plasma Frequency Magazine: Issue 8 October/November 2013"]
+         }
+      expected =
+         ( "magGenericWithIssue"
+         , "PlasmaFrequencyMagazine08.epub"
+         )
+
+
+testMagPunchinello1800s :: (Globals, [Formatter]) -> Test
+testMagPunchinello1800s (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "Punchinello magazine from the 1800s" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing
+            "Punchinello, Volume 1, No. 06, May 7, 1870"]
+         }
+      expected =
+         ( "magGenericVolNo2"
+         , "PunchinelloVol01No06.epub"
+         )
+
+
+testMagGenericVolNo3 :: (Globals, [Formatter]) -> Test
+testMagGenericVolNo3 (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "generic magazine with volume and 3-digit number" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing
+            "Blackwood's Edinburgh Magazine Volume 53, No. 327, January, 1843"]
+         }
+      expected =
+         ( "magGenericVolNo3"
+         , "BlackwoodsEdinburghMagazineVol53No327.epub"
+         )
+
+
+testMagSubjWithIssue :: (Globals, [Formatter]) -> Test
+testMagSubjWithIssue (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "with magazine subject and issue" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing "The Dark Issue 1"]
+         , metaCreators =
+            [ Creator (Just "aut") (Just "Okorafor, Nnedi & Swirsky, Rachel & Slatter, Angela & Hannett, Lisa L.") Nothing
+               "Nnedi Okorafor"
+            , Creator (Just "aut") Nothing Nothing
+               "Rachel Swirsky"
+            ]
+         , metaDates = Map.fromList [(Created, DateValue "2013-10-01")]
+         , metaSubjects =
+            [ "magazine"
+            , "horror"
+            , "dark fantasy"
+            ]
+         }
+      expected =
+         ( "magGenericSubjWithIssue"
+         , "TheDarkMagazine01.epub"
+         )
+
+
+testWomensInstituteLib :: (Globals, [Formatter]) -> Test
+testWomensInstituteLib (gs, fs) = TestCase $
+   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
+      "Women's Institute Library" expected
+   where
+      testOpts = (gOpts gs) { optPublisher = True }
+      meta = emptyMetadata
+         { metaCreators = [Creator Nothing
+            (Just "Woman's Institute of Domestic Arts and Sciences")
+            Nothing "Woman's Institute of Domestic Arts and Sciences"]
+         , metaTitles = [Title Nothing Nothing Nothing
+            "Woman's Institute Library of Cookery\nVolume 1: Essentials of Cookery; Cereals; Bread; Hot Breads"]
+         }
+      expected =
+         ( "nonficWomensInstituteLibrary"
+         , "WomansInstituteLibraryOfCookery_Volume1_EssentialsOfCookeryCerealsBreadHotBreads.epub"
+         )
+
+
+testAnthologyDate :: (Globals, [Formatter]) -> Test
+testAnthologyDate (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs
+      "anthology with date in title" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing "Flash Fiction Online - June 2015"]
+         , metaCreators =
+            [ Creator (Just "art") (Just "Bijelac, Dario") Nothing
+               "Dario Bijelac"
+            , Creator (Just "aut") (Just "Pearlman, Laura") Nothing
+               "Laura Pearlman"
+            ]
+         , metaDates = Map.fromList [(Epub, DateValue "2015-05-30T18:47:28.000000+00:00")]
+         , metaSubjects = ["anthology"]
+         }
+      expected =
+         ( "anthology_date"
+         , "FlashFictionOnline2015-06.epub"
+         )
+
+
+testAnthology :: (Globals, [Formatter]) -> Test
+testAnthology (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "anthology" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing "Creepy Secrets"]
+         , metaCreators =
+            [ Creator (Just "aut") (Just "Snively, Mortimer") Nothing
+               "Mortimer Snively"
+            , Creator (Just "aut") (Just "Baxter, Joanne") Nothing
+               "Joanne Baxter"
+            ]
+         , metaDates = Map.fromList [(Created, DateValue "2010-11-01")]
+         , metaSubjects = ["fantasy and horror anthology"]
+         }
+      expected =
+         ( "anthology"
+         , "CreepySecrets_2010.epub"
+         )
+
+
+testMagLunaStation :: (Globals, [Formatter]) -> Test
+testMagLunaStation (gs, fs) = TestCase $
+   assertNewName gs { gMetadata = meta } fs "Luna Station Quarterly magazine" expected
+   where
+      meta = emptyMetadata
+         { metaTitles = [Title Nothing Nothing Nothing
+            "Luna Station Quarterly - Issue 028"]
+         }
+      expected =
+         ( "magLunaStationQuarterly"
+         , "LunaStationQuarterly028.epub"
+         )
diff --git a/src/tests/EpubTools/Test/EpubName/PubYear.hs b/src/tests/EpubTools/Test/EpubName/PubYear.hs
new file mode 100644
--- /dev/null
+++ b/src/tests/EpubTools/Test/EpubName/PubYear.hs
@@ -0,0 +1,32 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module EpubTools.Test.EpubName.PubYear
+   ( pubYearTests
+   )
+   where
+
+import Test.HUnit ( Test (..), assertEqual )
+
+import Codec.Epub.Data.Metadata ( DateValue (..) )
+import EpubTools.EpubName.Format.PubYear
+
+
+pubYearTests :: Test
+pubYearTests = TestLabel "PubYear" $ TestList
+   [ testExtractYear "2001-03-25T00:00:00" "2001"
+   , testExtractYear "2007" "2007"
+   , testExtractYear "Sat Nov 03 18:49:50 +0100 2007" "2007"
+   , testExtractYear "2001-03-25 05:00:00+00:00" "2001"
+   , testExtractYear "2009-11-19T07:00:00+00:00" "2009"
+   , testExtractYear "2010-09-21" "2010"
+   , testExtractYear "2010-02-14T21:47:47.682882+00:00" "2010"
+   , testExtractYear "2011-11-16T09:19:50-0500" "2011"
+   , testExtractYear "2011-11-16T14:19:52Z" "2011"
+   , testExtractYear "Tue, 29 Nov 2011 06:21:19 +0000" "2011"
+   ]
+
+
+testExtractYear :: String -> String -> Test
+testExtractYear dateStr expected = TestCase $
+   assertEqual dateStr (extractYear . DateValue $ dateStr) (Just expected)
diff --git a/src/tests/test-epubname.hs b/src/tests/test-epubname.hs
new file mode 100644
--- /dev/null
+++ b/src/tests/test-epubname.hs
@@ -0,0 +1,45 @@
+-- License: ISC (see LICENSE)
+-- Author: Dino Morelli <dino@ui3.info>
+
+module Main
+   where
+
+import Control.Monad.Except
+import System.Exit
+import Test.HUnit ( Counts (..), Test (..), runTestTT )
+
+import EpubTools.EpubName.Format.Format
+import EpubTools.EpubName.Main
+import EpubTools.EpubName.Opts
+import EpubTools.Test.EpubName.Format
+import EpubTools.Test.EpubName.PubYear
+
+
+main :: IO ()
+main = do
+   dos <- defaultOptions
+   let testOpts = dos  -- Can use this to adjust the options with dos { ... }
+   ir <- runExceptT $ initialize testOpts
+   either (exitWith) (runTests testOpts) ir
+   
+
+runTests :: Options -> [Formatter] -> IO ()
+runTests opts fs = do
+   counts <- runTestTT $ tests opts fs
+   exit $ testsPassed counts
+
+
+exit :: Bool -> IO ()
+exit True  = exitWith ExitSuccess
+exit False = exitWith $ ExitFailure 1
+
+
+testsPassed :: Counts -> Bool
+testsPassed (Counts _ _ e f) = (e == 0) && (f == 0)
+
+
+tests :: Options -> [Formatter] -> Test
+tests opts fs = TestList
+   [ formatTests opts fs
+   , pubYearTests
+   ]
diff --git a/stack.yaml b/stack.yaml
--- a/stack.yaml
+++ b/stack.yaml
@@ -1,3 +1,7 @@
-resolver: lts-10.0
+resolver: lts-20.12
+
 packages:
 - .
+
+extra-deps:
+- epub-metadata-5.1
diff --git a/testsuite/EpubTools/Test/EpubName/Format.hs b/testsuite/EpubTools/Test/EpubName/Format.hs
deleted file mode 100644
--- a/testsuite/EpubTools/Test/EpubName/Format.hs
+++ /dev/null
@@ -1,1632 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.Test.EpubName.Format
-   ( formatTests
-   )
-   where
-
-import Codec.Epub.Data.Metadata
-import Codec.Epub.Data.Package
-import Control.Monad.Except
-import Test.HUnit ( Test (..), assertEqual )
-import Test.HUnit.Base ( Assertion )
-
-import EpubTools.EpubName.Format.Format
-import EpubTools.EpubName.Format.Util
-import EpubTools.EpubName.Opts
-
-
-pkg2, pkg3 :: Package
-pkg2 = Package "2.01" ""
-pkg3 = Package "3.0" ""
-
-
-formatTests :: Options -> [Formatter] -> Test
-formatTests opts fs = TestLabel "Format" $ TestList $
-   map (\f -> f ((Globals opts pkg2 emptyMetadata), fs))
-      [ testAuthorMinimal
-      , testAuthorOneName
-      , testAuthorRole
-      , testAuthorFileas
-      , testAuthorFileasParens
-      , testAuthorFull
-      , testAuthorMiddle
-      , testMultiAutCreators
-      , testMultiAutOneString
-      , testNoAuthor
-      , testColon
-      , testCreatorsNoAuthor
-      , testCreatorsNoAuthorPubDate
-      , testCreatorLastFirst
-      , testTitleCaps
-      , testTitleBracket
-      , testTitleNone
-      , testTitleMultiline
-      , testTitleHyphen
-      , testTitleHyphenDates
-      , testTitleRomanNum
-      , testAllPunctuation
-      , testPubYearNoDatesPresent
-      , testPubYearEpub3
-      , testPubYearEpub3Unwanted
-      , testPubYearEpub2
-      , testPubYearEpub2Any
-      , testPubYearEpub2Orig
-      , testPubYearEpub2Unwanted
-      , testMagAeon
-      , testMagAEon
-      , testMagApexPound
-      , testMagApexIssue
-      , testMagApexLong
-      , testChallengingDestinyShort
-      , testChallengingDestinyLong
-      , testAnalogSingle
-      , testAnalogDouble
-      , testAsimovs
-      , testFantasyMagazine
-      , testFsfShort
-      , testFsfShortComma
-      , testFsfLong
-      , testFsfAmpersand
-      , testFsfAmpersandSpaces
-      , testFsfVeryLong
-      , testMagFutureOrbits
-      , testGudShort
-      , testGudLong
-      , testGudVeryLong
-      , testInterzone
-      , testInterzoneCaps
-      , testInterzoneOldLong
-      , testInterzoneOldShort
-      , testNemesisShort
-      , testNemesisLong
-      , testMagSomethingWicked
-      , testMagSomethingWickedMonth
-      , testSFBestOf
-      , testBestSF
-      , testYearsBest
-      , testMagBlackStatic
-      , testRageMachineMag
-      , testEclipseMagWord
-      , testEclipseMagNum
-      , testBcs
-      , testBkpFileAs
-      , testBkpText
-      , testBkpMissing
-      , testMagUniverse
-      , testMagClarkesworld
-      , testLightspeedDate
-      , testLightspeedMagIssue
-      , testLightspeedIssue
-      , testLightspeedMagIssueDate
-      , testMagWeirdTales
-      , testMagGalaxysEdge
-      , testMagPlasmaFreq
-      , testMagPlasmaFreqMonth
-      , testMagPunchinello1800s
-      , testMagGenericVolNo3
-      , testMagSubjWithIssue
-      , testWomensInstituteLib
-      , testAnthologyDate
-      , testAnthology
-      , testMagLunaStation
-      ]
-
-
-assertNewName :: Globals -> [Formatter] -> String
-   -> (String, String) -> Assertion
-assertNewName globals fs desc expected = do
-   result <- runExceptT $ tryFormatting globals fs ""
-   let actual = either (\em -> ("NO FORMATTER", em)) id result
-   assertEqual desc expected actual
-
-
-testAuthorMinimal :: (Globals, [Formatter]) -> Test
-testAuthorMinimal (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "minimal author" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testAuthorOneName :: (Globals, [Formatter]) -> Test
-testAuthorOneName (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "author is a single word" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "Melville-MobyDick.epub"
-         )
-
-
-testAuthorRole :: (Globals, [Formatter]) -> Test
-testAuthorRole (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "author with role" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testAuthorFileas :: (Globals, [Formatter]) -> Test
-testAuthorFileas (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "author with file-as" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing
-            (Just "Melville, Herman")
-            Nothing
-            "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testAuthorFileasParens :: (Globals, [Formatter]) -> Test
-testAuthorFileasParens (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "author with file-as and pesky parenthesized full names" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing
-            (Just "Fletcher, J. S. (Joseph Smith)")
-            Nothing
-            "J. S. Fletcher"]
-         , metaTitles = [Title Nothing Nothing Nothing "The Middle of Things"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "FletcherJS-TheMiddleOfThings.epub"
-         )
-
-
-testAuthorFull :: (Globals, [Formatter]) -> Test
-testAuthorFull (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "author fully filled out" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Melville, Herman")
-            Nothing
-            "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testAuthorMiddle :: (Globals, [Formatter]) -> Test
-testAuthorMiddle (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "author with middle-name" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Wallace, David Foster")
-            Nothing
-            "David Foster Wallace"]
-         , metaTitles = [Title Nothing Nothing Nothing "Infinite Jest"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "WallaceDavidFoster-InfiniteJest.epub"
-         )
-
-
-testMultiAutCreators :: (Globals, [Formatter]) -> Test
-testMultiAutCreators (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "several authors" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "aut") Nothing
-               Nothing "James Patrick Kelly"
-            , Creator (Just "aut") (Just "Kessel, John")
-               Nothing "John Kessel"
-            , Creator (Just "aut") Nothing
-               Nothing "Jonathan Lethem"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Ninety Percent of Everything"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "Kelly_Kessel_Lethem-NinetyPercentOfEverything.epub"
-         )
-
-
-testMultiAutOneString :: (Globals, [Formatter]) -> Test
-testMultiAutOneString (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "more than one author separated by & and/or and" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Yvonne Q. Anderson & Eva Tunglewacker and Jefferson Milner"]
-         , metaTitles = [Title Nothing Nothing Nothing "Big Trouble"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "Anderson_Tunglewacker_Milner-BigTrouble.epub"
-         )
-
-
-testNoAuthor :: (Globals, [Formatter]) -> Test
-testNoAuthor (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "no creator(s) at all" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = []
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Some Collection of Fine Stories, Volume 1"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SomeCollectionOfFineStoriesVolume1.epub"
-         )
-
-
-testCreatorsNoAuthor :: (Globals, [Formatter]) -> Test
-testCreatorsNoAuthor (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "creators, but no author(s) at all" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "edt") Nothing
-               Nothing "Graham Spindlewest"
-            , Creator (Just "ill") Nothing
-               Nothing "Eva Tunglewacker"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Some Collection of Fine Stories, Volume 1"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SomeCollectionOfFineStoriesVolume1.epub"
-         )
-
-
-testColon :: (Globals, [Formatter]) -> Test
-testColon (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "colon becomes underscore" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Ed Howdershelt"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Book 1: 3rd World Products, Inc."]
-         }
-      expected =
-         ( "ordinary_book"
-         , "HowdersheltEd-Book1_3rdWorldProductsInc.epub"
-         )
-
-
-testCreatorsNoAuthorPubDate :: (Globals, [Formatter]) -> Test
-testCreatorsNoAuthorPubDate (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "creators, but no author(s) at all, with pub date" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "edt") Nothing
-               Nothing "Graham Spindlewest"
-            , Creator (Just "ill") Nothing
-               Nothing "Eva Tunglewacker"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Some Collection of Fine Stories, Volume 1"]
-         , metaDates = [Date (Just "publication")
-            "2008"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SomeCollectionOfFineStoriesVolume1_2008.epub"
-         )
-
-
-testCreatorLastFirst :: (Globals, [Formatter]) -> Test
-testCreatorLastFirst (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "creator text (not file-as) is last-name-first" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator Nothing Nothing
-               Nothing "Spindlewest, Graham"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "A Midnight's Horror Story"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SpindlewestGraham-AMidnightsHorrorStory.epub"
-         )
-
-
-testTitleCaps :: (Globals, [Formatter]) -> Test
-testTitleCaps (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "title all caps" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Greg Bear"]
-         , metaTitles = [Title Nothing Nothing Nothing "EON"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "BearGreg-Eon.epub"
-         )
-
-
-testTitleBracket :: (Globals, [Formatter]) -> Test
-testTitleBracket (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "title with brackets" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Mercedes Lackey"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "SKitty [Shipscat series #1]"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "LackeyMercedes-Skitty_ShipscatSeries1.epub"
-         )
-
-
-testTitleNone :: (Globals, [Formatter]) -> Test
-testTitleNone (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "missing title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Nobody McCrankypants"]
-         }
-      expected =
-         ( "NO FORMATTER"
-         , " [ERROR No formatter found]"
-         )
-
-
-testTitleMultiline :: (Globals, [Formatter]) -> Test
-testTitleMultiline (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "multiline title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator
-            Nothing
-            (Just "Castleman, Virginia Carter")
-            Nothing
-            "Virginia Carter Castleman"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Pocahontas.\nA Poem"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "CastlemanVirginiaCarter-Pocahontas_APoem.epub"
-         )
-
-
-testTitleHyphen :: (Globals, [Formatter]) -> Test
-testTitleHyphen (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "title with hyphen" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator
-            Nothing
-            (Just "Spinoza, Benedictus de")
-            Nothing
-            "Benedictus de Spinoza"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Theologico-Political Treatise"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "SpinozaBenedictusDe-Theologico-PoliticalTreatise.epub"
-         )
-
-
-testTitleHyphenDates :: (Globals, [Formatter]) -> Test
-testTitleHyphenDates (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "title with hyphen separating dates" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator
-            Nothing
-            (Just "Anonymous")
-            Nothing
-            "Anonymous"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Queen Victoria\nStory of Her Life and Reign, 1819-1901"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "Anonymous-QueenVictoria_StoryOfHerLifeAndReign1819-1901.epub"
-         )
-
-testTitleRomanNum :: (Globals, [Formatter]) -> Test
-testTitleRomanNum (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "title with Roman numerals" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator
-            Nothing
-            (Just "Hauptmann, Gerhart")
-            Nothing
-            "Gerhart Hauptmann"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "The Dramatic Works of Gerhart Hauptmann\nVolume III"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "HauptmannGerhart-TheDramaticWorksOfGerhartHauptmann_VolumeIII.epub"
-         )
-
-
-testAllPunctuation :: (Globals, [Formatter]) -> Test
-testAllPunctuation (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "big test of all punctuation" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Dino Morelli"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "The *crazy*: Sand-box. Of Smedley's discontent/loathing, fear & Malnourishment? (Maybe not!); [Part #2]"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MorelliDino-TheCrazy_Sand-BoxOfSmedleysDiscontentLoathingFearAndMalnourishmentMaybeNot_Part2.epub"
-         )
-
-
-testPubYearNoDatesPresent :: (Globals, [Formatter]) -> Test
-testPubYearNoDatesPresent (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta} fs
-      "book with no dates at all" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory.epub"
-         )
-
-
-testPubYearEpub3 :: (Globals, [Formatter]) -> Test
-testPubYearEpub3 (gs, fs) = TestCase $
-   assertNewName gs { gPackage = pkg3, gMetadata = meta} fs
-      "book with epub3 style (simple) date" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = [ Date Nothing "2003" ]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory_2003.epub"
-         )
-
-
-testPubYearEpub3Unwanted :: (Globals, [Formatter]) -> Test
-testPubYearEpub3Unwanted (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gPackage = pkg3,
-      gMetadata = meta } fs
-      "epub3 book with a publication year but we don't want"
-      expected
-   where
-      testOpts = (gOpts gs) { optPubYear = NoDate }
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates =
-            [ Date Nothing "2001"
-            , Date (Just "some date") "2000"
-            ]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory.epub"
-         )
-
-
-testPubYearEpub2 :: (Globals, [Formatter]) -> Test
-testPubYearEpub2 (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta} fs
-      "book with epub2 style (simple) date" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = [ Date Nothing "2003" ]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory.epub"
-         )
-
-
-testPubYearEpub2Any :: (Globals, [Formatter]) -> Test
-testPubYearEpub2Any (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta} fs
-      "book with epub2 style (simple) date, any switch" expected
-   where
-      testOpts = (gOpts gs) { optPubYear = AnyDate }
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates = [ Date Nothing "2003" ]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory_2003.epub"
-         )
-
-
-testPubYearEpub2Orig :: (Globals, [Formatter]) -> Test
-testPubYearEpub2Orig (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "book with epub2 style dates, orig attr" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates =
-            [ Date Nothing "2001"
-            , Date (Just "publication") "2003"
-            , Date (Just "some date") "2000"
-            , Date (Just "original-publication") "2002"
-            ]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory_2002.epub"
-         )
-
-
-testPubYearEpub2Unwanted :: (Globals, [Formatter]) -> Test
-testPubYearEpub2Unwanted (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "epub2 book with a publication year but we don't want"
-      expected
-   where
-      testOpts = (gOpts gs) { optPubYear = NoDate }
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Jim Jones"]
-         , metaTitles = [Title Nothing Nothing Nothing "A Timeless Story"]
-         , metaDates =
-            [ Date Nothing "2001"
-            , Date (Just "publication") "2003"
-            , Date (Just "some date") "2000"
-            , Date (Just "original-publication") "2002"
-            ]
-         }
-      expected =
-         ( "ordinary_book"
-         , "JonesJim-ATimelessStory.epub"
-         )
-
-
-testMagAeon :: (Globals, [Formatter]) -> Test
-testMagAeon (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Aeon magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "Aeon Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing "Aeon Eight"]
-         }
-      expected =
-         ( "magAeon"
-         , "AeonMagazine08.epub"
-         )
-
-
-testMagAEon :: (Globals, [Formatter]) -> Test
-testMagAEon (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "AEon magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing "AEon Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing "Aeon Thirteen"]
-         }
-      expected =
-         ( "magAeon"
-         , "AeonMagazine13.epub"
-         )
-
-
-testMagApexPound :: (Globals, [Formatter]) -> Test
-testMagApexPound (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Apex Magazine, pound sign and issue number" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Apex Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Apex Magazine #16"]
-         }
-      expected =
-         ( "magApex"
-         , "ApexMagazine016.epub"
-         )
-
-
-testMagApexIssue :: (Globals, [Formatter]) -> Test
-testMagApexIssue (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Apex Magazine, Issue number" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Apex Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Apex Magazine Issue 17"]
-         }
-      expected =
-         ( "magApex"
-         , "ApexMagazine017.epub"
-         )
-
-
-testMagApexLong :: (Globals, [Formatter]) -> Test
-testMagApexLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Apex Magazine, title with month year" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Apex Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Apex Magazine: Issue 101, October 2017"]
-         }
-      expected =
-         ( "magApex"
-         , "ApexMagazine101.epub"
-         )
-
-
-testChallengingDestinyShort :: (Globals, [Formatter]) -> Test
-testChallengingDestinyShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Challenging Destiny Magazine, short title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Crystalline Sphere Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Challenging Destiny #23"]
-         }
-      expected =
-         ( "magChallengingDestiny"
-         , "ChallengingDestinyMagazine023.epub"
-         )
-
-
-testChallengingDestinyLong :: (Globals, [Formatter]) -> Test
-testChallengingDestinyLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Challenging Destiny Magazine, long title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Crystalline Sphere Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Challenging Destiny #24: August 2007"]
-         }
-      expected =
-         ( "magChallengingDestiny"
-         , "ChallengingDestinyMagazine024.epub"
-         )
-
-
-testAnalogSingle :: (Globals, [Formatter]) -> Test
-testAnalogSingle (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Analog, single month" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Dell Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Analog SFF, November 2010"]
-         }
-      expected =
-         ( "magAnalog"
-         , "AnalogSF2010-11.epub"
-         )
-
-
-testAnalogDouble :: (Globals, [Formatter]) -> Test
-testAnalogDouble (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Analog, double month" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Dell Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Analog SFF, July-August 2003"]
-         }
-      expected =
-         ( "magAnalog"
-         , "AnalogSF2003-07_08.epub"
-         )
-
-
-testAsimovs :: (Globals, [Formatter]) -> Test
-testAsimovs (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Asimovs" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Dell Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Asimov's SF, August 2003"]
-         }
-      expected =
-         ( "magAnalog"
-         , "AsimovsSF2003-08.epub"
-         )
-
-testFantasyMagazine :: (Globals, [Formatter]) -> Test
-testFantasyMagazine (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Fantasy Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "aut") (Just "Howard, Kat & Tanzer, Molly & Beagle, Peter S. & Howard, Jonathan L. & Valentine, Genevieve & Vaughn, Carrie & Pilinovsky, Helen") Nothing "Kat Howard"
-            , Creator (Just "aut") Nothing Nothing "Molly Tanzer"
-            , Creator (Just "aut") Nothing Nothing "Peter S. Beagle"
-            , Creator (Just "aut") Nothing Nothing "Jonathan L. Howard"
-            , Creator (Just "aut") Nothing Nothing "Genevieve Valentine"
-            , Creator (Just "aut") Nothing Nothing "Carrie Vaughn"
-            , Creator (Just "aut") Nothing Nothing "Helen Pilinovsky"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Fantasy Magazine Issue 49"]
-         }
-      expected =
-         ( "magFantasyMag"
-         , "FantasyMagazine049.epub"
-         )
-
-
-testFsfShort :: (Globals, [Formatter]) -> Test
-testFsfShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, short" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing "FSF Oct/Nov 2003"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2003-10_11.epub"
-         )
-
-
-testFsfShortComma :: (Globals, [Formatter]) -> Test
-testFsfShortComma (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, short, comma" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing "FSF, April 2008"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2008-04.epub"
-         )
-
-
-testFsfLong :: (Globals, [Formatter]) -> Test
-testFsfLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Spilogale Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "FSF Magazine, April 2006"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2006-04.epub"
-         )
-
-
-testFsfAmpersand :: (Globals, [Formatter]) -> Test
-testFsfAmpersand (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, ampersand" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale"]
-         , metaTitles = [Title Nothing Nothing Nothing "F&SF, Apr 2004"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2004-04.epub"
-         )
-
-
-testFsfAmpersandSpaces :: (Globals, [Formatter]) -> Test
-testFsfAmpersandSpaces (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, ampersand, spaces" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale"]
-         , metaTitles = [Title Nothing Nothing Nothing "F & SF Dec 2003"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2003-12.epub"
-         )
-
-
-testFsfVeryLong :: (Globals, [Formatter]) -> Test
-testFsfVeryLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "FSF Magazine, very long name" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing
-            Nothing "Spilogale Inc."]
-         , metaTitles = [Title Nothing Nothing Nothing "Fantasy & Science Fiction, November/December 2017"]
-         }
-      expected =
-         ( "magFsf"
-         , "FantasyScienceFiction2017-11_12.epub"
-         )
-
-
-testMagFutureOrbits :: (Globals, [Formatter]) -> Test
-testMagFutureOrbits (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "testMagFutureOrbits" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Vander Neut Publications, L.L.C."]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Future Orbits Issue 5, June/July 2002"]
-         }
-      expected =
-         ( "magFutureOrbits"
-         , "FutureOrbitsMagazine05_2002-06_07.epub"
-         )
-
-
-testGudShort :: (Globals, [Formatter]) -> Test
-testGudShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Gud Magazine, short" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "GUD Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "GUD Magazine Issue 0 :: Spring 2007"]
-         }
-      expected =
-         ( "magGud"
-         , "GUDMagazine00.epub"
-         )
-
-
-testGudLong :: (Globals, [Formatter]) -> Test
-testGudLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Gud Magazine, long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "GUD Magazine Authors, Jeff Somers, Jeremy Shipp"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "GUD Magazine Issue 2 :: Spring 2008"]
-         }
-      expected =
-         ( "magGud"
-         , "GUDMagazine02.epub"
-         )
-
-
-testGudVeryLong :: (Globals, [Formatter]) -> Test
-testGudVeryLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Gud Magazine, very long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "GUD Magazine Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Greatest Uncommon Denominator Magazine Issue 4 :: Spring 2009"]
-         }
-      expected =
-         ( "magGud"
-         , "GUDMagazine04.epub"
-         )
-
-
-testInterzone :: (Globals, [Formatter]) -> Test
-testInterzone (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Interzone Magazine, Smashwords style" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "TTA Press"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Interzone 233 Mar - Apr 2011"]
-         }
-      expected =
-         ( "magInterzone"
-         , "InterzoneSFF233.epub"
-         )
-
-
-testInterzoneCaps :: (Globals, [Formatter]) -> Test
-testInterzoneCaps (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Interzone Magazine, started in 2014" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing (Just "Interzone 249")
-            Nothing "TTA Press"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "INTERZONE #249 SCIENCE FICTION & FANTASY MAGAZINE (NOVâ\x80\x93DEC 2013)"]
-         }
-      expected =
-         ( "magInterzone"
-         , "InterzoneSFF249.epub"
-         )
-
-
-testInterzoneOldLong :: (Globals, [Formatter]) -> Test
-testInterzoneOldLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Interzone Magazine, old style, long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "TTA Press Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Interzone Science Fiction and Fantasy Magazine #216"]
-         }
-      expected =
-         ( "magInterzone"
-         , "InterzoneSFF216.epub"
-         )
-
-
-testInterzoneOldShort :: (Globals, [Formatter]) -> Test
-testInterzoneOldShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Interzone Magazine, old style, short" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "TTA Press Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing 
-            "Interzone SFF #212"]
-         }
-      expected =
-         ( "magInterzone"
-         , "InterzoneSFF212.epub"
-         )
-
-
-testNemesisShort :: (Globals, [Formatter]) -> Test
-testNemesisShort (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Nemesis Magazine, short" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Stephen Adams"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Nemesis Magazine #2"]
-         }
-      expected =
-         ( "magNemesis"
-         , "NemesisMag002.epub"
-         )
-
-
-testNemesisLong :: (Globals, [Formatter]) -> Test
-testNemesisLong (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Nemesis Magazine, long" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Stephen Adams"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Nemesis Magazine #7: Featuring Victory Rose in Death Stalks the Ruins"]
-         }
-      expected =
-         ( "magNemesis"
-         , "NemesisMag007.epub"
-         )
-
-
-testMagSomethingWicked :: (Globals, [Formatter]) -> Test
-testMagSomethingWicked (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Something Wicked Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing Nothing
-            "Something Wicked Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Something Wicked SF and Horror Magazine #5"]
-         }
-      expected =
-         ( "magSomethingWicked"
-         , "SomethingWicked005.epub"
-         )
-
-
-testMagSomethingWickedMonth :: (Globals, [Formatter]) -> Test
-testMagSomethingWickedMonth (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Something Wicked Magazine, month in title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Authors, Something Wicked")
-            Nothing
-            "Something Wicked Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Something Wicked #14 (October 2011)"]
-         }
-      expected =
-         ( "magSomethingWicked"
-         , "SomethingWicked014.epub"
-         )
-
-
-testSFBestOf :: (Globals, [Formatter]) -> Test
-testSFBestOf (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Science Fiction: The Best of the Year" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "Rich Horton"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Science Fiction: The Best of the Year, 2007 Edition"]
-         , metaSubjects = ["anthology"]
-         }
-      expected =
-         ( "anthology"
-         , "ScienceFiction_TheBestOfTheYear2007Edition.epub"
-         )
-
-
-testBestSF :: (Globals, [Formatter]) -> Test
-testBestSF (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "The Best Science Fiction and Fantasy of the Year" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Strahan, Jonathan")
-            Nothing
-            "Jonathan Strahan"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "The Best Science Fiction and Fantasy of the Year: Volume 2"]
-         , metaSubjects = ["anthology"]
-         }
-      expected =
-         ( "anthology"
-         , "TheBestScienceFictionAndFantasyOfTheYear_Volume2.epub"
-         )
-
-
-testYearsBest :: (Globals, [Formatter]) -> Test
-testYearsBest (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "The Year's Best SF" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing Nothing
-            "Rich Horton, Michael Swanwick, Karen Joy Fowler"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "The Year's Best Science Fiction: 2008 Edition"]
-         , metaSubjects = ["anthology"]
-         }
-      expected =
-         ( "anthology"
-         , "TheYearsBestScienceFiction_2008Edition.epub"
-         )
-
-
-testMagBlackStatic :: (Globals, [Formatter]) -> Test
-testMagBlackStatic (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Black Static Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing Nothing 
-            Nothing "TTA Press Authors"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Black Static Horror Magazine #5"]
-         }
-      expected =
-         ( "magBlackStatic"
-         , "BlackStaticHorrorMagazine05.epub"
-         )
-
-
-testRageMachineMag :: (Globals, [Formatter]) -> Test
-testRageMachineMag (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Rage Machine Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "edt") Nothing 
-            Nothing "G. W. Thomas"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Rage Machine Magazine #1--December 2005"]
-         }
-      expected =
-         ( "magRageMachine"
-         , "RageMachineMagazine1_2005-12.epub"
-         )
-
-
-testEclipseMagWord :: (Globals, [Formatter]) -> Test
-testEclipseMagWord (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Eclipse Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Strahan, Jonathan")
-            Nothing
-            "Jonathan Strahan"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Eclipse One"]
-         }
-      expected =
-         ( "magEclipse_word"
-         , "Eclipse01.epub"
-         )
-
-
-testEclipseMagNum :: (Globals, [Formatter]) -> Test
-testEclipseMagNum (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Eclipse Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Strahan, Jonathan")
-            Nothing
-            "Jonathan Strahan"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Eclipse 4: New Science Fiction and Fantasy"]
-         }
-      expected =
-         ( "magEclipse_num"
-         , "Eclipse04.epub"
-         )
-
-
-testBcs :: (Globals, [Formatter]) -> Test
-testBcs (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Beneath Ceaseless Skies Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = 
-            [ Creator (Just "aut")
-               (Just "Tidwell, Erin A.")
-               Nothing
-               "Hoover, Kenneth Mark"
-            , Creator (Just "aut")
-               (Just "Tidwell, Erin A.")
-               Nothing
-               "Tidwell, Erin A."
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Beneath Ceaseless Skies #32"]
-         }
-      expected =
-         ( "magBcs"
-         , "BeneathCeaselessSkies_Issue032.epub"
-         )
-
-
-testBkpFileAs :: (Globals, [Formatter]) -> Test
-testBkpFileAs (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "book publisher suffix requested and present in file-as"
-      expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Herman Melville"]
-         , metaContributors = [Creator (Just "bkp") (Just "acme") 
-            Nothing "Acme Publishing, Inc."]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick_acme.epub"
-         )
-
-
-testBkpText :: (Globals, [Formatter]) -> Test
-testBkpText (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "book publisher suffix requested and present in text"
-      expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Herman Melville"]
-         , metaContributors = [Creator (Just "bkp") Nothing
-            Nothing "Acme Publishing, Inc."]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testBkpMissing :: (Globals, [Formatter]) -> Test
-testBkpMissing (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "book publisher suffix requested and not present"
-      expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Herman Melville"]
-         , metaTitles = [Title Nothing Nothing Nothing "Moby Dick"]
-         }
-      expected =
-         ( "ordinary_book"
-         , "MelvilleHerman-MobyDick.epub"
-         )
-
-
-testMagUniverse :: (Globals, [Formatter]) -> Test
-testMagUniverse (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "Jim Baen's Universe Magazine" expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing
-            Nothing "Jim Baen's Universe"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Jim Baen's Universe-Vol 4 Num 6"]
-         }
-      expected =
-         ( "magGenericVolNo2"
-         , "JimBaensUniverse-Vol04No06.epub"
-         )
-
-
-testMagClarkesworld :: (Globals, [Formatter]) -> Test
-testMagClarkesworld (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Clarkesworld Magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut")
-            (Just "Kowal, Mary Robinette") Nothing
-            "Mary Robinette Kowal"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Clarkesworld Magazine - Issue 21"]
-         }
-      expected =
-         ( "magClarkesworld"
-         , "Clarkesworld021.epub"
-         )
-
-
-testLightspeedDate :: (Globals, [Formatter]) -> Test
-testLightspeedDate (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Lightspeed Magazine, date in title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") Nothing Nothing
-            "Edited by John Joseph Adams"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Lightspeed Magazine, June 2010"]
-         }
-      expected =
-         ( "magLightspeed_date"
-         , "Lightspeed2010-06.epub"
-         )
-
-
-testLightspeedMagIssue :: (Globals, [Formatter]) -> Test
-testLightspeedMagIssue (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Lightspeed Magazine, issue number in title" expected
-   where
-      meta = emptyMetadata
-         { metaCreators = [Creator (Just "aut") (Just "Magazine, Lightspeed & Clark, Maggie & Valentine, Genevieve & Baxter, Stephen & Okorafor, Nnedi & Wilison, Daniel H. & Reed, Robert & Sedia, Ekaterina") Nothing "Lightspeed Magazine"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Lightspeed Magazine Issue 10"]
-         }
-      expected =
-         ( "magLightspeed_issue"
-         , "Lightspeed010.epub"
-         )
-
-
-testLightspeedIssue :: (Globals, [Formatter]) -> Test
-testLightspeedIssue (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Lightspeed Magazine, issue number in title" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Lightspeed Issue 33"]
-         }
-      expected =
-         ( "magLightspeed_issue"
-         , "Lightspeed033.epub"
-         )
-
-
-testLightspeedMagIssueDate :: (Globals, [Formatter]) -> Test
-testLightspeedMagIssueDate (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Lightspeed Magazine, both issue and date in title" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Lightspeed Magazine, Issue 62 (July 2015)"]
-         }
-      expected =
-         ( "magLightspeed_issue"
-         , "Lightspeed062.epub"
-         )
-
-
-testMagWeirdTales :: (Globals, [Formatter]) -> Test
-testMagWeirdTales (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Weird Tales magazine" expected
-   where
-      meta = emptyMetadata
-         { metaCreators =
-            [ Creator (Just "aut") Nothing Nothing "VanderMeer"
-            , Creator (Just "aut") Nothing Nothing "Ann"
-            , Creator (Just "aut") Nothing Nothing "Spinrad"
-            , Creator (Just "aut") Nothing Nothing "Norman"
-            ]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Weird Tales #350"]
-         }
-      expected =
-         ( "magWeirdTales"
-         , "WeirdTales350.epub"
-         )
-
-
-testMagGalaxysEdge :: (Globals, [Formatter]) -> Test
-testMagGalaxysEdge (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Galaxy's Edge magazine" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Galaxy's Edge Magazine: Issue 1 March 2013"]
-         }
-      expected =
-         ( "magGenericWithIssue"
-         , "GalaxysEdgeMagazine01.epub"
-         )
-
-
-testMagPlasmaFreq :: (Globals, [Formatter]) -> Test
-testMagPlasmaFreq (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Plasma Frequency magazine" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Plasma Frequency Magazine Issue 1"]
-         }
-      expected =
-         ( "magGenericWithIssue"
-         , "PlasmaFrequencyMagazine01.epub"
-         )
-
-
-testMagPlasmaFreqMonth :: (Globals, [Formatter]) -> Test
-testMagPlasmaFreqMonth (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Plasma Frequency magazine, incl month/year in title" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Plasma Frequency Magazine: Issue 8 October/November 2013"]
-         }
-      expected =
-         ( "magGenericWithIssue"
-         , "PlasmaFrequencyMagazine08.epub"
-         )
-
-
-testMagPunchinello1800s :: (Globals, [Formatter]) -> Test
-testMagPunchinello1800s (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "Punchinello magazine from the 1800s" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Punchinello, Volume 1, No. 06, May 7, 1870"]
-         }
-      expected =
-         ( "magGenericVolNo2"
-         , "PunchinelloVol01No06.epub"
-         )
-
-
-testMagGenericVolNo3 :: (Globals, [Formatter]) -> Test
-testMagGenericVolNo3 (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "generic magazine with volume and 3-digit number" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Blackwood's Edinburgh Magazine Volume 53, No. 327, January, 1843"]
-         }
-      expected =
-         ( "magGenericVolNo3"
-         , "BlackwoodsEdinburghMagazineVol53No327.epub"
-         )
-
-
-testMagSubjWithIssue :: (Globals, [Formatter]) -> Test
-testMagSubjWithIssue (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "with magazine subject and issue" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing "The Dark Issue 1"]
-         , metaCreators =
-            [ Creator (Just "aut") (Just "Okorafor, Nnedi & Swirsky, Rachel & Slatter, Angela & Hannett, Lisa L.") Nothing
-               "Nnedi Okorafor"
-            , Creator (Just "aut") Nothing Nothing
-               "Rachel Swirsky"
-            ]
-         , metaDates = [Date (Just "publication") "2013-10-01"]
-         , metaSubjects =
-            [ "magazine"
-            , "horror"
-            , "dark fantasy"
-            ]
-         }
-      expected =
-         ( "magGenericSubjWithIssue"
-         , "TheDarkMagazine01.epub"
-         )
-
-
-testWomensInstituteLib :: (Globals, [Formatter]) -> Test
-testWomensInstituteLib (gs, fs) = TestCase $
-   assertNewName gs { gOpts = testOpts, gMetadata = meta } fs
-      "Women's Institute Library" expected
-   where
-      testOpts = (gOpts gs) { optPublisher = True }
-      meta = emptyMetadata
-         { metaCreators = [Creator Nothing
-            (Just "Woman's Institute of Domestic Arts and Sciences")
-            Nothing "Woman's Institute of Domestic Arts and Sciences"]
-         , metaTitles = [Title Nothing Nothing Nothing
-            "Woman's Institute Library of Cookery\nVolume 1: Essentials of Cookery; Cereals; Bread; Hot Breads"]
-         }
-      expected =
-         ( "nonficWomensInstituteLibrary"
-         , "WomansInstituteLibraryOfCookery_Volume1_EssentialsOfCookeryCerealsBreadHotBreads.epub"
-         )
-
-
-testAnthologyDate :: (Globals, [Formatter]) -> Test
-testAnthologyDate (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs
-      "anthology with date in title" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing "Flash Fiction Online - June 2015"]
-         , metaCreators =
-            [ Creator (Just "art") (Just "Bijelac, Dario") Nothing
-               "Dario Bijelac"
-            , Creator (Just "aut") (Just "Pearlman, Laura") Nothing
-               "Laura Pearlman"
-            ]
-         , metaDates = [ Date Nothing "2015-05-30T18:47:28.000000+00:00" ]
-         , metaSubjects = ["anthology"]
-         }
-      expected =
-         ( "anthology_date"
-         , "FlashFictionOnline2015-06.epub"
-         )
-
-
-testAnthology :: (Globals, [Formatter]) -> Test
-testAnthology (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "anthology" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing "Creepy Secrets"]
-         , metaCreators =
-            [ Creator (Just "aut") (Just "Snively, Mortimer") Nothing
-               "Mortimer Snively"
-            , Creator (Just "aut") (Just "Baxter, Joanne") Nothing
-               "Joanne Baxter"
-            ]
-         , metaDates = [Date (Just "publication") "2010-11-01"]
-         , metaSubjects = ["fantasy and horror anthology"]
-         }
-      expected =
-         ( "anthology"
-         , "CreepySecrets_2010.epub"
-         )
-
-
-testMagLunaStation :: (Globals, [Formatter]) -> Test
-testMagLunaStation (gs, fs) = TestCase $
-   assertNewName gs { gMetadata = meta } fs "Luna Station Quarterly magazine" expected
-   where
-      meta = emptyMetadata
-         { metaTitles = [Title Nothing Nothing Nothing
-            "Luna Station Quarterly - Issue 028"]
-         }
-      expected =
-         ( "magLunaStationQuarterly"
-         , "LunaStationQuarterly028.epub"
-         )
diff --git a/testsuite/EpubTools/Test/EpubName/PubYear.hs b/testsuite/EpubTools/Test/EpubName/PubYear.hs
deleted file mode 100644
--- a/testsuite/EpubTools/Test/EpubName/PubYear.hs
+++ /dev/null
@@ -1,31 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module EpubTools.Test.EpubName.PubYear
-   ( pubYearTests
-   )
-   where
-
-import Test.HUnit ( Test (..), assertEqual )
-
-import EpubTools.EpubName.Format.PubYear
-
-
-pubYearTests :: Test
-pubYearTests = TestLabel "PubYear" $ TestList
-   [ testExtractYear "2001-03-25T00:00:00" "2001"
-   , testExtractYear "2007" "2007"
-   , testExtractYear "Sat Nov 03 18:49:50 +0100 2007" "2007"
-   , testExtractYear "2001-03-25 05:00:00+00:00" "2001"
-   , testExtractYear "2009-11-19T07:00:00+00:00" "2009"
-   , testExtractYear "2010-09-21" "2010"
-   , testExtractYear "2010-02-14T21:47:47.682882+00:00" "2010"
-   , testExtractYear "2011-11-16T09:19:50-0500" "2011"
-   , testExtractYear "2011-11-16T14:19:52Z" "2011"
-   , testExtractYear "Tue, 29 Nov 2011 06:21:19 +0000" "2011"
-   ]
-
-
-testExtractYear :: String -> String -> Test
-testExtractYear src expected = TestCase $
-   assertEqual src (extractYear src) (Just expected)
diff --git a/testsuite/EpubTools/test-epubname.hs b/testsuite/EpubTools/test-epubname.hs
deleted file mode 100644
--- a/testsuite/EpubTools/test-epubname.hs
+++ /dev/null
@@ -1,45 +0,0 @@
--- License: ISC (see LICENSE)
--- Author: Dino Morelli <dino@ui3.info>
-
-module Main
-   where
-
-import Control.Monad.Except
-import System.Exit
-import Test.HUnit ( Counts (..), Test (..), runTestTT )
-
-import EpubTools.EpubName.Format.Format
-import EpubTools.EpubName.Main
-import EpubTools.EpubName.Opts
-import EpubTools.Test.EpubName.Format
-import EpubTools.Test.EpubName.PubYear
-
-
-main :: IO ()
-main = do
-   dos <- defaultOptions
-   let testOpts = dos  -- Can use this to adjust the options with dos { ... }
-   ir <- runExceptT $ initialize testOpts
-   either (exitWith) (runTests testOpts) ir
-   
-
-runTests :: Options -> [Formatter] -> IO ()
-runTests opts fs = do
-   counts <- runTestTT $ tests opts fs
-   exit $ testsPassed counts
-
-
-exit :: Bool -> IO ()
-exit True  = exitWith ExitSuccess
-exit False = exitWith $ ExitFailure 1
-
-
-testsPassed :: Counts -> Bool
-testsPassed (Counts _ _ e f) = (e == 0) && (f == 0)
-
-
-tests :: Options -> [Formatter] -> Test
-tests opts fs = TestList
-   [ formatTests opts fs
-   , pubYearTests
-   ]
