bindery 0.0.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ .ruby-version
1
2
  *.gem
2
3
  .bundle
3
4
  Gemfile.lock
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ rvm: 1.9.2
2
+ notifications:
3
+ recipients:
4
+ - glv@vanderburg.org
data/ChangeLog ADDED
@@ -0,0 +1,18 @@
1
+ 2013-08-10 Glenn Vanderburg
2
+
3
+ * content_methods.rb: Completely changed the behavior of the
4
+ chapter method when a block is passed. The previous design was
5
+ embarrassingly bad. There was no need to use the passed block
6
+ to dynamically calculate the parameters that could be passed
7
+ directly to the method; Ruby has other ways of doing that. Now,
8
+ the block is used to define hierarchical book structure: nested
9
+ chapters, parts, sections, etc.
10
+
11
+ If you were using the block to dynamically build content and
12
+ return the chapter title and file name, simply move the content of
13
+ the block to a method, and then you can splice the returned title
14
+ and filename into the arguments of chapter using the splat
15
+ operator (*) or (if the block used the strategy of returning a
16
+ hash) include the activesupport gem, require
17
+ 'active_support/core_ext/hash/slice', and use Hash#slice(:title,
18
+ :filename).
data/README.md CHANGED
@@ -1,11 +1,11 @@
1
- # Bindery
1
+ # Bindery [![Build Status](https://secure.travis-ci.org/glv/bindery.png)](http://travis-ci.org/glv/bindery)
2
2
 
3
3
  [Bindery][] is a [Ruby][] library for packaging ebooks.
4
4
 
5
5
  Electronic book formats are typically rather simple.
6
6
  An EPUB book, for example, is just HTML, CSS, and some image files packed into a single zip file along with various bits of metadata.
7
7
  But there are numerous tricky details, and a lot of redundancy in the metadata.
8
- Bindery aims to simplify the process.
8
+ Bindery aims to simplify the process, while stopping short of being a full ebook authoring system.
9
9
 
10
10
  To use Bindery, you write a simple Ruby program that describes the book's structure and important metadata.
11
11
  This program is also responsible for identifying the HTML files that correspond to the book's chapters; those files might already exist, but the program can generate them on the fly as well.
@@ -49,13 +49,11 @@ Bindery.book do |b|
49
49
  # is a Ruby Markdown library that supports numerous common extensions
50
50
  # to the basic Markdown syntax.)
51
51
  Dir['*.maruku'].sort.each do |file_name|
52
- b.chapter do
53
- output_file_name = file_name.sub(/\.maruku$/, '.xhtml_gen')
54
- doc = Maruku.new(IO.read(file_name))
55
- open(output_file_name, "w") {|os| os.write(doc.to_html)}
56
- # return a hash with title and file info:
57
- { :title => doc.attributes[:title], :file => output_file_name }
58
- end
52
+ output_file_name = file_name.sub(/\.maruku$/, '.xhtml_gen')
53
+ doc = Maruku.new(IO.read(file_name))
54
+ open(output_file_name, "w") {|os| os.write(doc.to_html)}
55
+
56
+ b.chapter doc.attributes[:title], output_file_name
59
57
  end
60
58
 
61
59
  b.backmatter 'Colophon' # filename assumed to be colophon.xhtml
@@ -64,18 +62,22 @@ end
64
62
 
65
63
  ## Status
66
64
 
67
- Bindery is currently limited to generating EPUB books that do not contain images or other non-textual content.
65
+ Bindery is currently limited to generating EPUB books.
68
66
 
69
67
  It is also in a very early stage.
70
68
  It's capable of generating very simple books, but many features (including the frontmatter and backmatter methods in the example above) do not work yet.
71
- Additionally, validation is sketchy, so it's quite likely that the books you build using Bindery will not be valid EPUB files.
72
69
  But the basics are there, and contributions are welcome.
73
70
 
71
+ Generated EPUB books will be valid according to [epubcheck][] *except* perhaps for chapter content.
72
+ EPUB places some additional restrictions on XHTML and CSS, and if the supplied chapter content violates those restrictions then the EPUB file will be invalid.
73
+ Most EPUB readers are fairly permissive about such things, but some are more particular.
74
+ I plan to build support for tidying up the XHTML and CSS and eliminating invalid constructs, but at the moment that's a low priority.
75
+
74
76
  Planned features include:
75
77
 
76
78
  * options for sections (with and without section title pages) rather than just a flat chapter structure.
77
79
  * additional metadata including all of [Dublin Core][].
78
- * support for images
80
+ * title and cover pages
79
81
  * support for multiple stylesheets
80
82
  * support for generating Mobipocket books
81
83
 
@@ -102,6 +104,13 @@ Bindery is designed to support those use cases, not just brand new books written
102
104
  If you want to publish in both electronic and paper formats, Bindery is probably not for you.
103
105
  Bindery is targeted at electronic books only.
104
106
 
107
+ ## Acknowledgments
108
+
109
+ In February 2013, about a year and a half after initially writing Bindery,
110
+ I discovered the [Python Epub Builder][pyepub], written by (apparently)
111
+ Bin Tan. It was more complete than Bindery was at the time, and it inspired
112
+ me to work on Bindery again, and gave me some valuable ideas.
113
+
105
114
  [adobe indesign]: http://www.adobe.com/products/indesign.html
106
115
  [asciidoc]: http://www.methods.co.nz/asciidoc/
107
116
  [best software writing]: http://www.apress.com/9781590595008
@@ -109,6 +118,7 @@ Bindery is targeted at electronic books only.
109
118
  [devonthink]: http://www.devon-technologies.com/products/devonthink/index.html
110
119
  [dublin core]: http://dublincore.org/sm
111
120
  [epub]: http://idpf.org/epub
121
+ [epubcheck]: http://code.google.com/p/epubcheck/
112
122
  [git]: http://git-scm.com/
113
123
  [github]: http://github.com/
114
124
  [git-scribe]: http://github.com/schacon/git-scribe
@@ -118,6 +128,7 @@ Bindery is targeted at electronic books only.
118
128
  [johnson dt2]: http://boingboing.net/2009/01/27/diy-how-to-write-a-b.html
119
129
  [markdown]: http://daringfireball.net/projects/markdown/
120
130
  [microsoft word]: http://office.microsoft.com/word/
131
+ [pyepub]: http://code.google.com/p/python-epub-builder/
121
132
  [rake]: http://rake.rubyforge.org/
122
133
  [ruby]: http://ruby-lang.org/
123
134
  [sproutcore]: http://www.sproutcore.com/
data/TODO.taskpaper CHANGED
@@ -1,13 +1,14 @@
1
1
  - Flesh out validation
2
2
  Consider moving details to format modules.
3
3
  Investigate Mobi validity constraints to see whether that makes sense.
4
+ - Cache fetched image (and other) files so we don't have to fetch them twice while building multiple formats.
4
5
  - Add more metadata support
5
6
  - subtitle
6
7
  - different kinds of authors
7
8
  - Title page
8
9
  - Title image
9
- - Image support
10
- - A basic CSS file
10
+ - Image support @done
11
+ - A basic CSS file @done
11
12
  - Support for alternative CSS files
12
13
  - Frontmatter
13
14
  - Nested chapter structures
@@ -40,7 +40,7 @@ def process_part(url, i)
40
40
  content.children[0..1].each{|c| c.remove}
41
41
  os.write content.serialize
42
42
  end
43
- { :title => title, :file => file_name }
43
+ [title, file_name]
44
44
  end
45
45
 
46
46
  Bindery.book do |b|
@@ -53,6 +53,6 @@ Bindery.book do |b|
53
53
  b.language 'en'
54
54
 
55
55
  PARTS.each_with_index do |url, i|
56
- b.chapter { process_part(url, i) }
56
+ b.chapter *process_part(url, i)
57
57
  end
58
58
  end
@@ -0,0 +1 @@
1
+ <h2>Appendices</h2>
@@ -11,17 +11,23 @@ Bindery.book do |b|
11
11
 
12
12
  b.chapter "Chapter 1", 'chapter_1.xhtml'
13
13
  b.chapter "Chapter 2", 'chapter_2.xhtml', :body_only => false
14
- b.chapter "Chapter 3" do
15
- # It's good practice to have a way to distinguish source files from
16
- # those generated as part of the build, so you can have a Rake task
17
- # or something to clean up the intermediate files.
18
- "chapter_3.xhtml_gen".tap do |filename|
19
- File.open(filename, "w") do |os|
20
- os.write %{
21
- <p>Let's just write this chapter in line, shall we?</p>
22
- <p>It seems easier that way, when the chapters are so short.</p>
23
- }
24
- end
25
- end
14
+
15
+ # It's good practice to have a way to distinguish source files from
16
+ # those generated as part of the build, so you can have a Rake task
17
+ # or something to clean up the intermediate files.
18
+ fn = 'chapter_3.xhtml_gen'
19
+ File.open(fn, 'w') do |os|
20
+ os.write %{
21
+ <p>Let's just write this chapter in line, shall we?</p>
22
+ <p>It seems easier that way, when the chapters are so short.</p>
23
+ }
24
+ end
25
+ b.chapter "Chapter 3", fn
26
+
27
+ b.part("Appendices", 'appendices.xhtml') do |div|
28
+ div.appendix "Appendix 1: Errata", 'errata.xhtml'
29
+ div.appendix "Appendix 2: Index", 'index.xhtml'
26
30
  end
31
+
32
+ b.part "Colophon", 'colophon.xhtml'
27
33
  end
@@ -9,6 +9,8 @@
9
9
  <body>
10
10
  <h2>Chapter 2</h2>
11
11
 
12
- <p>Unlike Chapter 1, this chapter's source file is a full, valid XHTML file, not just the body.</p>
12
+ <p>Unlike Chapter 1, this chapter's source file is a full, valid XHTML
13
+ file, not just the body. It will cause some errors for an epub
14
+ reader that really validates the spec.</p>
13
15
  </body>
14
16
  </html>
@@ -0,0 +1,3 @@
1
+ <h2>Colophon</h2>
2
+
3
+ <p>Made with bindery!</p>
@@ -0,0 +1,5 @@
1
+ <h2>Appendix 1: Errata</h2>
2
+
3
+ <ol>
4
+ <li><cite>Appendix 1: Errata</cite>: title should be "Erratum"</li>
5
+ </ol>
@@ -0,0 +1,3 @@
1
+ <h2>Appendix 2: Index</h2>
2
+
3
+ <p>(Expand this section when there's something worth looking up.)</p>
@@ -0,0 +1,52 @@
1
+ require 'bindery'
2
+ require 'open-uri'
3
+ require 'nokogiri'
4
+ #require 'tidy_ffi'
5
+
6
+ PARTS = [
7
+ 'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/3917.html',
8
+ 'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/4305.html',
9
+ 'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/4586.html',
10
+ 'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/4647.html',
11
+ 'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/5103.html',
12
+ 'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/5343.html',
13
+ 'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/5406.html',
14
+ 'http://webcache.googleusercontent.com/search?q=cache:http://mvanier.livejournal.com/5846.html'
15
+ ]
16
+
17
+ TITLES = [
18
+ "Part 1: Basics",
19
+ nil,
20
+ nil,
21
+ nil,
22
+ "Part 5: Error-Handling Monads",
23
+ "Part 6: More on Error-Handling Monads",
24
+ "Part 7: State Monads",
25
+ "Part 8: More on State Monads"
26
+ ]
27
+
28
+ def process_part(url, i)
29
+ file_name = "chapter_#{i+1}.xhtml_gen"
30
+ doc = Nokogiri::HTML(open(url), nil, 'UTF-8')
31
+ title = TITLES[i] || doc.at_css('h1.b-singlepost-title').text.sub(/^.*?Tutorial \(p(art .*?)\).*$/, 'P\\1')
32
+ open(file_name, 'w') do |os|
33
+ content = doc.at_css('div.b-singlepost-body')
34
+ os.puts "<h1>#{title}</h1>"
35
+ os.write content.serialize
36
+ end
37
+ [title, file_name]
38
+ end
39
+
40
+ Bindery.book do |b|
41
+ b.output 'mvanier_monad_tutorial'
42
+ b.format :epub
43
+
44
+ b.title "Yet Another Monad Tutorial"
45
+ b.author 'Mike Vanier'
46
+ b.url 'http://glenn.mp/book/mvanier_monad_tutorial'
47
+ b.language 'en'
48
+
49
+ PARTS.each_with_index do |url, i|
50
+ b.chapter *process_part(url, i)
51
+ end
52
+ end
data/lib/bindery.rb CHANGED
@@ -4,7 +4,8 @@ require 'bindery/extensions/file'
4
4
  require 'bindery/version'
5
5
  require 'bindery/bindery'
6
6
  require 'bindery/book'
7
+ require 'bindery/content_methods'
7
8
  require 'bindery/book_builder'
8
- require 'bindery/chapter'
9
+ require 'bindery/division'
9
10
 
10
11
  require 'bindery/formats/epub'
data/lib/bindery/book.rb CHANGED
@@ -1,13 +1,23 @@
1
1
  module Bindery
2
2
  class Book
3
+ class Metadata < Struct.new(:name, :value, :options)
4
+ end
5
+
6
+ class DublinMetadata < Metadata
7
+ end
8
+
3
9
  attr_accessor :output, :url, :isbn, :title, :language, :author, :subtitle
4
10
 
11
+ def metadata
12
+ @metadata ||= []
13
+ end
14
+
5
15
  def formats
6
16
  @formats ||= []
7
17
  end
8
18
 
9
- def chapters
10
- @chapters ||= []
19
+ def divisions
20
+ @divisions ||= []
11
21
  end
12
22
 
13
23
  def full_title
@@ -15,14 +25,14 @@ module Bindery
15
25
  end
16
26
 
17
27
  def valid?
18
- configuration_valid? && metadata_valid? && chapters_valid?
28
+ configuration_valid? && metadata_valid? && divisions_valid?
19
29
  end
20
30
 
21
31
  def configuration_valid?
22
32
  true
23
33
  # formats specified or correctly defaulted
24
34
  # ouput specified
25
- # at least one chapter
35
+ # at least one division
26
36
  end
27
37
 
28
38
  def metadata_valid?
@@ -31,8 +41,8 @@ module Bindery
31
41
  # what is there is correct
32
42
  end
33
43
 
34
- def chapters_valid?
35
- chapters.all?{|chapter| chapter.valid?} # && chapter file names are unique
44
+ def divisions_valid?
45
+ divisions.all?{|div| div.valid?} # && division file names are unique
36
46
  end
37
47
 
38
48
  def generate
@@ -3,6 +3,27 @@ module Bindery
3
3
  class BookBuilder
4
4
  attr_accessor :book
5
5
 
6
+ Metadata = {
7
+ :contributor => nil,
8
+ :cover => :special,
9
+ :coverage => nil,
10
+ :creator => nil,
11
+ :date => nil,
12
+ :description => nil,
13
+ :format => nil,
14
+ :identifier => :required,
15
+ :language => :required,
16
+ :publisher => nil,
17
+ :relation => nil,
18
+ :rights => nil,
19
+ :source => nil,
20
+ :subject => nil,
21
+ :title => :required,
22
+ :type => nil
23
+ }
24
+
25
+ include ContentMethods
26
+
6
27
  def initialize
7
28
  self.book = ::Bindery::Book.new
8
29
  end
@@ -16,7 +37,42 @@ module Bindery
16
37
  raise "output already set to #{book.output}" unless book.output.nil?
17
38
  book.output = basename.to_s
18
39
  end
40
+
41
+ def divisions
42
+ book.divisions
43
+ end
44
+
45
+ # ----------------------------------------------------
46
+ # Metadata elements
47
+
48
+ # Allows grouping metadata elements together in a named block
49
+ # within the book specification. Use of this method is not necessary;
50
+ # all of the metadata methods can be called directly on the BookBuilder
51
+ # instance. It is usually best, though, to have them clearly grouped
52
+ # within a metadata block.
53
+ def metadata
54
+ yield self
55
+ end
19
56
 
57
+ def metadata_element(name, value, options={})
58
+ name_sym = name.to_sym
59
+ if Metadata[name_sym] == :special
60
+ book.metadata << Bindery::Book::Metadata.new(name_sym, value, options)
61
+ else
62
+ book.metadata << Bindery::Book::DublinMetadata.new(name_sym, value, options)
63
+ end
64
+ end
65
+
66
+ def method_missing(name, *args, &block)
67
+ if Metadata.include?(name.to_sym)
68
+ metadata_element(name, *args, &block)
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+ # TODO: Most of these could be switched to be general metadata
75
+ # objects. Should they be?
20
76
  def url(url)
21
77
  book.url = url
22
78
  end
@@ -41,61 +97,6 @@ module Bindery
41
97
  book.author = author
42
98
  end
43
99
 
44
- # :call-seq:
45
- # chapter(title, filename, options={})
46
- # chapter(title, options={}) { ... }
47
- # chapter(options={}) { ... }
48
- #
49
- # Add a chapter to the book.
50
- #
51
- # If called with a title and a filename, the chapter's content should
52
- # be found in the named file.
53
- #
54
- # If called with a title and a block, the block should generate or
55
- # retrieve the chapter's content, write it to a file, and return the
56
- # file name.
57
- #
58
- # If called with no parameters and a block, the block should generate
59
- # or retrieve the chapter's content, write it to a file, and return a
60
- # hash with the following keys:
61
- # [:title] the chapter title (required)
62
- # [:file] the name of the file containing the chapter content (required)
63
- #
64
- # An options hash parameter is always allowed, and the following
65
- # options are supported:
66
- # [:body_only] the file contains only the body of the XHTML document,
67
- # and Bindery should wrap it to create a valid document.
68
- # Defaults to true.
69
- def chapter(*args)
70
- default_options = {:body_only => true}
71
- options = default_options.merge(args.last.kind_of?(Hash) ? args.pop : {})
72
- if block_given?
73
- chapter_dynamic(options, *args){yield}
74
- else
75
- chapter_static(options, *args)
76
- end
77
- end
78
-
79
- protected
80
-
81
- def chapter_static(options, *args)
82
- title, file = args
83
- raise ArgumentError, "title not specified" if title.nil?
84
- raise ArgumentError, "file not specified" if file.nil?
85
- book.chapters << Chapter.new(title, file, options)
86
- end
87
-
88
- def chapter_dynamic(options, title=nil)
89
- if title
90
- file = yield
91
- raise "expected the block to return a filename string" unless file.kind_of?(String)
92
- chapter_static(options, title, yield)
93
- else
94
- info = yield
95
- raise "expected the block to return a hash containing :title, :file, etc." unless info.kind_of?(Hash)
96
- chapter_static(options, info[:title], info[:file])
97
- end
98
- end
99
100
  end
100
101
 
101
102
  end
@@ -0,0 +1,36 @@
1
+ module Bindery
2
+ module ContentMethods
3
+
4
+ # Add a division to the book.
5
+ #
6
+ # The following options are supported:
7
+ # [:body_only] the file contains only the body of the XHTML document,
8
+ # and Bindery should wrap it to create a valid document.
9
+ # Defaults to true.
10
+ def div(div_type, title, filename, options={})
11
+ options = {:body_only => true}.merge(options)
12
+ raise ArgumentError, "title not specified" if title.nil?
13
+ raise ArgumentError, "file not specified" if filename.nil?
14
+ div = Division.new(div_type, title, filename, options)
15
+ divisions << div
16
+ yield div if block_given?
17
+ end
18
+
19
+ def chapter(title, filename, options={}, &block)
20
+ div('chapter', title, filename, options, &block)
21
+ end
22
+
23
+ def section(title, filename, options={}, &block)
24
+ div('section', title, filename, options, &block)
25
+ end
26
+
27
+ def part(title, filename, options={}, &block)
28
+ div('part', title, filename, options, &block)
29
+ end
30
+
31
+ def appendix(title, filename, options={}, &block)
32
+ div('appendix', title, filename, options, &block)
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,43 @@
1
+ module Bindery
2
+ class Division
3
+ attr_accessor :div_type
4
+ attr_accessor :file
5
+ attr_accessor :title
6
+ attr_accessor :options
7
+
8
+ include ContentMethods
9
+
10
+ def initialize(div_type, title, file, options)
11
+ self.div_type = div_type
12
+ self.title = title
13
+ self.file = file
14
+ self.options = options
15
+ end
16
+
17
+ def valid?
18
+ true
19
+ # ??? title specified? Is this required by the spec? Think about
20
+ # in what sense a chapter needs a title. Wouldn't it be nice
21
+ # if (say) Pratchett's books could be broken up a bit even
22
+ # though he doesn't have titled chapters? Aren't there books
23
+ # with actual chapters but no titles? Does epub provide
24
+ # another way to provide subdivisions without separate
25
+ # chapters that would break up the text flow?
26
+ # file exists, readable
27
+ # file content properly formed? Does that matter? Can we
28
+ # verify it?
29
+ end
30
+
31
+ def divisions
32
+ @divisions ||= []
33
+ end
34
+
35
+ def body_only?
36
+ options.fetch(:body_only, true)
37
+ end
38
+
39
+ def include_images?
40
+ options.fetch(:include_images, true)
41
+ end
42
+ end
43
+ end
@@ -35,7 +35,7 @@ module Bindery
35
35
  def initialize(book)
36
36
  self.book = book
37
37
  book.extend BookMethods
38
- book.chapters.each{|chapter| chapter.extend ChapterMethods}
38
+ book.divisions.each{|division| division.extend DivisionMethods}
39
39
  self.manifest_entries = []
40
40
  end
41
41
 
@@ -48,8 +48,8 @@ module Bindery
48
48
  zipfile.write_file 'META-INF/container.xml', container
49
49
 
50
50
  # also frontmatter, backmatter
51
- book.chapters.each do |chapter|
52
- write_chapter(chapter, zipfile)
51
+ book.divisions.each do |division|
52
+ write_division(division, zipfile)
53
53
  end
54
54
 
55
55
  zipfile.mkdir 'css'
@@ -93,7 +93,7 @@ module Bindery
93
93
  }
94
94
 
95
95
  xm.manifest {
96
- book.chapters.each{|chapter| xm.item 'id'=>chapter.epub_id, 'href'=>chapter.epub_output_file, 'media-type'=>'application/xhtml+xml'}
96
+ book.divisions.each{|division| division.write_item(xm)}
97
97
  # also frontmatter, backmatter
98
98
  xm.item 'id'=>'stylesheet', 'href'=>'css/book.css', 'media-type'=>'text/css'
99
99
  manifest_entries.each do |entry|
@@ -104,7 +104,7 @@ module Bindery
104
104
  }
105
105
 
106
106
  xm.spine('toc'=>'ncx') {
107
- book.chapters.each{|chapter| xm.itemref 'idref'=>chapter.epub_id}
107
+ book.divisions.each{|division| division.write_itemref(xm)}
108
108
  }
109
109
 
110
110
  # xm.guide {
@@ -134,36 +134,31 @@ module Bindery
134
134
  }
135
135
 
136
136
  xm.navMap {
137
- play_order = 1
137
+ play_order = 0
138
138
 
139
139
  # also frontmatter, backmatter
140
- book.chapters.each do |chapter|
141
- xm.navPoint('class'=>'chapter', 'id'=>chapter.epub_id, 'playOrder'=>play_order) {
142
- xm.navLabel {
143
- xm.text chapter.title
144
- }
145
- xm.content 'src'=>chapter.epub_output_file
146
- }
140
+ book.divisions.each do |division|
147
141
  play_order += 1
142
+ play_order = division.write_navpoint(xm, play_order)
148
143
  end
149
144
  }
150
145
  }
151
146
  end
152
147
 
153
- def write_chapter(chapter, zipfile)
148
+ def write_division(division, zipfile)
154
149
  save_options = Nokogiri::XML::Node::SaveOptions
155
- File.open(chapter.file, 'r:UTF-8') do |ch_in|
150
+ File.open(division.file, 'r:UTF-8') do |ch_in|
156
151
  doc = Nokogiri.HTML(ch_in.read)
157
- include_images(doc, zipfile) if chapter.include_images?
158
- zipfile.get_output_stream(chapter.epub_output_file) do |ch_out|
159
- if chapter.body_only?
160
- # FIXME: must HTML-escape the chapter title
152
+ include_images(doc, zipfile) if division.include_images?
153
+ zipfile.get_output_stream(division.epub_output_file) do |ch_out|
154
+ if division.body_only?
155
+ # FIXME: must HTML-escape the division title
161
156
  ch_out.write %{|<?xml version="1.0" encoding="UTF-8" ?>
162
157
  |<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
163
158
  |<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
164
159
  |<head>
165
160
  | <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" />
166
- | <title>#{chapter.title}</title>
161
+ | <title>#{division.title}</title>
167
162
  | <link rel="stylesheet" href="css/book.css" type="text/css" />
168
163
  |</head>
169
164
  |}.strip_margin
@@ -175,6 +170,9 @@ module Bindery
175
170
  end
176
171
  end
177
172
  end
173
+ division.divisions.each do |div|
174
+ write_division(div, zipfile)
175
+ end
178
176
  end
179
177
 
180
178
  def include_images(doc, zipfile)
@@ -184,13 +182,17 @@ module Bindery
184
182
  url = img['src']
185
183
  img_fn = make_image_file_name(zipfile, url)
186
184
  # TODO: These images should be cached somewhere for multi-format runs
187
- open(url, 'r') do |is|
188
- zipfile.get_output_stream(img_fn) do |os|
189
- os.write is.read
185
+ begin
186
+ open(url, 'r') do |is|
187
+ zipfile.get_output_stream(img_fn) do |os|
188
+ os.write is.read
189
+ end
190
190
  end
191
+ add_manifest_entry(img_fn)
192
+ img['src'] = img_fn
193
+ rescue OpenURI::HTTPError => ex
194
+ puts "Image fetch failed: #{ex.message} (#{url})"
191
195
  end
192
- add_manifest_entry(img_fn)
193
- img['src'] = img_fn
194
196
  end
195
197
  end
196
198
 
@@ -291,7 +293,7 @@ module Bindery
291
293
  end
292
294
 
293
295
  def depth
294
- 1
296
+ (divisions.map(&:depth) + [0]).max
295
297
  end
296
298
 
297
299
  def ident
@@ -299,7 +301,12 @@ module Bindery
299
301
  end
300
302
  end
301
303
 
302
- module ChapterMethods
304
+ module DivisionMethods
305
+
306
+ def self.extended(obj)
307
+ obj.divisions.each{|division| division.extend DivisionMethods}
308
+ end
309
+
303
310
  def epub_id
304
311
  @epub_id ||= File.stemname(file)
305
312
  end
@@ -307,6 +314,50 @@ module Bindery
307
314
  def epub_output_file
308
315
  @epub_output_file ||= "#{epub_id}.xhtml"
309
316
  end
317
+
318
+ def depth
319
+ (divisions.map(&:depth) + [0]).max + 1
320
+ end
321
+
322
+ def write_item(xm)
323
+ xm.item('id' => epub_id,
324
+ 'href' => epub_output_file,
325
+ 'media-type' => 'application/xhtml+xml')
326
+ divisions.each{|div| div.write_item(xm)}
327
+ end
328
+
329
+ def write_itemref(xm)
330
+ xm.itemref('idref' => epub_id)
331
+ divisions.each{|div| div.write_itemref(xm)}
332
+ end
333
+
334
+ def write_navpoint(xm, play_order)
335
+ xm.navPoint('class'=>'chapter', 'id'=>epub_id, 'playOrder'=>play_order) {
336
+ xm.navLabel {
337
+ xm.text title
338
+ }
339
+ xm.content 'src'=>epub_output_file
340
+ divisions.each do |division|
341
+ play_order += 1
342
+ play_order = division.write_navpoint(xm, play_order)
343
+ end
344
+ }
345
+ play_order
346
+ end
347
+
348
+ end
349
+
350
+ module MetadataMethods
351
+ def to_xml(builder)
352
+ builder.meta(options.merge(:name => name, :content => value))
353
+ %{<dc:#{name}>#{value}</dc:#{name}>}
354
+ end
355
+ end
356
+
357
+ module DublinMetadataMethods
358
+ def to_xml(builder)
359
+ builder.dc name, value, options
360
+ end
310
361
  end
311
362
  end
312
363
  end
@@ -1,3 +1,3 @@
1
1
  module Bindery
2
- VERSION = "0.0.3"
2
+ VERSION = "2.0.0"
3
3
  end
@@ -38,53 +38,32 @@ describe Bindery::BookBuilder do
38
38
  end
39
39
  end
40
40
 
41
- describe "#chapter" do
41
+ describe "#div" do
42
42
 
43
- context "when called without a block" do
44
- it "raises an exception if title or filename are not supplied" do
45
- expect { subject.chapter }.to raise_error(ArgumentError, "title not specified")
46
- expect { subject.chapter "A" }.to raise_error(ArgumentError, "file not specified")
47
- end
48
-
49
- it "stores a new chapter with the supplied options" do
50
- subject.chapter "Foo", "bar.xhtml"
51
- book.chapters.should have(1).elements
52
- book.chapters.first.title.should == "Foo"
53
- book.chapters.first.file.should == "bar.xhtml"
54
- end
43
+ it "raises an exception if title or filename are not supplied" do
44
+ expect { subject.div("chapter", nil, "fn") }.to raise_error(ArgumentError, "title not specified")
45
+ expect { subject.div("chapter", "A", nil) }.to raise_error(ArgumentError, "file not specified")
46
+ end
47
+
48
+ it "stores a new division with the supplied options" do
49
+ subject.div 'excerpt', "Foo", "bar.xhtml", :a => :b
50
+ book.divisions.should have(1).elements
51
+ div = book.divisions.first
52
+ div.div_type.should == 'excerpt'
53
+ div.title.should == "Foo"
54
+ div.file.should == "bar.xhtml"
55
+ div.options.should include(:a => :b)
55
56
  end
56
57
 
57
- context "when called with a block" do
58
-
59
- context "and title is supplied as an argument" do
60
- it "expects the block to result in a filename string" do
61
- expect { subject.chapter("Foo"){ 3 } }.to raise_error
62
- end
63
-
64
- it "stores the title and the filename as a new chapter" do
65
- subject.chapter("Foo"){ 'bar.xhtml' }
66
- book.chapters.should have(1).elements
67
- book.chapters.first.title.should == "Foo"
68
- book.chapters.first.file.should == "bar.xhtml"
69
- end
70
- end
71
-
72
- context "and title is not supplied as an argument" do
73
- it "expects the block to result in a hash with :title and :file options" do
74
- expect { subject.chapter{ "Invalid" } }.to raise_error
75
- expect { subject.chapter{ {:title => 'Foo'} } }.to raise_error
76
- expect { subject.chapter{ {:file => 'bar.xhtml'} } }.to raise_error
77
- end
78
-
79
- it "stores the title and the filename as a new chapter" do
80
- subject.chapter { {:title => 'Foo', :file => 'bar.xhtml'} }
81
- book.chapters.should have(1).elements
82
- book.chapters.first.title.should == "Foo"
83
- book.chapters.first.file.should == "bar.xhtml"
84
- end
58
+ end
59
+
60
+ %w[chapter section part appendix].each do |div_type|
61
+ describe "##{div_type}" do
62
+ it "calls #div with the appropriate type" do
63
+ subject.expects(div_type.to_sym).once
64
+ subject.send(div_type, :title, :filename, {:a => :b})
85
65
  end
86
-
87
66
  end
88
-
89
67
  end
90
- end
68
+
69
+ end
metadata CHANGED
@@ -1,113 +1,150 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: bindery
3
- version: !ruby/object:Gem::Version
3
+ version: !ruby/object:Gem::Version
4
+ version: 2.0.0
4
5
  prerelease:
5
- version: 0.0.3
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - Glenn Vanderburg
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
-
13
- date: 2011-06-18 00:00:00 -05:00
14
- default_executable:
15
- dependencies:
16
- - !ruby/object:Gem::Dependency
12
+ date: 2014-12-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
17
15
  name: builder
18
- requirement: &id001 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
19
17
  none: false
20
- requirements:
21
- - - ">="
22
- - !ruby/object:Gem::Version
23
- version: "0"
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
24
22
  type: :runtime
25
23
  prerelease: false
26
- version_requirements: *id001
27
- - !ruby/object:Gem::Dependency
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
28
31
  name: nokogiri
29
- requirement: &id002 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
30
33
  none: false
31
- requirements:
32
- - - ">="
33
- - !ruby/object:Gem::Version
34
- version: "0"
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
35
38
  type: :runtime
36
39
  prerelease: false
37
- version_requirements: *id002
38
- - !ruby/object:Gem::Dependency
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
39
47
  name: zip
40
- requirement: &id003 !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
41
49
  none: false
42
- requirements:
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- version: "0"
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
46
54
  type: :runtime
47
55
  prerelease: false
48
- version_requirements: *id003
49
- - !ruby/object:Gem::Dependency
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
50
63
  name: mocha
51
- requirement: &id004 !ruby/object:Gem::Requirement
64
+ requirement: !ruby/object:Gem::Requirement
52
65
  none: false
53
- requirements:
54
- - - ">="
55
- - !ruby/object:Gem::Version
56
- version: "0"
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
57
70
  type: :development
58
71
  prerelease: false
59
- version_requirements: *id004
60
- - !ruby/object:Gem::Dependency
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
61
79
  name: rake
62
- requirement: &id005 !ruby/object:Gem::Requirement
80
+ requirement: !ruby/object:Gem::Requirement
63
81
  none: false
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: "0"
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
68
86
  type: :development
69
87
  prerelease: false
70
- version_requirements: *id005
71
- - !ruby/object:Gem::Dependency
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
72
95
  name: rspec
73
- requirement: &id006 !ruby/object:Gem::Requirement
96
+ requirement: !ruby/object:Gem::Requirement
74
97
  none: false
75
- requirements:
98
+ requirements:
76
99
  - - ~>
77
- - !ruby/object:Gem::Version
78
- version: "2.0"
100
+ - !ruby/object:Gem::Version
101
+ version: '2.0'
79
102
  type: :development
80
103
  prerelease: false
81
- version_requirements: *id006
82
- description: |-
83
- Bindery is a Ruby library for easy packaging of ebooks.
84
- You supply the chapter content (in HTML format) and explain the book's structure to bindery,
85
- and bindery generates the various other files required by ebook formats and assembles them
86
- into a completed book suitable for installation on an ebook reader.
87
- email:
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: '2.0'
110
+ description: ! 'Bindery is a Ruby library for easy packaging of ebooks.
111
+
112
+ You supply the chapter content (in HTML format) and explain the book''s structure
113
+ to bindery,
114
+
115
+ and bindery generates the various other files required by ebook formats and assembles
116
+ them
117
+
118
+ into a completed book suitable for installation on an ebook reader.'
119
+ email:
88
120
  - glv@vanderburg.org
89
121
  executables: []
90
-
91
122
  extensions: []
92
-
93
123
  extra_rdoc_files: []
94
-
95
- files:
124
+ files:
96
125
  - .gitignore
126
+ - .travis.yml
127
+ - ChangeLog
97
128
  - Gemfile
98
129
  - README.md
99
130
  - Rakefile
100
131
  - TODO.taskpaper
101
132
  - bindery.gemspec
102
133
  - examples/ashtray/book.rb
134
+ - examples/trivial/appendices.xhtml
103
135
  - examples/trivial/book.rb
104
136
  - examples/trivial/chapter_1.xhtml
105
137
  - examples/trivial/chapter_2.xhtml
138
+ - examples/trivial/colophon.xhtml
139
+ - examples/trivial/errata.xhtml
140
+ - examples/trivial/index.xhtml
141
+ - examples/vanier_monads/book.rb
106
142
  - lib/bindery.rb
107
143
  - lib/bindery/bindery.rb
108
144
  - lib/bindery/book.rb
109
145
  - lib/bindery/book_builder.rb
110
- - lib/bindery/chapter.rb
146
+ - lib/bindery/content_methods.rb
147
+ - lib/bindery/division.rb
111
148
  - lib/bindery/extensions/file.rb
112
149
  - lib/bindery/extensions/string.rb
113
150
  - lib/bindery/extensions/zip_file.rb
@@ -116,38 +153,34 @@ files:
116
153
  - spec/bindery/bindery_spec.rb
117
154
  - spec/bindery/book_builder_spec.rb
118
155
  - spec/spec_helper.rb
119
- has_rdoc: true
120
156
  homepage: http://github.com/glv/bindery
121
157
  licenses: []
122
-
123
158
  post_install_message:
124
159
  rdoc_options: []
125
-
126
- require_paths:
160
+ require_paths:
127
161
  - lib
128
- required_ruby_version: !ruby/object:Gem::Requirement
162
+ required_ruby_version: !ruby/object:Gem::Requirement
129
163
  none: false
130
- requirements:
131
- - - ">="
132
- - !ruby/object:Gem::Version
164
+ requirements:
165
+ - - ! '>='
166
+ - !ruby/object:Gem::Version
133
167
  version: 1.9.2
134
- required_rubygems_version: !ruby/object:Gem::Requirement
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
169
  none: false
136
- requirements:
137
- - - ">="
138
- - !ruby/object:Gem::Version
139
- hash: -2203226033245718984
140
- segments:
170
+ requirements:
171
+ - - ! '>='
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ segments:
141
175
  - 0
142
- version: "0"
176
+ hash: -394401607600380017
143
177
  requirements: []
144
-
145
178
  rubyforge_project: bindery
146
- rubygems_version: 1.5.2
179
+ rubygems_version: 1.8.23
147
180
  signing_key:
148
181
  specification_version: 3
149
182
  summary: Easy ebook packaging with Ruby
150
- test_files:
183
+ test_files:
151
184
  - spec/bindery/bindery_spec.rb
152
185
  - spec/bindery/book_builder_spec.rb
153
186
  - spec/spec_helper.rb
@@ -1,28 +0,0 @@
1
- module Bindery
2
- class Chapter
3
- attr_accessor :file
4
- attr_accessor :title
5
- attr_accessor :options
6
-
7
- def initialize(title, file, options)
8
- self.title = title
9
- self.file = file
10
- self.options = options
11
- end
12
-
13
- def valid?
14
- true
15
- # title specified
16
- # file exists, readable
17
- # file content properly formed? Does that matter? Can we verify it?
18
- end
19
-
20
- def body_only?
21
- options.fetch(:body_only, true)
22
- end
23
-
24
- def include_images?
25
- options.fetch(:include_images, true)
26
- end
27
- end
28
- end