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.
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