bookbinder 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +97 -0
 - data/Rakefile +12 -0
 - data/bin/bookbinder +17 -0
 - data/lib/bookbinder/document_proxy.rb +171 -0
 - data/lib/bookbinder/file.rb +149 -0
 - data/lib/bookbinder/file_system/directory.rb +62 -0
 - data/lib/bookbinder/file_system/memory.rb +57 -0
 - data/lib/bookbinder/file_system/zip_file.rb +106 -0
 - data/lib/bookbinder/file_system.rb +35 -0
 - data/lib/bookbinder/media_type.rb +17 -0
 - data/lib/bookbinder/operations.rb +59 -0
 - data/lib/bookbinder/package/epub.rb +69 -0
 - data/lib/bookbinder/package/openbook.rb +33 -0
 - data/lib/bookbinder/package.rb +295 -0
 - data/lib/bookbinder/transform/epub/audio_overlay.rb +227 -0
 - data/lib/bookbinder/transform/epub/audio_soundtrack.rb +73 -0
 - data/lib/bookbinder/transform/epub/contributor.rb +11 -0
 - data/lib/bookbinder/transform/epub/cover_image.rb +80 -0
 - data/lib/bookbinder/transform/epub/cover_page.rb +148 -0
 - data/lib/bookbinder/transform/epub/creator.rb +67 -0
 - data/lib/bookbinder/transform/epub/description.rb +43 -0
 - data/lib/bookbinder/transform/epub/language.rb +29 -0
 - data/lib/bookbinder/transform/epub/metadata.rb +140 -0
 - data/lib/bookbinder/transform/epub/nav.rb +60 -0
 - data/lib/bookbinder/transform/epub/nav_toc.rb +177 -0
 - data/lib/bookbinder/transform/epub/ncx.rb +63 -0
 - data/lib/bookbinder/transform/epub/ocf.rb +33 -0
 - data/lib/bookbinder/transform/epub/opf.rb +22 -0
 - data/lib/bookbinder/transform/epub/package_identifier.rb +87 -0
 - data/lib/bookbinder/transform/epub/rendition.rb +265 -0
 - data/lib/bookbinder/transform/epub/resources.rb +38 -0
 - data/lib/bookbinder/transform/epub/spine.rb +79 -0
 - data/lib/bookbinder/transform/epub/title.rb +92 -0
 - data/lib/bookbinder/transform/epub/version.rb +39 -0
 - data/lib/bookbinder/transform/generator.rb +8 -0
 - data/lib/bookbinder/transform/openbook/json.rb +15 -0
 - data/lib/bookbinder/transform/organizer.rb +41 -0
 - data/lib/bookbinder/transform.rb +7 -0
 - data/lib/bookbinder/version.rb +5 -0
 - data/lib/bookbinder.rb +29 -0
 - metadata +131 -0
 
    
        data/README.md
    ADDED
    
    | 
         @@ -0,0 +1,97 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Bookbinder
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            Ebook format conversion.
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            ## Basic use
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            Display the contents of an EPUB as a JSON "map":
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                $ bookbinder map path/to/file.epub
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            Convert an EPUB to an Openbook directory (the directory need not exist yet):
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                $ bookbinder convert path/to/file.epub path/to/dir
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
            Convert an EPUB to an Openbook archive:
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                $ bookbinder convert path/to/file.epub path/to/file.openbook
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            Convert an Openbook to an EPUB (...is not yet fully implemented!)
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            ## Use as a Ruby library
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            EPUB to Openbook:
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                require 'bookbinder'
         
     | 
| 
      
 29 
     | 
    
         
            +
                epub = Bookbinder::Package::EPUB.read('book.epub')
         
     | 
| 
      
 30 
     | 
    
         
            +
                openbook = epub.export(Bookbinder::Package::Openbook)
         
     | 
| 
      
 31 
     | 
    
         
            +
                openbook.write('book.openbook')
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
            For other basic actions, take a look at `lib/bookbinder/operations.rb`. This
         
     | 
| 
      
 34 
     | 
    
         
            +
            class provides a basic layer of convenience, such as reducing the above to:
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
                require 'bookbinder'
         
     | 
| 
      
 37 
     | 
    
         
            +
                Bookbinder::Operations.convert('book.epub', 'book.openbook')
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            ## Improving Bookbinder
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
            Inside Bookbinder, a "book" is simply a nested hash of properties and values.
         
     | 
| 
      
 43 
     | 
    
         
            +
            This hash is called "the map". Properties of the map that are transferrable
         
     | 
| 
      
 44 
     | 
    
         
            +
            between ebook package formats should follow the Openbook convention, which is
         
     | 
| 
      
 45 
     | 
    
         
            +
            currently maintained here:
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
            > https://gist.github.com/joseph/7303930
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
            The key to Bookbinder is this: for every feature of an ebook format, we create
         
     | 
| 
      
 50 
     | 
    
         
            +
            a "transform" class that does two things:
         
     | 
| 
      
 51 
     | 
    
         
            +
            - parses the raw config from the package into standard Openbook properties
         
     | 
| 
      
 52 
     | 
    
         
            +
              on the map; and
         
     | 
| 
      
 53 
     | 
    
         
            +
            - generates raw config into the package from those same standard Openbook
         
     | 
| 
      
 54 
     | 
    
         
            +
              properties on the map
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
            Basically, for every feature, the transform class describes how to read it, 
         
     | 
| 
      
 57 
     | 
    
         
            +
            and how to write it.
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
            The nice thing about this set-up is that if multiple package formats
         
     | 
| 
      
 60 
     | 
    
         
            +
            support the same feature, their transform classes work on the same map. Say
         
     | 
| 
      
 61 
     | 
    
         
            +
            you are converting from EPUB3 to Openbook - the book's title is parsed out of
         
     | 
| 
      
 62 
     | 
    
         
            +
            the EPUB file into the map using the transform at
         
     | 
| 
      
 63 
     | 
    
         
            +
            `lib/bookbinder/transform/epub/title.rb`. Then the map is handed over to
         
     | 
| 
      
 64 
     | 
    
         
            +
            the Openbook package, and the transform at
         
     | 
| 
      
 65 
     | 
    
         
            +
            `lib/bookbinder/transform/openbook/title.rb` would write it out to the 
         
     | 
| 
      
 66 
     | 
    
         
            +
            new package file.
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
            You can of course convert a package to its own format: in this case the same
         
     | 
| 
      
 69 
     | 
    
         
            +
            transform class does both the reading and the writing out -- the effect of
         
     | 
| 
      
 70 
     | 
    
         
            +
            this is to "tidy" the package.
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
            To add a package format Bookbinder, you should create the package class in
         
     | 
| 
      
 73 
     | 
    
         
            +
            `lib/bookbinder/package`, then create directory of transforms in
         
     | 
| 
      
 74 
     | 
    
         
            +
            `lib/bookbinder/transform`. You can borrow transforms from other packages. For
         
     | 
| 
      
 75 
     | 
    
         
            +
            instance, it might make sense for a DAISY package to share some of the
         
     | 
| 
      
 76 
     | 
    
         
            +
            transforms in EPUB, or for a hPub package to borrow some transforms 
         
     | 
| 
      
 77 
     | 
    
         
            +
            from Openbook.
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
            If you are adding a new feature to Bookbinder, you create the
         
     | 
| 
      
 80 
     | 
    
         
            +
            appropriate transform class for each package that supports the feature, and
         
     | 
| 
      
 81 
     | 
    
         
            +
            then add the equivalent tests.
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
            ## Planned format support
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
            * Openbook
         
     | 
| 
      
 87 
     | 
    
         
            +
            * EPUB3
         
     | 
| 
      
 88 
     | 
    
         
            +
            * EPUB2
         
     | 
| 
      
 89 
     | 
    
         
            +
            * hPub
         
     | 
| 
      
 90 
     | 
    
         
            +
            * PDF
         
     | 
| 
      
 91 
     | 
    
         
            +
            * ...
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
            ## Attribution and licensing
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
            Bookbinder was originally developed at OverDrive, Inc. Released under the
         
     | 
| 
      
 97 
     | 
    
         
            +
            MIT License. See `MIT-LICENSE` in this directory.
         
     | 
    
        data/Rakefile
    ADDED
    
    
    
        data/bin/bookbinder
    ADDED
    
    | 
         @@ -0,0 +1,17 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            #!/usr/bin/env ruby
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'bookbinder'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            if ARGV[0] == 'map'
         
     | 
| 
      
 6 
     | 
    
         
            +
              puts Bookbinder::Operations.map(ARGV[1])
         
     | 
| 
      
 7 
     | 
    
         
            +
            elsif ARGV[0] == 'validate'
         
     | 
| 
      
 8 
     | 
    
         
            +
              puts('Validate: not yet implemented.')
         
     | 
| 
      
 9 
     | 
    
         
            +
            elsif ARGV[0] == 'normalize'
         
     | 
| 
      
 10 
     | 
    
         
            +
              puts('Normalize: still too dangerous.')
         
     | 
| 
      
 11 
     | 
    
         
            +
            elsif ARGV[0] == 'convert'
         
     | 
| 
      
 12 
     | 
    
         
            +
              src_path = ARGV[1]
         
     | 
| 
      
 13 
     | 
    
         
            +
              dest_path = ARGV[2]
         
     | 
| 
      
 14 
     | 
    
         
            +
              src_pkg, dest_pkg = Bookbinder::Operations.convert(src_path, dest_path)
         
     | 
| 
      
 15 
     | 
    
         
            +
              puts("Converted #{src_pkg.class} at #{src_path}")
         
     | 
| 
      
 16 
     | 
    
         
            +
              puts("------ to #{dest_pkg.class} at #{dest_path}")
         
     | 
| 
      
 17 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,171 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Bookbinder::DocumentProxy
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              XMLNS = {
         
     | 
| 
      
 4 
     | 
    
         
            +
                'xhtml' => 'http://www.w3.org/1999/xhtml',
         
     | 
| 
      
 5 
     | 
    
         
            +
                'dc' => 'http://purl.org/dc/elements/1.1/',
         
     | 
| 
      
 6 
     | 
    
         
            +
                'dcterms' => 'http://purl.org/dc/terms/',
         
     | 
| 
      
 7 
     | 
    
         
            +
                'mathml' => 'http://www.w3.org/1998/Math/MathML',
         
     | 
| 
      
 8 
     | 
    
         
            +
                'svg' => 'http://www.w3.org/2000/svg',
         
     | 
| 
      
 9 
     | 
    
         
            +
                'ocf' => 'urn:oasis:names:tc:opendocument:xmlns:container',
         
     | 
| 
      
 10 
     | 
    
         
            +
                'opf' => 'http://www.idpf.org/2007/opf',
         
     | 
| 
      
 11 
     | 
    
         
            +
                'ncx' => 'http://www.daisy.org/z3986/2005/ncx/',
         
     | 
| 
      
 12 
     | 
    
         
            +
                'epub' => 'http://www.idpf.org/2007/ops'
         
     | 
| 
      
 13 
     | 
    
         
            +
              }
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              XML_PREFIX = {
         
     | 
| 
      
 16 
     | 
    
         
            +
                'ibooks' => 'http://vocabulary.itunes.apple.com/rdf/ibooks/vocabulary-extensions-1.0/',
         
     | 
| 
      
 17 
     | 
    
         
            +
                'rendition' => 'http://www.idpf.org/vocab/rendition/#'
         
     | 
| 
      
 18 
     | 
    
         
            +
              }
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              attr_reader(:doc)
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
              def load(string)
         
     | 
| 
      
 25 
     | 
    
         
            +
                begin
         
     | 
| 
      
 26 
     | 
    
         
            +
                  @doc = Nokogiri::XML(string)
         
     | 
| 
      
 27 
     | 
    
         
            +
                rescue
         
     | 
| 
      
 28 
     | 
    
         
            +
                  @doc = Nokogiri::HTML(string)
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
                self
         
     | 
| 
      
 31 
     | 
    
         
            +
              end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
              def build(&blk)
         
     | 
| 
      
 35 
     | 
    
         
            +
                builder = Nokogiri::XML::Builder.new { |x|
         
     | 
| 
      
 36 
     | 
    
         
            +
                  @doc = x.doc
         
     | 
| 
      
 37 
     | 
    
         
            +
                  yield(self, x)
         
     | 
| 
      
 38 
     | 
    
         
            +
                }
         
     | 
| 
      
 39 
     | 
    
         
            +
                self
         
     | 
| 
      
 40 
     | 
    
         
            +
              end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
              def new_node(tag, options = {})
         
     | 
| 
      
 44 
     | 
    
         
            +
                Nokogiri::XML::Node.new(tag, doc).tap { |node|
         
     | 
| 
      
 45 
     | 
    
         
            +
                  yield(node)  if block_given?
         
     | 
| 
      
 46 
     | 
    
         
            +
                  if parent = options[:append]
         
     | 
| 
      
 47 
     | 
    
         
            +
                    parent = find(parent)  if parent.kind_of?(String)
         
     | 
| 
      
 48 
     | 
    
         
            +
                    parent.add_child(node)
         
     | 
| 
      
 49 
     | 
    
         
            +
                  end
         
     | 
| 
      
 50 
     | 
    
         
            +
                }
         
     | 
| 
      
 51 
     | 
    
         
            +
              end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
              # Given a CSS query, returns the first result, or nil.
         
     | 
| 
      
 55 
     | 
    
         
            +
              #
         
     | 
| 
      
 56 
     | 
    
         
            +
              def find(query)
         
     | 
| 
      
 57 
     | 
    
         
            +
                @doc.at_css(query, node_namespaces(@doc.root))
         
     | 
| 
      
 58 
     | 
    
         
            +
              end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
              # Given a CSS query, returns all results, or an empty array.
         
     | 
| 
      
 62 
     | 
    
         
            +
              #
         
     | 
| 
      
 63 
     | 
    
         
            +
              def search(query)
         
     | 
| 
      
 64 
     | 
    
         
            +
                @doc.css(query, node_namespaces(@doc.root))
         
     | 
| 
      
 65 
     | 
    
         
            +
              end
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
              # Iterates over the results of the search for the given CSS query.
         
     | 
| 
      
 69 
     | 
    
         
            +
              #
         
     | 
| 
      
 70 
     | 
    
         
            +
              def each(query, &blk)
         
     | 
| 
      
 71 
     | 
    
         
            +
                search(query).each(&blk)
         
     | 
| 
      
 72 
     | 
    
         
            +
              end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
              def find_within(node, query)
         
     | 
| 
      
 76 
     | 
    
         
            +
                node.at_css(query, node_namespaces(node))
         
     | 
| 
      
 77 
     | 
    
         
            +
              end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
              def search_within(node, query)
         
     | 
| 
      
 81 
     | 
    
         
            +
                node.css(query, node_namespaces(node))
         
     | 
| 
      
 82 
     | 
    
         
            +
              end
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
              def each_within(node, query, &blk)
         
     | 
| 
      
 86 
     | 
    
         
            +
                search_within(node, query).each(&blk)
         
     | 
| 
      
 87 
     | 
    
         
            +
              end
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
             
     | 
| 
      
 90 
     | 
    
         
            +
              def add_namespace(namespace_label, default = false)
         
     | 
| 
      
 91 
     | 
    
         
            +
                add_node_namespace(@doc.root, namespace_label, default)
         
     | 
| 
      
 92 
     | 
    
         
            +
              end
         
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
              def add_prefix(prefix_label, prefix_attribute = 'prefix')
         
     | 
| 
      
 96 
     | 
    
         
            +
                add_node_prefix(@doc.root, prefix_label, prefix_attribute)
         
     | 
| 
      
 97 
     | 
    
         
            +
              end
         
     | 
| 
      
 98 
     | 
    
         
            +
             
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
              def add_node_namespace(node, namespace_label, default = false)
         
     | 
| 
      
 101 
     | 
    
         
            +
                xmlns = default ? nil : namespace_label
         
     | 
| 
      
 102 
     | 
    
         
            +
                node.add_namespace_definition(xmlns, XMLNS[namespace_label])
         
     | 
| 
      
 103 
     | 
    
         
            +
              end
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
              def add_node_prefix(node, prefix_label, prefix_attribute = 'prefix')
         
     | 
| 
      
 107 
     | 
    
         
            +
                prefix = "#{prefix_label}: #{XML_PREFIX[prefix_label]}"
         
     | 
| 
      
 108 
     | 
    
         
            +
                prefixes = [node[prefix_attribute], prefix].compact
         
     | 
| 
      
 109 
     | 
    
         
            +
                node[prefix_attribute] = prefixes.join("\n")
         
     | 
| 
      
 110 
     | 
    
         
            +
              end
         
     | 
| 
      
 111 
     | 
    
         
            +
             
     | 
| 
      
 112 
     | 
    
         
            +
             
     | 
| 
      
 113 
     | 
    
         
            +
              def to_str
         
     | 
| 
      
 114 
     | 
    
         
            +
                if @doc.kind_of?(Nokogiri::HTML::Document)
         
     | 
| 
      
 115 
     | 
    
         
            +
                  # Remove the old-style charset meta tag that Nokogiri auto-inserts.
         
     | 
| 
      
 116 
     | 
    
         
            +
                  # This is nasty business, but apparently once again Nokogiri is
         
     | 
| 
      
 117 
     | 
    
         
            +
                  # wrong and Markus Gylling knows best:
         
     | 
| 
      
 118 
     | 
    
         
            +
                  # http://code.google.com/p/epubcheck/issues/detail?id=135#c3
         
     | 
| 
      
 119 
     | 
    
         
            +
                  html = @doc.to_xhtml
         
     | 
| 
      
 120 
     | 
    
         
            +
                  html.sub!(
         
     | 
| 
      
 121 
     | 
    
         
            +
                    '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />',
         
     | 
| 
      
 122 
     | 
    
         
            +
                    ''
         
     | 
| 
      
 123 
     | 
    
         
            +
                  )
         
     | 
| 
      
 124 
     | 
    
         
            +
                  html
         
     | 
| 
      
 125 
     | 
    
         
            +
                elsif @doc.kind_of?(Nokogiri::XML::Document)
         
     | 
| 
      
 126 
     | 
    
         
            +
                  @doc.to_xml
         
     | 
| 
      
 127 
     | 
    
         
            +
                end
         
     | 
| 
      
 128 
     | 
    
         
            +
              end
         
     | 
| 
      
 129 
     | 
    
         
            +
             
     | 
| 
      
 130 
     | 
    
         
            +
             
     | 
| 
      
 131 
     | 
    
         
            +
              protected
         
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                # When performing a css() or at_css() query that has a namespaced
         
     | 
| 
      
 134 
     | 
    
         
            +
                # element (epub|switch) or a namespaced attribute (*[epub|type]),
         
     | 
| 
      
 135 
     | 
    
         
            +
                # you need to list the namespaces you want to use.
         
     | 
| 
      
 136 
     | 
    
         
            +
                #
         
     | 
| 
      
 137 
     | 
    
         
            +
                # You could just use the namespaces in the document, but it's possible
         
     | 
| 
      
 138 
     | 
    
         
            +
                # that the document author has bound the namespace to something
         
     | 
| 
      
 139 
     | 
    
         
            +
                # unexpected. For eg, they might have <dublincore:title> elements,
         
     | 
| 
      
 140 
     | 
    
         
            +
                # rather than <dc:title> elements. They probably deserve everything
         
     | 
| 
      
 141 
     | 
    
         
            +
                # that's coming to them, but it's valid XML, so we should support it.
         
     | 
| 
      
 142 
     | 
    
         
            +
                #
         
     | 
| 
      
 143 
     | 
    
         
            +
                # Therefore we should supply the namespace labels that WE expect,
         
     | 
| 
      
 144 
     | 
    
         
            +
                # rather than what the document author supplied, and let LibXML
         
     | 
| 
      
 145 
     | 
    
         
            +
                # do the translation automatically.
         
     | 
| 
      
 146 
     | 
    
         
            +
                #
         
     | 
| 
      
 147 
     | 
    
         
            +
                # This method constructs a hash of namespaces we can give to the
         
     | 
| 
      
 148 
     | 
    
         
            +
                # Nokogiri query, using our XMLNS constant. It also includes any
         
     | 
| 
      
 149 
     | 
    
         
            +
                # namespaces defined on the node or its ancestors -- typically
         
     | 
| 
      
 150 
     | 
    
         
            +
                # so that we can get the 'default' namespace for the document.
         
     | 
| 
      
 151 
     | 
    
         
            +
                #
         
     | 
| 
      
 152 
     | 
    
         
            +
             
     | 
| 
      
 153 
     | 
    
         
            +
                def node_namespaces(node)
         
     | 
| 
      
 154 
     | 
    
         
            +
                  @node_namespaces ||= {}
         
     | 
| 
      
 155 
     | 
    
         
            +
                  @node_namespaces[node] ||= node.namespaces.merge(default_namespaces)
         
     | 
| 
      
 156 
     | 
    
         
            +
                end
         
     | 
| 
      
 157 
     | 
    
         
            +
             
     | 
| 
      
 158 
     | 
    
         
            +
             
     | 
| 
      
 159 
     | 
    
         
            +
                def default_namespaces
         
     | 
| 
      
 160 
     | 
    
         
            +
                  @default_namespaces ||= XMLNS.inject({}) { |acc, arr|
         
     | 
| 
      
 161 
     | 
    
         
            +
                    key, val = arr
         
     | 
| 
      
 162 
     | 
    
         
            +
                    acc.update("xmlns#{key ? ":#{key}" : ''}" => val)
         
     | 
| 
      
 163 
     | 
    
         
            +
                  }
         
     | 
| 
      
 164 
     | 
    
         
            +
                end
         
     | 
| 
      
 165 
     | 
    
         
            +
             
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
      
 167 
     | 
    
         
            +
                def method_missing(mthd, *args, &blk)
         
     | 
| 
      
 168 
     | 
    
         
            +
                  @doc.send(mthd, *args, &blk)
         
     | 
| 
      
 169 
     | 
    
         
            +
                end
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,149 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Bookbinder::File
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              attr_accessor(:path, :file_type)
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              def initialize(path, file_system)
         
     | 
| 
      
 7 
     | 
    
         
            +
                @path = path
         
     | 
| 
      
 8 
     | 
    
         
            +
                @file_system = file_system
         
     | 
| 
      
 9 
     | 
    
         
            +
                @file_type = analyze_file_type(@path)
         
     | 
| 
      
 10 
     | 
    
         
            +
              end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
              # Gets a representation of the contents -- if json, as a hash, if xml,
         
     | 
| 
      
 14 
     | 
    
         
            +
              # as a Nokogiri document, etc. If mode includes 'w', then the document
         
     | 
| 
      
 15 
     | 
    
         
            +
              # will be saved eventually.
         
     | 
| 
      
 16 
     | 
    
         
            +
              #
         
     | 
| 
      
 17 
     | 
    
         
            +
              def document(mode = 'rw')
         
     | 
| 
      
 18 
     | 
    
         
            +
                dirty!  if mode.match(/w/)
         
     | 
| 
      
 19 
     | 
    
         
            +
                @document ||= string_to_document
         
     | 
| 
      
 20 
     | 
    
         
            +
              end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              def new_xml_document(&blk)
         
     | 
| 
      
 24 
     | 
    
         
            +
                @file_system.write(@path, '')  unless dirty?
         
     | 
| 
      
 25 
     | 
    
         
            +
                dirty!
         
     | 
| 
      
 26 
     | 
    
         
            +
                @document = Bookbinder::DocumentProxy.new.build(&blk)
         
     | 
| 
      
 27 
     | 
    
         
            +
              end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
              # Indicates that we should write out the document on save.
         
     | 
| 
      
 31 
     | 
    
         
            +
              #
         
     | 
| 
      
 32 
     | 
    
         
            +
              def dirty!
         
     | 
| 
      
 33 
     | 
    
         
            +
                @dirty = true
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
              # Has the document changed, therefore we should write it out on save?
         
     | 
| 
      
 38 
     | 
    
         
            +
              #
         
     | 
| 
      
 39 
     | 
    
         
            +
              def dirty?
         
     | 
| 
      
 40 
     | 
    
         
            +
                @dirty ? true : false
         
     | 
| 
      
 41 
     | 
    
         
            +
              end
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
              # Modifies the file in this file_system.
         
     | 
| 
      
 45 
     | 
    
         
            +
              #
         
     | 
| 
      
 46 
     | 
    
         
            +
              def save
         
     | 
| 
      
 47 
     | 
    
         
            +
                @file_system.write(@path, document_to_string)  if dirty?
         
     | 
| 
      
 48 
     | 
    
         
            +
                reset
         
     | 
| 
      
 49 
     | 
    
         
            +
              end
         
     | 
| 
      
 50 
     | 
    
         
            +
             
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
              # Resets state so that the next call to `document` will load it
         
     | 
| 
      
 53 
     | 
    
         
            +
              # fresh from the string. Returns self for easy chaining.
         
     | 
| 
      
 54 
     | 
    
         
            +
              #
         
     | 
| 
      
 55 
     | 
    
         
            +
              def reset
         
     | 
| 
      
 56 
     | 
    
         
            +
                @document = nil
         
     | 
| 
      
 57 
     | 
    
         
            +
                @dirty = false
         
     | 
| 
      
 58 
     | 
    
         
            +
                self
         
     | 
| 
      
 59 
     | 
    
         
            +
              end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
              # Writes the file to another file system.
         
     | 
| 
      
 63 
     | 
    
         
            +
              #
         
     | 
| 
      
 64 
     | 
    
         
            +
              def copy_to(dest_file_system, dest_path)
         
     | 
| 
      
 65 
     | 
    
         
            +
                @file_system.get_file(@path) { |file|
         
     | 
| 
      
 66 
     | 
    
         
            +
                  dest_file_system.set_file(dest_path, file)
         
     | 
| 
      
 67 
     | 
    
         
            +
                }
         
     | 
| 
      
 68 
     | 
    
         
            +
                self
         
     | 
| 
      
 69 
     | 
    
         
            +
              end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
              # Guesses the mime-type (aka content-type, aka media-type)
         
     | 
| 
      
 73 
     | 
    
         
            +
              # of this file based on its extension.
         
     | 
| 
      
 74 
     | 
    
         
            +
              #
         
     | 
| 
      
 75 
     | 
    
         
            +
              def media_type
         
     | 
| 
      
 76 
     | 
    
         
            +
                @media_type ||= Bookbinder::MediaType.of(@path)
         
     | 
| 
      
 77 
     | 
    
         
            +
              end
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
              # Proxy through to FileSystem#exists?.
         
     | 
| 
      
 81 
     | 
    
         
            +
              #
         
     | 
| 
      
 82 
     | 
    
         
            +
              def exists?
         
     | 
| 
      
 83 
     | 
    
         
            +
                @file_system.exists?(path)
         
     | 
| 
      
 84 
     | 
    
         
            +
              end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
              # Proxy through to FileSystem#read.
         
     | 
| 
      
 88 
     | 
    
         
            +
              #
         
     | 
| 
      
 89 
     | 
    
         
            +
              def read
         
     | 
| 
      
 90 
     | 
    
         
            +
                @file_system.read(path)
         
     | 
| 
      
 91 
     | 
    
         
            +
              end
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
             
     | 
| 
      
 94 
     | 
    
         
            +
              # Proxy through to FileSystem#write.
         
     | 
| 
      
 95 
     | 
    
         
            +
              #
         
     | 
| 
      
 96 
     | 
    
         
            +
              def write(data)
         
     | 
| 
      
 97 
     | 
    
         
            +
                @file_system.write(path, data)
         
     | 
| 
      
 98 
     | 
    
         
            +
              end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
              # Proxy through to FileSystem#get_file.
         
     | 
| 
      
 102 
     | 
    
         
            +
              #
         
     | 
| 
      
 103 
     | 
    
         
            +
              def get_file(mode = 'r', &blk)
         
     | 
| 
      
 104 
     | 
    
         
            +
                @file_system.get_file(path, mode, &blk)
         
     | 
| 
      
 105 
     | 
    
         
            +
              end
         
     | 
| 
      
 106 
     | 
    
         
            +
             
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
              # Proxy through to FileSystem#set_file.
         
     | 
| 
      
 109 
     | 
    
         
            +
              #
         
     | 
| 
      
 110 
     | 
    
         
            +
              def set_file(file_io)
         
     | 
| 
      
 111 
     | 
    
         
            +
                @file_system.set_file(path, io)
         
     | 
| 
      
 112 
     | 
    
         
            +
              end
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
              protected
         
     | 
| 
      
 116 
     | 
    
         
            +
             
     | 
| 
      
 117 
     | 
    
         
            +
                def analyze_file_type(path)
         
     | 
| 
      
 118 
     | 
    
         
            +
                  if media_type.match(/json/)
         
     | 
| 
      
 119 
     | 
    
         
            +
                    :json
         
     | 
| 
      
 120 
     | 
    
         
            +
                  elsif media_type.match(/xml$/) || media_type.match(/html$/)
         
     | 
| 
      
 121 
     | 
    
         
            +
                    :xml
         
     | 
| 
      
 122 
     | 
    
         
            +
                  end
         
     | 
| 
      
 123 
     | 
    
         
            +
                end
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                def string_to_document(ftype = @file_type)
         
     | 
| 
      
 127 
     | 
    
         
            +
                  if ftype == :json
         
     | 
| 
      
 128 
     | 
    
         
            +
                    JSON.load(@file_system.read(@path))
         
     | 
| 
      
 129 
     | 
    
         
            +
                  elsif ftype == :xml
         
     | 
| 
      
 130 
     | 
    
         
            +
                    Bookbinder::DocumentProxy.new.load(@file_system.read(@path))
         
     | 
| 
      
 131 
     | 
    
         
            +
                  elsif ftype == :text
         
     | 
| 
      
 132 
     | 
    
         
            +
                    @file_system.read(@path)
         
     | 
| 
      
 133 
     | 
    
         
            +
                  end
         
     | 
| 
      
 134 
     | 
    
         
            +
                end
         
     | 
| 
      
 135 
     | 
    
         
            +
             
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                def document_to_string
         
     | 
| 
      
 138 
     | 
    
         
            +
                  raise 'Document not loaded'  unless @document
         
     | 
| 
      
 139 
     | 
    
         
            +
                  if @document.kind_of?(Bookbinder::DocumentProxy)
         
     | 
| 
      
 140 
     | 
    
         
            +
                    @document.to_str
         
     | 
| 
      
 141 
     | 
    
         
            +
                  elsif @document.kind_of?(Hash)
         
     | 
| 
      
 142 
     | 
    
         
            +
                    JSON.dump(@document)
         
     | 
| 
      
 143 
     | 
    
         
            +
                  else
         
     | 
| 
      
 144 
     | 
    
         
            +
                    @document.to_s
         
     | 
| 
      
 145 
     | 
    
         
            +
                  end
         
     | 
| 
      
 146 
     | 
    
         
            +
                end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
             
     | 
| 
      
 149 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,62 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Bookbinder::FileSystem::Directory
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              def initialize(path)
         
     | 
| 
      
 4 
     | 
    
         
            +
                @dir = path
         
     | 
| 
      
 5 
     | 
    
         
            +
              end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              def exists?(path)
         
     | 
| 
      
 9 
     | 
    
         
            +
                File.exists?(full_path(path))
         
     | 
| 
      
 10 
     | 
    
         
            +
              end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
              def read(path)
         
     | 
| 
      
 14 
     | 
    
         
            +
                fpath = full_path(path)
         
     | 
| 
      
 15 
     | 
    
         
            +
                must_exist(fpath)
         
     | 
| 
      
 16 
     | 
    
         
            +
                File.read(fpath)
         
     | 
| 
      
 17 
     | 
    
         
            +
              end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
              def write(path, data)
         
     | 
| 
      
 21 
     | 
    
         
            +
                fpath = full_path(path)
         
     | 
| 
      
 22 
     | 
    
         
            +
                FileUtils.mkdir_p(File.dirname(fpath))
         
     | 
| 
      
 23 
     | 
    
         
            +
                File.open(fpath, 'w') { |f| f.write(data) }
         
     | 
| 
      
 24 
     | 
    
         
            +
                nil
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              def get_file(path, mode = 'r', &blk)
         
     | 
| 
      
 29 
     | 
    
         
            +
                fpath = full_path(path)
         
     | 
| 
      
 30 
     | 
    
         
            +
                must_exist(fpath)  if mode[0] != 'w'
         
     | 
| 
      
 31 
     | 
    
         
            +
                File.open(fpath, mode, &blk)
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              def set_file(path, file)
         
     | 
| 
      
 36 
     | 
    
         
            +
                fpath = full_path(path)
         
     | 
| 
      
 37 
     | 
    
         
            +
                FileUtils.mkdir_p(File.dirname(fpath))
         
     | 
| 
      
 38 
     | 
    
         
            +
                FileUtils.cp(file.path, fpath)
         
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
              def each
         
     | 
| 
      
 43 
     | 
    
         
            +
                Dir.glob(File.join(@dir, '**', '*')) { |path|
         
     | 
| 
      
 44 
     | 
    
         
            +
                  next  if File.directory?(path)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  yield(path.sub(/#{@dir}\//, ''))
         
     | 
| 
      
 46 
     | 
    
         
            +
                }
         
     | 
| 
      
 47 
     | 
    
         
            +
              end
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
              protected
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                def must_exist(fpath)
         
     | 
| 
      
 53 
     | 
    
         
            +
                  return  if File.exists?(fpath)
         
     | 
| 
      
 54 
     | 
    
         
            +
                  raise(Bookbinder::FileSystem::UnknownPath, fpath)
         
     | 
| 
      
 55 
     | 
    
         
            +
                end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                def full_path(path)
         
     | 
| 
      
 59 
     | 
    
         
            +
                  File.join(@dir, path)
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,57 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Bookbinder::FileSystem::Memory
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              def initialize
         
     | 
| 
      
 4 
     | 
    
         
            +
                @index = {}
         
     | 
| 
      
 5 
     | 
    
         
            +
              end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              def exists?(path)
         
     | 
| 
      
 9 
     | 
    
         
            +
                @index.key?(path)
         
     | 
| 
      
 10 
     | 
    
         
            +
              end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
              def read(path)
         
     | 
| 
      
 14 
     | 
    
         
            +
                raise(Bookbinder::FileSystem::UnknownPath, path)  unless exists?(path)
         
     | 
| 
      
 15 
     | 
    
         
            +
                @index[path]
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
              def write(path, data)
         
     | 
| 
      
 20 
     | 
    
         
            +
                @index[path] = data
         
     | 
| 
      
 21 
     | 
    
         
            +
                nil
         
     | 
| 
      
 22 
     | 
    
         
            +
              end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
              # Creates a tempfile so you can do memory-efficient stuff.
         
     | 
| 
      
 26 
     | 
    
         
            +
              #
         
     | 
| 
      
 27 
     | 
    
         
            +
              def get_file(path, mode = 'r', &blk)
         
     | 
| 
      
 28 
     | 
    
         
            +
                read_before = mode[0] != 'w'
         
     | 
| 
      
 29 
     | 
    
         
            +
                write_after = mode[0] != 'r'
         
     | 
| 
      
 30 
     | 
    
         
            +
                begin
         
     | 
| 
      
 31 
     | 
    
         
            +
                  tmpfile = Tempfile.new(File.basename(path))
         
     | 
| 
      
 32 
     | 
    
         
            +
                  if read_before
         
     | 
| 
      
 33 
     | 
    
         
            +
                    tmpfile.write(read(path))
         
     | 
| 
      
 34 
     | 
    
         
            +
                    tmpfile.rewind
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
                  blk.call(tmpfile)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  if write_after
         
     | 
| 
      
 38 
     | 
    
         
            +
                    tmpfile.rewind
         
     | 
| 
      
 39 
     | 
    
         
            +
                    write(path, tmpfile.read)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  end
         
     | 
| 
      
 41 
     | 
    
         
            +
                ensure
         
     | 
| 
      
 42 
     | 
    
         
            +
                  tmpfile.close
         
     | 
| 
      
 43 
     | 
    
         
            +
                  tmpfile.unlink
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
              end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
              def set_file(path, file)
         
     | 
| 
      
 49 
     | 
    
         
            +
                write(path, file.read)
         
     | 
| 
      
 50 
     | 
    
         
            +
              end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
              def each(&blk)
         
     | 
| 
      
 54 
     | 
    
         
            +
                @index.each_key(&blk)
         
     | 
| 
      
 55 
     | 
    
         
            +
              end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,106 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Bookbinder::FileSystem::ZipFile < Bookbinder::FileSystem
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              def initialize(path)
         
     | 
| 
      
 4 
     | 
    
         
            +
                @zipfile_path = path
         
     | 
| 
      
 5 
     | 
    
         
            +
                @zipfile = nil
         
     | 
| 
      
 6 
     | 
    
         
            +
                instantiate_zipfile  if File.exists?(@zipfile_path)
         
     | 
| 
      
 7 
     | 
    
         
            +
              end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
              def exists?(path)
         
     | 
| 
      
 11 
     | 
    
         
            +
                @zipfile && @zipfile.find_entry(path) ? true : false
         
     | 
| 
      
 12 
     | 
    
         
            +
              end
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              def read(path)
         
     | 
| 
      
 16 
     | 
    
         
            +
                must_exist(path)
         
     | 
| 
      
 17 
     | 
    
         
            +
                @zipfile.read(path)
         
     | 
| 
      
 18 
     | 
    
         
            +
              end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              def write(path, data)
         
     | 
| 
      
 22 
     | 
    
         
            +
                return set_mimetype(data)  if path == 'mimetype'
         
     | 
| 
      
 23 
     | 
    
         
            +
                instantiate_zipfile
         
     | 
| 
      
 24 
     | 
    
         
            +
                @zipfile.get_output_stream(path) { |f| f.write(data) }
         
     | 
| 
      
 25 
     | 
    
         
            +
                nil
         
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
              def get_file(path, mode = 'r', &blk)
         
     | 
| 
      
 30 
     | 
    
         
            +
                read_before = mode[0] != 'w'
         
     | 
| 
      
 31 
     | 
    
         
            +
                write_after = mode[0] != 'r'
         
     | 
| 
      
 32 
     | 
    
         
            +
                if read_before
         
     | 
| 
      
 33 
     | 
    
         
            +
                  must_exist(path)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  tmp_path = Dir::Tmpname.create(File.basename(path)) { |tmp_path|
         
     | 
| 
      
 35 
     | 
    
         
            +
                    raise Errno::EEXIST  if File.exists?(tmp_path)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  }
         
     | 
| 
      
 37 
     | 
    
         
            +
                  @zipfile.commit
         
     | 
| 
      
 38 
     | 
    
         
            +
                  @zipfile.extract(path, tmp_path)
         
     | 
| 
      
 39 
     | 
    
         
            +
                  File.open(tmp_path, mode) { |tmp_file|
         
     | 
| 
      
 40 
     | 
    
         
            +
                    blk.call(tmp_file)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    if write_after
         
     | 
| 
      
 42 
     | 
    
         
            +
                      set_file(path, tmp_file)
         
     | 
| 
      
 43 
     | 
    
         
            +
                      @zipfile.commit
         
     | 
| 
      
 44 
     | 
    
         
            +
                    end
         
     | 
| 
      
 45 
     | 
    
         
            +
                  }
         
     | 
| 
      
 46 
     | 
    
         
            +
                  File.unlink(tmp_path)
         
     | 
| 
      
 47 
     | 
    
         
            +
                else
         
     | 
| 
      
 48 
     | 
    
         
            +
                  tmp_file = Tempfile.new(File.basename(path))
         
     | 
| 
      
 49 
     | 
    
         
            +
                  blk.call(tmp_file)
         
     | 
| 
      
 50 
     | 
    
         
            +
                  tmp_file.close
         
     | 
| 
      
 51 
     | 
    
         
            +
                  set_file(path, tmp_file)  if write_after
         
     | 
| 
      
 52 
     | 
    
         
            +
                  tmp_file.unlink
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
                nil
         
     | 
| 
      
 55 
     | 
    
         
            +
              end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
              def set_file(path, file)
         
     | 
| 
      
 59 
     | 
    
         
            +
                instantiate_zipfile
         
     | 
| 
      
 60 
     | 
    
         
            +
                @zipfile.add(path, file.path) { true }
         
     | 
| 
      
 61 
     | 
    
         
            +
                @zipfile.commit
         
     | 
| 
      
 62 
     | 
    
         
            +
                nil
         
     | 
| 
      
 63 
     | 
    
         
            +
              end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
              def each(&blk)
         
     | 
| 
      
 67 
     | 
    
         
            +
                @zipfile.each(&blk)
         
     | 
| 
      
 68 
     | 
    
         
            +
              end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
              def close
         
     | 
| 
      
 72 
     | 
    
         
            +
                @zipfile.close  if @zipfile
         
     | 
| 
      
 73 
     | 
    
         
            +
              end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
              protected
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                def must_exist(path)
         
     | 
| 
      
 79 
     | 
    
         
            +
                  raise(Bookbinder::FileSystem::UnknownPath, path)  unless exists?(path)
         
     | 
| 
      
 80 
     | 
    
         
            +
                end
         
     | 
| 
      
 81 
     | 
    
         
            +
             
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                def instantiate_zipfile(options = {})
         
     | 
| 
      
 84 
     | 
    
         
            +
                  @zipfile ||= Zip::File.open(@zipfile_path, Zip::File::CREATE)
         
     | 
| 
      
 85 
     | 
    
         
            +
                end
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
             
     | 
| 
      
 88 
     | 
    
         
            +
                # This is all EPUB's fault.
         
     | 
| 
      
 89 
     | 
    
         
            +
                #
         
     | 
| 
      
 90 
     | 
    
         
            +
                def set_mimetype(mimetype)
         
     | 
| 
      
 91 
     | 
    
         
            +
                  if @zipfile || File.exists?(@zipfile_path)
         
     | 
| 
      
 92 
     | 
    
         
            +
                    raise("Cannot set mimetype for existing archive: #{@zipfile_path}")
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
                  Zip::OutputStream.open(@zipfile_path) { |stream|
         
     | 
| 
      
 95 
     | 
    
         
            +
                    stream.put_next_entry(
         
     | 
| 
      
 96 
     | 
    
         
            +
                      'mimetype',
         
     | 
| 
      
 97 
     | 
    
         
            +
                      nil,
         
     | 
| 
      
 98 
     | 
    
         
            +
                      nil,
         
     | 
| 
      
 99 
     | 
    
         
            +
                      Zip::Entry::STORED,
         
     | 
| 
      
 100 
     | 
    
         
            +
                      Zlib::NO_COMPRESSION
         
     | 
| 
      
 101 
     | 
    
         
            +
                    )
         
     | 
| 
      
 102 
     | 
    
         
            +
                    stream.write(mimetype)
         
     | 
| 
      
 103 
     | 
    
         
            +
                  }
         
     | 
| 
      
 104 
     | 
    
         
            +
                end
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
      
 106 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,35 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Bookbinder::FileSystem
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              def exists?(path)
         
     | 
| 
      
 4 
     | 
    
         
            +
                # IMPLEMENT IN SUBCLASS
         
     | 
| 
      
 5 
     | 
    
         
            +
              end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              def read(path)
         
     | 
| 
      
 9 
     | 
    
         
            +
                # IMPLEMENT IN SUBCLASS
         
     | 
| 
      
 10 
     | 
    
         
            +
              end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
              def write(path, data)
         
     | 
| 
      
 14 
     | 
    
         
            +
                # IMPLEMENT IN SUBCLASS
         
     | 
| 
      
 15 
     | 
    
         
            +
              end
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              def get_file(path, mode = 'r', &blk)
         
     | 
| 
      
 19 
     | 
    
         
            +
                # IMPLEMENT IN SUBCLASS
         
     | 
| 
      
 20 
     | 
    
         
            +
              end
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
              def set_file(path, file)
         
     | 
| 
      
 24 
     | 
    
         
            +
                # IMPLEMENT IN SUBCLASS
         
     | 
| 
      
 25 
     | 
    
         
            +
              end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              def each(&blk)
         
     | 
| 
      
 29 
     | 
    
         
            +
                # IMPLEMENT IN SUBCLASS
         
     | 
| 
      
 30 
     | 
    
         
            +
              end
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
              class UnknownPath < RuntimeError; end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
            end
         
     |