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
@@ -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,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
|