bb-epub 0.0.1

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.
@@ -0,0 +1,67 @@
1
+ class BbEPUB::Transform::Creator < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [BbEPUB::Transform::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 BbEPUB::Transform::Description < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [BbEPUB::Transform::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 BbEPUB::Transform::Language < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [BbEPUB::Transform::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,143 @@
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 BbEPUB::Transform::Metadata < Bookbinder::Transform
73
+
74
+
75
+ def dependencies
76
+ [BbEPUB::Transform::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
+ if refines = tag['refines']
86
+ refines = "opf|metadata > *[id=\"#{refines.sub(/^#/,'')}\"]"
87
+ next if opf_doc.find(refines)
88
+ end
89
+
90
+ # Find the basic name and value of the meta tag
91
+ name = tag.node_name
92
+ value = { '@' => tag.content.strip }
93
+ # If the meta tag has attributes, add them to the value
94
+ # (but note that these are deprecated in EPUB3 in favor
95
+ # of refinements).
96
+ tag.attributes.each_pair { |key, attr|
97
+ value[key] = { '@' => attr.value }
98
+ value[key]['deprecated'] = true unless key == 'id' || key == 'refines'
99
+ }
100
+ if name == 'meta'
101
+ if tag['property']
102
+ name = tag['property']
103
+ value.delete('property')
104
+ elsif tag['name']
105
+ # Note that <meta name="" content=""> is deprecated in EPUB3.
106
+ name = tag['name']
107
+ value['@'] = tag['content']
108
+ value['deprecated'] = true
109
+ value.delete('name')
110
+ end
111
+ end
112
+ # If the meta tag is refined by another meta tag, add the
113
+ # refinements to our value hash.
114
+ if tag['id']
115
+ refines = opf_doc.search(
116
+ 'opf|metadata > opf|meta[refines="#'+tag['id']+'"]'
117
+ )
118
+ refines.each { |refinement|
119
+ refine_value = { '@' => refinement.content.strip }
120
+ refinement.attributes.each_pair { |key, attr|
121
+ refine_value[key] = attr.value
122
+ }
123
+ value[refinement['property']] = refine_value
124
+ }
125
+ end
126
+ # Add the name and value to our metadata hash
127
+ add_to_hash(md, name, value)
128
+ }
129
+ package.map['metadata'] = md
130
+ end
131
+
132
+
133
+ def from_map(package)
134
+ end
135
+
136
+
137
+ protected
138
+
139
+ def add_to_hash(hash, key, val)
140
+ hash[key] = [hash[key], val].flatten.compact
141
+ end
142
+
143
+ end
@@ -0,0 +1,60 @@
1
+ class BbEPUB::Transform::Nav < Bookbinder::Transform
2
+
3
+ DEFAULT_FILE_NAME = 'book-nav.xhtml'
4
+
5
+ def dependencies
6
+ [BbEPUB::Transform::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
@@ -0,0 +1,177 @@
1
+ class BbEPUB::Transform::NavToc < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [BbEPUB::Transform::Nav, BbEPUB::Transform::NCX]
5
+ end
6
+
7
+
8
+ def to_map(package)
9
+ toc = []
10
+ package.if_file(:nav) { |nav_file|
11
+ nav_doc = nav_file.document('r')
12
+ base_path = content_rooted_path(package.content_root, nav_file.path)
13
+ nav_doc.each('nav[epub|type="toc"] > ol > li') { |li|
14
+ toc.push(extract_nav_chapter(nav_doc, li, base_path))
15
+ }
16
+ }
17
+ unless toc.any?
18
+ package.if_file(:ncx) { |ncx_file|
19
+ ncx_doc = ncx_file.document
20
+ base_path = content_rooted_path(package.content_root, ncx_file.path)
21
+ ncx_doc.each('ncx|navMap > ncx|navPoint') { |point|
22
+ toc.push(extract_ncx_chapter(ncx_doc, point, base_path))
23
+ }
24
+ }
25
+ end
26
+ if toc.any?
27
+ package.map['nav'] ||= {}
28
+ package.map['nav']['toc'] = toc
29
+ end
30
+ end
31
+
32
+
33
+ def from_map(package)
34
+ package.if_file(:ncx) { |ncx_file|
35
+ @max_depth = 0
36
+ @chapter_count = 0
37
+ toc = (package.map['nav'] || {})['toc'] || []
38
+ build_ncx_map(ncx_file.document, toc)
39
+ build_ncx_depth_meta(ncx_file.document, @max_depth)
40
+ }
41
+ package.if_file(:nav) { |nav_file|
42
+ toc = (package.map['nav'] || {})['toc'] || []
43
+ build_nav_tag(nav_file.document, toc)
44
+ }
45
+ end
46
+
47
+
48
+ protected
49
+
50
+ # NAV to_map
51
+
52
+ def extract_nav_chapter(doc, li, base_path)
53
+ {}.tap { |chp|
54
+ content_node = doc.find_within(li, '> a, span')
55
+ chp['title'] = content_node['title'] || content_node.content.strip
56
+ if content_node.node_name.downcase == 'a'
57
+ chp['path'] = resolved_path(base_path, content_node['href'])
58
+ end
59
+ children = doc.search_within(li, '> li')
60
+ if children.any?
61
+ chp['contents'] = children.collect { |child|
62
+ extract_nav_chapter(doc, child, base_path)
63
+ }
64
+ end
65
+ }
66
+ end
67
+
68
+
69
+ # NCX to_map
70
+
71
+ def extract_ncx_chapter(doc, point, base_path)
72
+ {
73
+ 'title' => doc.find_within(point, 'text').text,
74
+ 'path' => resolved_path(
75
+ base_path,
76
+ doc.find_within(point, 'content')['src']
77
+ )
78
+ }.tap { |out|
79
+ children = doc.search_within(point, '> navPoint')
80
+ if children.any?
81
+ out['contents'] = children.collect { |pt|
82
+ extract_ncx_chapter(doc, pt, base_path)
83
+ }
84
+ end
85
+ }
86
+ end
87
+
88
+
89
+ # NAV from_map
90
+
91
+ def build_nav_tag(nav_doc, toc)
92
+ nav_tag_frag = Nokogiri::XML::DocumentFragment.parse('')
93
+ Nokogiri::XML::Builder.with(nav_tag_frag) { |x|
94
+ x.nav('epub:type' => 'toc') { build_nav_ol_for_chapters(x, toc) }
95
+ }
96
+ nav_doc.find('body').add_child(nav_tag_frag)
97
+ end
98
+
99
+
100
+ def build_nav_ol_for_chapters(x, chapters)
101
+ x.ol {
102
+ chapters.each { |chp|
103
+ x.li {
104
+ atts = { 'href' => chp['path'] }
105
+ x.a(chp['title'], atts)
106
+ build_nav_ol_for_chapters(x, chp['contents']) if chp['contents']
107
+ }
108
+ }
109
+ }
110
+ end
111
+
112
+
113
+ # NCX from_map
114
+
115
+ def build_ncx_map(ncx_doc, toc)
116
+ nav_map_frag = Nokogiri::XML::DocumentFragment.parse('')
117
+ Nokogiri::XML::Builder.with(nav_map_frag) { |x|
118
+ x.navMap {
119
+ toc.each { |chp| build_ncx_point_for_chapter(x, chp, 0) }
120
+ }
121
+ }
122
+ ncx_doc.root.add_child(nav_map_frag)
123
+ end
124
+
125
+
126
+ def build_ncx_depth_meta(ncx_doc, max_depth)
127
+ ncx_doc.new_node('meta', :append => 'ncx|head') { |depth_meta_tag|
128
+ depth_meta_tag['name'] = 'dtb:depth'
129
+ depth_meta_tag['content'] = max_depth
130
+ }
131
+ end
132
+
133
+
134
+ def build_ncx_point_for_chapter(x, chp, depth)
135
+ @chapter_count += 1
136
+ depth += 1
137
+ @max_depth = depth if depth > @max_depth
138
+ x.navPoint(
139
+ 'id' => "toc-#{@chapter_count}",
140
+ 'class' => 'chapter',
141
+ 'playOrder' => @chapter_count
142
+ ) {
143
+ x.navLabel { x.text_(chp['title']) }
144
+ x.content('src' => chp['path'])
145
+ if chp['children']
146
+ chp['children'].each { |chd|
147
+ build_ncx_point_for_chapter(x, chd, depth)
148
+ }
149
+ end
150
+ }
151
+ end
152
+
153
+
154
+ # Utilities
155
+ def content_rooted_path(content_root, path)
156
+ File.dirname(path).sub(/^#{content_root}\/?/, '').sub(/^\.$/, '')
157
+ end
158
+
159
+
160
+ def resolved_path(base_path, path)
161
+ relative_path = File.join(base_path, path).sub(/^\//, '')
162
+ relative_parts = relative_path.split('/')
163
+ resolved_parts = []
164
+ while relative_parts.any?
165
+ part = relative_parts.shift
166
+ if part == '.'
167
+ # do nothing
168
+ elsif part == '..'
169
+ resolved_parts.pop
170
+ else
171
+ resolved_parts.push(part)
172
+ end
173
+ end
174
+ File.join(resolved_parts)
175
+ end
176
+
177
+ end
@@ -0,0 +1,63 @@
1
+ class BbEPUB::Transform::NCX < Bookbinder::Transform
2
+
3
+ DEFAULT_FILE_NAME = 'book.ncx'
4
+
5
+
6
+ def dependencies
7
+ [BbEPUB::Transform::Resources]
8
+ end
9
+
10
+
11
+ def to_map(package)
12
+ opf_doc = package.file(:opf).document('r')
13
+ spine = opf_doc.find('opf|spine')
14
+ if spine && ncx_id = spine['toc']
15
+ ncx_item = opf_doc.find('opf|manifest > opf|item[id="'+ncx_id+'"]')
16
+ ncx_path = package.make_path(ncx_item['href'])
17
+ package.file_aliases[:ncx] = ncx_path
18
+ package.map['resources'].delete_if { |rsrc|
19
+ package.file_path(rsrc['path']) == ncx_path
20
+ }
21
+ end
22
+ end
23
+
24
+
25
+ def from_map(package)
26
+ if package.options['ncx_file'] != false
27
+ stub_ncx(package)
28
+ add_to_opf_manifest(package)
29
+ end
30
+ end
31
+
32
+
33
+ protected
34
+
35
+ def stub_ncx(package)
36
+ package.file_aliases[:ncx] = DEFAULT_FILE_NAME
37
+ package.file(:ncx).new_xml_document { |doc, x|
38
+ x.ncx('version' => '2005-1', 'xml:lang' => 'en') {
39
+ doc.add_node_namespace(x.parent, 'ncx', true)
40
+ x.head {
41
+ x.meta(
42
+ 'name' => 'dtb:generator',
43
+ 'content' => "Bookbinder #{Bookbinder::VERSION}"
44
+ )
45
+ }
46
+ }
47
+ }
48
+ end
49
+
50
+
51
+ def add_to_opf_manifest(package)
52
+ opf_file = package.file(:opf)
53
+ ncx_file = package.file(:ncx)
54
+ ncx_id = package.make_id(DEFAULT_FILE_NAME)
55
+ opf_file.document.new_node('item', :append => 'opf|manifest') { |item_tag|
56
+ item_tag['href'] = package.make_href(DEFAULT_FILE_NAME)
57
+ item_tag['media-type'] = ncx_file.media_type
58
+ item_tag['id'] = ncx_id
59
+ }
60
+ opf_file.document.find('opf|spine')['toc'] = ncx_id
61
+ end
62
+
63
+ end
@@ -0,0 +1,33 @@
1
+ class BbEPUB::Transform::OCF < Bookbinder::Transform
2
+
3
+ OCF_PATH = 'META-INF/container.xml'
4
+
5
+ # No dependencies! Something has to have no dependencies, after all.
6
+
7
+ def to_map(package)
8
+ package.file_aliases[:ocf] = OCF_PATH
9
+ ocf = package.file(:ocf, false)
10
+ opf_path = ocf.document('r').find('ocf|rootfile')['full-path']
11
+ package.file_aliases[:opf] = File.basename(opf_path)
12
+ package.content_root = File.dirname(opf_path).sub(/^\./, '')
13
+ end
14
+
15
+
16
+ def from_map(package)
17
+ package.file_aliases[:ocf] = OCF_PATH
18
+ package.file_aliases[:opf] = 'book.opf'
19
+ opf_file = package.file(:opf)
20
+ package.file(:ocf, false).new_xml_document { |doc, x|
21
+ x.container('version' => '1.0') {
22
+ doc.add_node_namespace(x.parent, 'ocf', true)
23
+ x.rootfiles {
24
+ x.rootfile(
25
+ 'full-path' => opf_file.path,
26
+ 'media-type' => opf_file.media_type
27
+ )
28
+ }
29
+ }
30
+ }
31
+ end
32
+
33
+ end
@@ -0,0 +1,22 @@
1
+ class BbEPUB::Transform::OPF < Bookbinder::Transform
2
+
3
+ def dependencies
4
+ [BbEPUB::Transform::OCF]
5
+ end
6
+
7
+
8
+ def from_map(package)
9
+ package.file(:opf).new_xml_document { |opf_doc, x|
10
+ x.package('version' => '3.0') {
11
+ opf_doc.add_node_namespace(x.parent, 'opf', true)
12
+ x.metadata {
13
+ opf_doc.add_node_namespace(x.parent, 'dc')
14
+ opf_doc.add_node_namespace(x.parent, 'dcterms')
15
+ }
16
+ x.manifest
17
+ x.spine
18
+ }
19
+ }
20
+ end
21
+
22
+ end