bookbinder 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/README.md +97 -0
  2. data/Rakefile +12 -0
  3. data/bin/bookbinder +17 -0
  4. data/lib/bookbinder/document_proxy.rb +171 -0
  5. data/lib/bookbinder/file.rb +149 -0
  6. data/lib/bookbinder/file_system/directory.rb +62 -0
  7. data/lib/bookbinder/file_system/memory.rb +57 -0
  8. data/lib/bookbinder/file_system/zip_file.rb +106 -0
  9. data/lib/bookbinder/file_system.rb +35 -0
  10. data/lib/bookbinder/media_type.rb +17 -0
  11. data/lib/bookbinder/operations.rb +59 -0
  12. data/lib/bookbinder/package/epub.rb +69 -0
  13. data/lib/bookbinder/package/openbook.rb +33 -0
  14. data/lib/bookbinder/package.rb +295 -0
  15. data/lib/bookbinder/transform/epub/audio_overlay.rb +227 -0
  16. data/lib/bookbinder/transform/epub/audio_soundtrack.rb +73 -0
  17. data/lib/bookbinder/transform/epub/contributor.rb +11 -0
  18. data/lib/bookbinder/transform/epub/cover_image.rb +80 -0
  19. data/lib/bookbinder/transform/epub/cover_page.rb +148 -0
  20. data/lib/bookbinder/transform/epub/creator.rb +67 -0
  21. data/lib/bookbinder/transform/epub/description.rb +43 -0
  22. data/lib/bookbinder/transform/epub/language.rb +29 -0
  23. data/lib/bookbinder/transform/epub/metadata.rb +140 -0
  24. data/lib/bookbinder/transform/epub/nav.rb +60 -0
  25. data/lib/bookbinder/transform/epub/nav_toc.rb +177 -0
  26. data/lib/bookbinder/transform/epub/ncx.rb +63 -0
  27. data/lib/bookbinder/transform/epub/ocf.rb +33 -0
  28. data/lib/bookbinder/transform/epub/opf.rb +22 -0
  29. data/lib/bookbinder/transform/epub/package_identifier.rb +87 -0
  30. data/lib/bookbinder/transform/epub/rendition.rb +265 -0
  31. data/lib/bookbinder/transform/epub/resources.rb +38 -0
  32. data/lib/bookbinder/transform/epub/spine.rb +79 -0
  33. data/lib/bookbinder/transform/epub/title.rb +92 -0
  34. data/lib/bookbinder/transform/epub/version.rb +39 -0
  35. data/lib/bookbinder/transform/generator.rb +8 -0
  36. data/lib/bookbinder/transform/openbook/json.rb +15 -0
  37. data/lib/bookbinder/transform/organizer.rb +41 -0
  38. data/lib/bookbinder/transform.rb +7 -0
  39. data/lib/bookbinder/version.rb +5 -0
  40. data/lib/bookbinder.rb +29 -0
  41. metadata +131 -0
@@ -0,0 +1,73 @@
1
+ # For the specification, see the iBooks Asset Guide, specifically the
2
+ # section titled "Ambient Soundtrack" in version 5.1:
3
+ #
4
+ # https://itunesconnect.apple.com/docs/iBooksAssetGuide5.1Revision2.pdf
5
+ #
6
+ #
7
+ class Bookbinder::Transform::EPUB_AudioSoundtrack < Bookbinder::Transform
8
+
9
+ def dependencies
10
+ [Bookbinder::Transform::EPUB_Spine]
11
+ end
12
+
13
+
14
+ # Iterate through each spine item, looking for
15
+ #
16
+ # <audio epub:type="ibooks:soundtrack" src="..." />
17
+ #
18
+ def to_map(package)
19
+ package.map['spine'].each { |cmpt|
20
+ find_soundtrack_in_component(cmpt, package.file(cmpt['path']))
21
+ }
22
+ end
23
+
24
+
25
+ # Iterate through each spine item, adding an audio tag to components
26
+ # that don't have one, or setting the src if the audio tag exists.
27
+ #
28
+ def from_map(package)
29
+ package.map['spine'].each { |cmpt|
30
+ if cmpt['audio-soundtrack']
31
+ add_soundtrack_to_component(cmpt, package.file(cmpt['path']))
32
+ end
33
+ }
34
+ end
35
+
36
+
37
+ protected
38
+
39
+ def find_soundtrack_in_component(cmpt, cmpt_file)
40
+ soundtrack_tag = soundtrack_tag_in_document(cmpt_file.document)
41
+ cmpt['audio-soundtrack'] = soundtrack_tag ? soundtrack_tag['src'] : nil
42
+ end
43
+
44
+
45
+ def add_soundtrack_to_component(cmpt, cmpt_file)
46
+ cmpt_doc = cmpt_file.document
47
+ unless soundtrack_tag = soundtrack_tag_in_document(cmpt_doc)
48
+ cmpt_doc.add_namespace('epub')
49
+ cmpt_doc.add_prefix('ibooks', 'epub:prefix')
50
+
51
+ soundtrack_tag = cmpt_doc.new_node('audio', :append => 'body')
52
+ soundtrack_tag['epub:type'] = 'ibooks:soundtrack'
53
+
54
+ cmpt_doc.new_node('style', :append => 'head') { |style_tag|
55
+ style_tag['type'] = 'text/css'
56
+ style_tag['id'] = 'BB_HIDE_AUDIO_SOUNDTRACK'
57
+ style_tag.content = [
58
+ 'audio[epub|type="ibooks:soundtrack"] {',
59
+ 'position: absolute;',
60
+ 'top: -100px;',
61
+ '}'
62
+ ].join
63
+ }
64
+ end
65
+ soundtrack_tag['src'] = cmpt['audio-soundtrack']
66
+ end
67
+
68
+
69
+ def soundtrack_tag_in_document(cmpt_doc)
70
+ cmpt_doc.find('audio[epub|type="ibooks:soundtrack"]')
71
+ end
72
+
73
+ end
@@ -0,0 +1,11 @@
1
+ require 'bookbinder/transform/epub/creator'
2
+
3
+ class Bookbinder::Transform::EPUB_Contributor < Bookbinder::Transform::EPUB_Creator
4
+
5
+ protected
6
+
7
+ def actor_type
8
+ 'contributor'
9
+ end
10
+
11
+ end
@@ -0,0 +1,80 @@
1
+ # The best source of information about wading through the EPUB
2
+ # cover image quagmire has always been Keith's article on the
3
+ # Threepress blog:
4
+ #
5
+ # http://blog.safaribooksonline.com/2009/11/20/best-practices-in-epub-cover-images/
6
+ #
7
+ # He added an update for EPUB3, which follows the spec but is
8
+ # a bit easier to grok:
9
+ #
10
+ # http://blog.safaribooksonline.com/2011/05/26/covers-in-epub3/
11
+ #
12
+ class Bookbinder::Transform::EPUB_CoverImage < Bookbinder::Transform
13
+
14
+ def dependencies
15
+ [
16
+ Bookbinder::Transform::EPUB_Resources,
17
+ Bookbinder::Transform::EPUB_Metadata
18
+ ]
19
+ end
20
+
21
+
22
+ # If it's EPUB3, the cover will be in the 'properties' attribute
23
+ # of the manifest item: 'cover-image'
24
+ #
25
+ # Otherwise, look for a manifest item with an 'id' of 'cover-image'.
26
+ #
27
+ # Or, look for a meta tag with a 'name' of 'cover', then find the
28
+ # manifest item that has the 'id' that matches meta's 'content'.
29
+ #
30
+ # Set map['cover'] to this item (and remove it from map['resources']).
31
+ #
32
+ def to_map(package)
33
+ opf_doc = package.file(:opf).document('r')
34
+ cover_item = opf_doc.find('opf|manifest > opf|item[properties~="cover-image"]')
35
+ cover_item ||= opf_doc.find('opf|manifest > opf|item[id="cover-image"]')
36
+ cover_item ||= opf_doc.find('opf|manifest > opf|item[id="cover_image"]')
37
+ cover_meta_props = (package.map['metadata'] || {}).delete('cover')
38
+ if !cover_item && cover_meta_props && cover_meta_props.any?
39
+ cover_image_id = cover_meta_props.first['content']['@']
40
+ cover_item = opf_doc.find('opf|manifest > opf|item[id="'+cover_image_id+'"]')
41
+ end
42
+ covers = {}
43
+ if cover_item
44
+ rsrc = package.map['resources'].detect { |r|
45
+ r['id'] == cover_item['id']
46
+ }
47
+ covers.update("front" => package.map['resources'].delete(rsrc))
48
+ end
49
+ package.map['cover'] = covers
50
+ end
51
+
52
+
53
+ # Belt and braces: give the manifest item a property of
54
+ # 'cover-image', an 'id' of 'cover-image' (updating any
55
+ # idrefs) and create a meta tag with 'name'='cover' and
56
+ # 'content'='cover-image'.
57
+ #
58
+ def from_map(package)
59
+ return unless package.map['cover'] && cover = package.map['cover']['front']
60
+ opf_doc = package.file(:opf).document
61
+
62
+ opf_doc.new_node('item', :append => 'opf|manifest') { |manifest_item_tag|
63
+ manifest_item_tag['href'] = package.make_href(cover['path'])
64
+ manifest_item_tag['media-type'] = cover['media-type']
65
+ manifest_item_tag['id'] = 'cover-image'
66
+ manifest_item_tag['properties'] = 'cover-image'
67
+ }
68
+
69
+ cover_id = package.make_id(cover['path'])
70
+ opf_doc.each('[idref="'+cover_id+'"]') { |idref|
71
+ idref['idref'] = cover_id
72
+ }
73
+
74
+ opf_doc.new_node('meta', :append => 'opf|metadata') { |cover_meta_tag|
75
+ cover_meta_tag['name'] = 'cover'
76
+ cover_meta_tag['content'] = 'cover-image'
77
+ }
78
+ end
79
+
80
+ end
@@ -0,0 +1,148 @@
1
+ class Bookbinder::Transform::EPUB_CoverPage < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [Bookbinder::Transform::EPUB_Spine]
5
+ end
6
+
7
+
8
+ # A: look in the Nav (if it exists) for a landmark li with an
9
+ # epub:type of 'cover', and find the spine item with that href.
10
+ #
11
+ # B: look for an OPF <guide><reference type="cover"> and find
12
+ # the spine item with the same href.
13
+ #
14
+ # C: look at the first spine item:
15
+ # - is it have /cover/ in the filename?
16
+ # - no? does it have an image and no body text?
17
+ # - no? does it have an svg and no body text?
18
+ #
19
+ # -> If found, add to map['nav']['landmarks'] with a 'type'
20
+ # of 'cover'.
21
+ #
22
+ def to_map(package)
23
+ cover_page_item =
24
+ cover_page_item_from_nav(package) ||
25
+ cover_page_item_from_opf_guide(package) ||
26
+ cover_page_item_from_first_spine_item(package)
27
+
28
+ if cover_page_item
29
+ package.map['nav'] ||= {}
30
+ package.map['nav']['landmarks'] ||= []
31
+ package.map['nav']['landmarks'].unshift(cover_page_item)
32
+ package.map['spine'].each { |item|
33
+ if item['path'] == cover_page_item['path']
34
+ item['linear'] = false
35
+ end
36
+ }
37
+ end
38
+ end
39
+
40
+
41
+ # Do nothing unless we have a map['nav']['landmark'] type='cover'.
42
+ #
43
+ # In the Nav (if it exists), create a landmark with an
44
+ # epub:type of 'cover'. Actually, don't -- let the landmarks feature
45
+ # handle this.
46
+ #
47
+ # In the OPF, create a <guide> element if it doesn't exist,
48
+ # and create a <reference type="cover" title="Cover" href="..."> tag
49
+ # within it.
50
+ #
51
+ def from_map(package)
52
+ return unless package.map['nav'] && package.map['nav']['landmarks']
53
+ cover_page_item = package.map['nav']['landmarks'].detect { |item|
54
+ item['type'] == 'cover'
55
+ }
56
+ return unless cover_page_item
57
+
58
+ opf_doc = package.file(:opf).document
59
+ unless guide_tag = opf_doc.find('opf|guide')
60
+ guide_tag = opf_doc.new_node('guide', :append => opf_doc.root)
61
+ end
62
+
63
+ opf_doc.new_node('reference', :append => guide_tag) { |ref_tag|
64
+ ref_tag['type'] = 'cover'
65
+ ref_tag['href'] = package.make_href(cover_page_item['path'])
66
+ ref_tag['title'] = cover_page_item['title']
67
+ }
68
+ end
69
+
70
+
71
+ protected
72
+
73
+ # Look for an EPUB3 landmark with type 'cover'.
74
+ #
75
+ def cover_page_item_from_nav(package)
76
+ return unless nav_file = package.file(:nav)
77
+ nav_doc = nav_file.document('r')
78
+ if li = nav_doc.find('nav[epub|type="landmark"] li[epub|type="cover"]')
79
+ href_to_cover_page_item(
80
+ package,
81
+ li['href'],
82
+ li['title'] || li.content.strip
83
+ )
84
+ end
85
+ end
86
+
87
+
88
+ # Look for a guide reference with type 'cover' in the OPF.
89
+ #
90
+ def cover_page_item_from_opf_guide(package)
91
+ opf_doc = package.file(:opf).document('r')
92
+ if guide_ref_tag = opf_doc.find('opf|guide > opf|reference[type="cover"]')
93
+ href_to_cover_page_item(package, guide_ref_tag['href'])
94
+ end
95
+ end
96
+
97
+
98
+ # Investigate whether the first spine item is a cover page.
99
+ #
100
+ def cover_page_item_from_first_spine_item(package)
101
+ spine = package.map['spine']
102
+ if spine.any? && file_path = package.map['spine'].first['path']
103
+ file_href = package.make_href(file_path)
104
+ if file_path.match(/cover/)
105
+ return href_to_cover_page_item(package, file_href)
106
+ end
107
+ package.if_file(file_path) { |cmpt_file|
108
+ if cmpt_doc = cmpt_file.document('r')
109
+ body = cmpt_doc.find('body')
110
+ nodes = body.xpath('.//text()[normalize-space()]')
111
+ # If the body has no text...
112
+ if nodes.empty?
113
+ # ...and it has an <img> or an <svg>...
114
+ if cmpt_doc.find('body img') || cmpt_doc.find('body svg')
115
+ # ...we'll treat it as a cover page.
116
+ return href_to_cover_page_item(package, file_href)
117
+ end
118
+ end
119
+ end
120
+ }
121
+ end
122
+ end
123
+
124
+
125
+ # Given a href for a cover page, create the cover page item
126
+ # that will go into the landmarks array in the package map.
127
+ #
128
+ def href_to_cover_page_item(package, cover_href, page_title = nil)
129
+ cover_href = cover_href.sub(/#.*$/, '')
130
+ cover_page_path = package.make_path(cover_href)
131
+ if cover_page_file = package.file(cover_page_path)
132
+ if doc = cover_page_file.document('r')
133
+ unless page_title
134
+ title_tag = cover_page_file.document('r').find('head > title')
135
+ page_title = title_tag ? title_tag.content.strip : 'Cover page'
136
+ end
137
+ {
138
+ 'type' => 'cover',
139
+ 'path' => cover_page_path,
140
+ 'title' => page_title,
141
+ # FIXME: acquire the real media-type from manifest item?
142
+ 'media-type' => cover_page_file.media_type
143
+ }
144
+ end
145
+ end
146
+ end
147
+
148
+ end
@@ -0,0 +1,67 @@
1
+ class Bookbinder::Transform::EPUB_Creator < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [Bookbinder::Transform::EPUB_Metadata]
5
+ end
6
+
7
+
8
+ def to_map(package)
9
+ return unless package.map['metadata']
10
+ return unless metadata_array = package.map['metadata'][actor_type]
11
+ actors = []
12
+ actor_data = metadata_array.sort { |a, b|
13
+ if a['display-seq'] && b['display-seq']
14
+ a['display-seq']['@'].to_i <=> b['display-seq']['@'].to_i
15
+ else
16
+ 0
17
+ end
18
+ }
19
+ actor_data.each { |cdata|
20
+ actor = {
21
+ 'name' => cdata['@'],
22
+ 'role' => cdata['role'] ? cdata['role']['@'] : 'aut',
23
+ }
24
+ actor['file-as'] = cdata['file-as']['@'] if cdata['file-as']
25
+ # Eh, 'alternate-script' is too messy for now.
26
+
27
+ metadata_array.delete(cdata)
28
+ actors.push(actor)
29
+ }
30
+ package.map['metadata'].delete(actor_type) if metadata_array.empty?
31
+ package.map[actor_type] = actors
32
+ end
33
+
34
+
35
+ def from_map(package)
36
+ return unless actors = package.map[actor_type]
37
+ opf_doc = package.file(:opf).document
38
+ metadata_tag = opf_doc.find('opf|metadata')
39
+ actors.each_with_index { |actor, seq|
40
+ actor_tag = opf_doc.new_node('dc:'+actor_type, :append => metadata_tag)
41
+ actor_tag.content = actor['name']
42
+ if actors.length > 1
43
+ seq += 1
44
+ actor_id = "dc-#{actor_type}-metadata-#{seq}"
45
+ actor_tag['id'] = actor_id
46
+ opf_doc.new_node('meta', :append => metadata_tag) { |role_meta_tag|
47
+ role_meta_tag.content = actor['role']
48
+ role_meta_tag['property'] = 'role'
49
+ role_meta_tag['refines'] = '#'+actor_id
50
+ }
51
+ opf_doc.new_node('meta', :append => metadata_tag) { |seq_meta_tag|
52
+ seq_meta_tag.content = seq
53
+ seq_meta_tag['property'] = 'display-seq'
54
+ seq_meta_tag['refines'] = '#'+actor_id
55
+ }
56
+ end
57
+ }
58
+ end
59
+
60
+
61
+ protected
62
+
63
+ def actor_type
64
+ 'creator'
65
+ end
66
+
67
+ end
@@ -0,0 +1,43 @@
1
+ class Bookbinder::Transform::EPUB_Description < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [Bookbinder::Transform::EPUB_Metadata]
5
+ end
6
+
7
+
8
+ def to_map(package)
9
+ desc_hashes = package.map['metadata'].delete('description')
10
+ descs = []
11
+ if desc_hashes && desc_hashes.any?
12
+ descs = desc_hashes.collect { |dh| dh['@'] }
13
+ descs.sort! { |a, b| a.length <=> b.length }
14
+ full = descs.shift
15
+ if full && !full.empty?
16
+ short = descs.shift || first_sentence(full)
17
+ desc = { 'full' => full }
18
+ desc['short'] = short if short
19
+ package.map['description'] = desc
20
+ end
21
+ end
22
+ end
23
+
24
+
25
+ def from_map(package)
26
+ if desc = package.map['description']
27
+ opf_doc = package.file(:opf).document
28
+ opf_doc.new_node('dc:description', :append => 'opf|metadata') { |desc_tag|
29
+ desc_tag.content = desc['full']
30
+ }
31
+ end
32
+ end
33
+
34
+
35
+ protected
36
+
37
+ def first_sentence(str)
38
+ str = Nokogiri::HTML(str).text
39
+ sentences = str.split(/\./)
40
+ sentences.first+'.' if sentences.any?
41
+ end
42
+
43
+ end
@@ -0,0 +1,29 @@
1
+ class Bookbinder::Transform::EPUB_Language < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [Bookbinder::Transform::EPUB_Metadata]
5
+ end
6
+
7
+
8
+ def to_map(package)
9
+ lang_hashes = package.map['metadata'].delete('language')
10
+ if lang_hashes && lang_hashes.any?
11
+ package.map['language'] = lang_hashes.collect { |lh| lh['@'] }
12
+ else
13
+ package.warn('No <dc:language> found in OPF metadata')
14
+ end
15
+ end
16
+
17
+
18
+ def from_map(package)
19
+ if langs = package.map['language']
20
+ opf_doc = package.file(:opf).document
21
+ langs.each { |lang|
22
+ opf_doc.new_node('dc:language', :append => 'opf|metadata') { |lang_tag|
23
+ lang_tag.content = lang
24
+ }
25
+ }
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,140 @@
1
+ # Takes all the elements in the <metadata> tag of the OPF file and
2
+ # generates a clean hash of the data. Other transforms will pick and
3
+ # choose from this hash. What's left over is mostly just for
4
+ # preserving EPUB's guff.
5
+ #
6
+ # So this:
7
+ #
8
+ # <metadata>
9
+ # <dc:identifier id="isbn-id">
10
+ # urn:isbn:9780101010101
11
+ # </dc:identifier>
12
+ # <meta
13
+ # refines="#isbn-id"
14
+ # property="identifier-type"
15
+ # scheme="onix:codelist5"
16
+ # >
17
+ # 15
18
+ # </meta>
19
+ # <dc:source id="src-id">
20
+ # urn:isbn:9780375704024
21
+ # </dc:source>
22
+ # <meta
23
+ # refines="#src-id"
24
+ # property="identifier-type"
25
+ # scheme="onix:codelist5"
26
+ # >
27
+ # 15
28
+ # </meta>
29
+ # </metadata>
30
+ #
31
+ # ... becomes this:
32
+ #
33
+ # "metadata": {
34
+ # "identifier": [{
35
+ # "@": "urn:isbn:9780101010101",
36
+ # "identifier-type": {
37
+ # "@": 15,
38
+ # "scheme": {
39
+ # "@": "onix-codelist5"
40
+ # }
41
+ # }
42
+ # }],
43
+ # "source": [{
44
+ # "@": "urn:isbn:9780375704024",
45
+ # "identifier-type": {
46
+ # "@": 15,
47
+ # "scheme": {
48
+ # "@": "onix-codelist5"
49
+ # }
50
+ # }
51
+ # }]
52
+ # }
53
+ #
54
+ # You can nest properties arbitrarily deep in the map's "metadata" structure.
55
+ #
56
+ # "metadata": {
57
+ # "some-property": [{
58
+ # "@": "value-of-some-property",
59
+ # "some-property-property": {
60
+ # "@": "value-of-some-property-property",
61
+ # "some-property-property-property": {
62
+ # "@": "value-of-etc"
63
+ # }
64
+ # }
65
+ # }],
66
+ # "other-property": [
67
+ # { "@": "other-property-first-value" },
68
+ # { "@": "other-property-second-value" }
69
+ # ]
70
+ # }
71
+ #
72
+ class Bookbinder::Transform::EPUB_Metadata < Bookbinder::Transform
73
+
74
+
75
+ def dependencies
76
+ [Bookbinder::Transform::EPUB_OPF]
77
+ end
78
+
79
+
80
+ def to_map(package)
81
+ opf_doc = package.file(:opf).document('r')
82
+ md = {}
83
+ opf_doc.each('opf|metadata > *') { |tag|
84
+ # If this tag refines another metadata tag, we can skip it
85
+ next if tag['refines'] && opf_doc.find('opf|metadata > '+tag['refines'])
86
+
87
+ # Find the basic name and value of the meta tag
88
+ name = tag.node_name
89
+ value = { '@' => tag.content.strip }
90
+ # If the meta tag has attributes, add them to the value
91
+ # (but note that these are deprecated in EPUB3 in favor
92
+ # of refinements).
93
+ tag.attributes.each_pair { |key, attr|
94
+ value[key] = { '@' => attr.value }
95
+ value[key]['deprecated'] = true unless key == 'id' || key == 'refines'
96
+ }
97
+ if name == 'meta'
98
+ if tag['property']
99
+ name = tag['property']
100
+ value.delete('property')
101
+ elsif tag['name']
102
+ # Note that <meta name="" content=""> is deprecated in EPUB3.
103
+ name = tag['name']
104
+ value['@'] = tag['content']
105
+ value['deprecated'] = true
106
+ value.delete('name')
107
+ end
108
+ end
109
+ # If the meta tag is refined by another meta tag, add the
110
+ # refinements to our value hash.
111
+ if tag['id']
112
+ refines = opf_doc.search(
113
+ 'opf|metadata > opf|meta[refines="#'+tag['id']+'"]'
114
+ )
115
+ refines.each { |refinement|
116
+ refine_value = { '@' => refinement.content.strip }
117
+ refinement.attributes.each_pair { |key, attr|
118
+ refine_value[key] = attr.value
119
+ }
120
+ value[refinement['property']] = refine_value
121
+ }
122
+ end
123
+ # Add the name and value to our metadata hash
124
+ add_to_hash(md, name, value)
125
+ }
126
+ package.map['metadata'] = md
127
+ end
128
+
129
+
130
+ def from_map(package)
131
+ end
132
+
133
+
134
+ protected
135
+
136
+ def add_to_hash(hash, key, val)
137
+ hash[key] = [hash[key], val].flatten.compact
138
+ end
139
+
140
+ end
@@ -0,0 +1,60 @@
1
+ class Bookbinder::Transform::EPUB_Nav < Bookbinder::Transform
2
+
3
+ DEFAULT_FILE_NAME = 'book-nav.xhtml'
4
+
5
+ def dependencies
6
+ [Bookbinder::Transform::EPUB_Resources]
7
+ end
8
+
9
+
10
+ def to_map(package)
11
+ opf_doc = package.file(:opf).document('r')
12
+ if nav_item = opf_doc.find('opf|manifest > opf|item[properties~="nav"]')
13
+ nav_path = package.make_path(nav_item['href'])
14
+ package.file_aliases[:nav] = nav_path
15
+ package.map['resources'].delete_if { |rsrc|
16
+ package.file_path(rsrc['path']) == nav_path
17
+ }
18
+ end
19
+ end
20
+
21
+
22
+ def from_map(package)
23
+ if package.options['nav_file'] != false
24
+ stub_nav(package)
25
+ add_to_opf_manifest(package)
26
+ end
27
+ end
28
+
29
+
30
+ protected
31
+
32
+ def stub_nav(package)
33
+ package.file_aliases[:nav] = DEFAULT_FILE_NAME
34
+ package.file(:nav).new_xml_document { |doc, x|
35
+ x.doc.create_internal_subset('html', nil, nil)
36
+ x.html {
37
+ doc.add_node_namespace(x.parent, 'xhtml', true)
38
+ doc.add_node_namespace(x.parent, 'epub')
39
+ x.head {
40
+ x.meta('charset' => 'utf-8')
41
+ x.style('ol { list-style: none; }')
42
+ }
43
+ x.body
44
+ }
45
+ }
46
+ end
47
+
48
+
49
+ def add_to_opf_manifest(package)
50
+ opf_file = package.file(:opf)
51
+ nav_file = package.file(:nav)
52
+ opf_file.document.new_node('item', :append => 'opf|manifest') { |item_tag|
53
+ item_tag['href'] = package.make_href(DEFAULT_FILE_NAME)
54
+ item_tag['id'] = package.make_id(DEFAULT_FILE_NAME)
55
+ item_tag['properties'] = 'nav'
56
+ item_tag['media-type'] = nav_file.media_type
57
+ }
58
+ end
59
+
60
+ end