mint 0.2.9 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,292 @@
1
+ require 'nokogiri'
2
+ require 'hashie'
3
+ require 'zip/zip'
4
+ require 'zip/zipfilesystem'
5
+ require 'active_support/core_ext/hash/deep_merge'
6
+ require 'active_support/core_ext/hash/keys'
7
+
8
+ # Note: This code is not as clean as I want it to be. It is an example
9
+ # plugin with which I'm developing the Mint plugin system. Code cleanup
10
+ # to follow.
11
+
12
+ module Mint
13
+ META_DIR = 'META-INF'
14
+ CONTENT_DIR = 'OPS'
15
+
16
+ # Add chapters to document -- this is probably not a sustainable pattern
17
+ # for all plugins, but it's useful here.
18
+ class Document
19
+ def chapters
20
+ html_document = Nokogiri::HTML::Document.parse render
21
+ EPub.split_on(html_document, 'h2').map &:to_s
22
+ end
23
+ end
24
+
25
+ class InvalidDocumentError < StandardError; end
26
+
27
+ class EPub < Plugin
28
+ def self.after_publish(document)
29
+ # This check doesn't currently follow simlinks
30
+ if document.destination_directory == Dir.getwd
31
+ raise InvalidDocumentError
32
+ end
33
+
34
+ Dir.chdir document.destination_directory do
35
+ metadata = standardize document.metadata
36
+ chapters = document.chapters
37
+ locals = { chapters: chapters }.merge metadata
38
+
39
+ prepare_directory!
40
+ create_chapters! chapters, :locals => metadata
41
+
42
+ create! do |container|
43
+ container.type = 'container'
44
+ container.locals = locals
45
+ end
46
+
47
+ create! do |content|
48
+ content.type = 'content'
49
+ content.locals = locals
50
+ end
51
+
52
+ create! do |toc|
53
+ toc.type = 'toc'
54
+ toc.locals = locals
55
+ end
56
+
57
+ create! do |title|
58
+ title.type = 'title'
59
+ title.locals = locals
60
+ end
61
+ end
62
+
63
+ FileUtils.rm document.destination_file
64
+
65
+ self.zip! document.destination_directory,
66
+ :mimetype => 'application/epub+zip',
67
+ :extension => 'epub'
68
+
69
+ FileUtils.rm_r document.destination_directory
70
+ end
71
+
72
+ protected
73
+
74
+ def self.split_on(document, tag_name, opts={})
75
+ container_node = opts[:container] || '#container'
76
+
77
+ new_document = document.dup.tap do |node|
78
+ container = node.at container_node
79
+
80
+ unless container
81
+ raise InvalidDocumentError,
82
+ "Document doesn't contain expected container: #{container}",
83
+ caller
84
+ end
85
+
86
+ div = nil
87
+ container.element_children.each do |elem|
88
+ if elem.name == tag_name
89
+ div = node.create_element 'div'
90
+ # div.add_class 'chapter'
91
+ elem.replace div
92
+ end
93
+ div << elem if div
94
+ end
95
+ end
96
+
97
+ new_document.search('div div')
98
+ end
99
+
100
+ # This is an opinionated version of ZIP, specifically
101
+ # tailored to ePub creation
102
+ def self.zip!(directory, opts={})
103
+ default_opts = {
104
+ extension: 'zip',
105
+ mimetype: nil
106
+ }
107
+
108
+ opts = default_opts.merge opts
109
+ extension = opts[:extension]
110
+ parent_directory = File.expand_path "#{directory}/.."
111
+ child_directory = File.basename directory
112
+
113
+ Zip::ZipOutputStream.open "#{directory}.#{extension}" do |zos|
114
+ if opts[:mimetype]
115
+ zos.put_next_entry('mimetype', nil, nil, Zip::ZipEntry::STORED)
116
+ zos << opts[:mimetype]
117
+ end
118
+
119
+ Dir.chdir parent_directory do
120
+ Dir["#{child_directory}/**/*"].each do |file|
121
+ if File.file? file
122
+ relative_path = Helpers.normalize_path(file, child_directory)
123
+ zos.put_next_entry(relative_path,
124
+ nil,
125
+ nil,
126
+ Zip::ZipEntry::DEFLATED)
127
+ zos << File.read(file)
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ def self.create!
135
+ options = Hashie::Mash.new
136
+ yield options if block_given?
137
+ options = options.to_hash.symbolize_keys
138
+
139
+ type = options[:type] || 'container'
140
+ default_options =
141
+ case type.to_sym
142
+ when :container
143
+ container_defaults
144
+ when :content
145
+ content_defaults
146
+ when :toc
147
+ toc_defaults
148
+ when :title
149
+ title_defaults
150
+ else
151
+ {}
152
+ end
153
+
154
+ create_from_template! default_options.deep_merge(options)
155
+ end
156
+
157
+ def self.create_chapters!(chapters, opts={})
158
+ opts = chapter_defaults.deep_merge(opts)
159
+ template_file = EPub.template_directory + '/layout.haml'
160
+ renderer = Tilt.new template_file, :ugly => false
161
+ chapters.map do |chapter|
162
+ renderer.render Object.new, opts[:locals].merge(:content => chapter)
163
+ end.each_with_index do |text, id|
164
+ create_chapter!(id + 1, text)
165
+ end
166
+ end
167
+
168
+ private
169
+
170
+ def self.create_from_template!(opts={})
171
+ template_file = EPub.template_directory + "/#{opts[:from]}"
172
+ renderer = Tilt.new template_file, :ugly => false
173
+ content = renderer.render Object.new, opts[:locals]
174
+
175
+ File.open(opts[:to], 'w') do |f|
176
+ f << content
177
+ end
178
+ end
179
+
180
+ def self.prepare_directory!
181
+ [META_DIR, CONTENT_DIR].each do |dir|
182
+ FileUtils.mkdir dir unless File.exist?(dir)
183
+ end
184
+ end
185
+
186
+ def self.locals_lookup_table
187
+ {
188
+ author: [:creators, :array],
189
+ authors: [:creators, :array],
190
+ editor: [:contributors, :array],
191
+ editors: [:contributors, :array],
192
+ barcode: [:uuid, :string],
193
+ upc: [:uuid, :string],
194
+ copyright: [:rights, :string]
195
+ }
196
+ end
197
+
198
+ def self.standardize(metadata)
199
+ sanitized_metadata =
200
+ Helpers.symbolize_keys(metadata, :downcase => true)
201
+ standardized_metadata =
202
+ Helpers.standardize(sanitized_metadata,
203
+ :table => locals_lookup_table)
204
+ end
205
+
206
+ def self.chapter_filename(id)
207
+ "OPS/chapter-#{id}.html"
208
+ end
209
+
210
+ # def self.metadata_from(document)
211
+ # document.metadata
212
+ # end
213
+
214
+ # def self.chapters_from(document)
215
+ # html_text = File.read document.destination_file
216
+ # html_document = Nokogiri::HTML::Document.parse html_text
217
+ # chapter_contents = self.split_on(html_document, 'h2')
218
+ # chapter_ids = (1..chapters.length).map {|x| "chapter-#{x}" }
219
+ # chapters = Hash[chapter_ids.zip chapter_contents]
220
+ # end
221
+
222
+ def self.create_chapter!(id, text)
223
+ File.open chapter_filename(id), 'w' do |file|
224
+ file << text
225
+ end
226
+ end
227
+
228
+ def self.chapter_defaults
229
+ {
230
+ locals: {
231
+ title: 'Untitled'
232
+ }
233
+ }
234
+ end
235
+
236
+ def self.container_defaults
237
+ defaults = {
238
+ from: 'container.haml',
239
+ to: "#{META_DIR}/container.xml",
240
+ locals: {
241
+ opf_file: 'OPS/content.opf'
242
+ }
243
+ }
244
+ end
245
+
246
+ def self.content_defaults
247
+ defaults = {
248
+ from: 'content.haml',
249
+ to: "#{CONTENT_DIR}/content.opf",
250
+ locals: {
251
+ title: 'Untitled',
252
+ language: 'English',
253
+ short_title: '',
254
+ uuid: 'Unspecified',
255
+ description: 'No description',
256
+ date: Date.today,
257
+ creators: ['Anonymous'],
258
+ contributors: [],
259
+ publisher: 'Self published',
260
+ genre: 'Non-fiction',
261
+ rights: 'All Rights Reserved',
262
+ ncx_file: 'toc.ncx',
263
+ style_file: 'style.css',
264
+ title_file: 'title.html',
265
+ }
266
+ }
267
+ end
268
+
269
+ def self.toc_defaults
270
+ defaults = {
271
+ from: 'toc.haml',
272
+ to: "#{CONTENT_DIR}/toc.ncx",
273
+ locals: {
274
+ uuid: 'Unspecified',
275
+ title: 'Untitled',
276
+ title_file: 'title.html',
277
+ }
278
+ }
279
+ end
280
+
281
+ def self.title_defaults
282
+ defaults = {
283
+ from: 'title.haml',
284
+ to: "#{CONTENT_DIR}/title.html",
285
+ locals: {
286
+ title: 'Untitled',
287
+ creators: ['Anonymous']
288
+ }
289
+ }
290
+ end
291
+ end
292
+ end
@@ -1,3 +1,3 @@
1
1
  module Mint
2
- VERSION = '0.2.9'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -0,0 +1,5 @@
1
+ !!! XML
2
+ %container(version='1.0' xmlns='urn:oasis:names:tc:opendocument:xmlns:container')
3
+ %rootfiles
4
+ %rootfile(full-path=opf_file
5
+ media-type='application/oebps-package+xml')
@@ -0,0 +1,36 @@
1
+ !!! XML
2
+ %package(version='2.0' xmlns='http://www.idpf.org/2007/opf' unique-identifier=uuid)
3
+ %metadata{ 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/',
4
+ 'xmlns:dcterms' => 'http://purl.org/dc/terms/',
5
+ 'xmlns:opf' => 'http://www.idpf.org/2007/opf',
6
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance' }
7
+
8
+ %dc:title= title
9
+ %dc:language{ 'xsi:type' => 'dcterms:RFC3066' }= language
10
+ %dc:identifier{ :id => uuid }= short_title
11
+ %dc:description= description
12
+ %dc:date{ 'xsi:type' => 'dcterms:W3CDTF' }= date
13
+
14
+ - creators.each do |creator|
15
+ %dc:creator{ 'opf:file-as' => creator, 'opf:role' => 'aut' }
16
+ - contributors.each do |contributor|
17
+ %dc:contributor{ 'opf:file-as' => contributor,
18
+ 'opf:role' => 'edt' }
19
+
20
+ %dc:publisher= publisher
21
+ %dc:type= genre
22
+ %dc:rights= rights
23
+
24
+ %manifest
25
+ %item(id='ncx' href=ncx_file media-type='application/x-dtbncx+xml')/
26
+ %item(id='style' href=style_file media-type='text/css')/
27
+ %item(id='title' href=title_file media-type='application/xhtml+xml')/
28
+ - chapters.each_with_index do |content, id|
29
+ - idx = id + 1
30
+ %item(id="chapter-#{idx}" href="chapter-#{idx}.html" media-type='application/xhtml+xml')/
31
+
32
+ %spine(toc='ncx')
33
+ %itemref(idref='title')/
34
+ - chapters.each_with_index do |content, id|
35
+ - idx = id + 1
36
+ %itemref(idref="chapter-#{idx}")/
@@ -0,0 +1,6 @@
1
+ !!! 1.1
2
+ %html(xmlns='http://www.w3.org/1999/xhtml')
3
+ %head
4
+ %title= title
5
+ %body
6
+ #container= content
@@ -0,0 +1,11 @@
1
+ !!! 1.1
2
+ %html(xmlns='http://www.w3.org/1999/xhtml')
3
+ %head
4
+ %meta(http-equiv='Content-Type'
5
+ content='text/html; charset=utf-8')
6
+ %title= title
7
+ %link(href='style.css' type='text/css' rel='stylesheet')
8
+
9
+ %body
10
+ %h1= title
11
+ %h2= Mint::Helpers.listify creators
@@ -0,0 +1,26 @@
1
+ !!! XML
2
+ <!DOCTYPE ncx PUBLIC '-//NISO//DTD ncx 2005-1//EN' 'http://www.daisy.org/z3986/2005/ncx-2005-1.dtd'>
3
+
4
+ %ncx(xmlns='http://www.daisy.org/z3986/2005/ncx/' version='2005-1')
5
+ %head
6
+ %meta(name='dtb:uid' content=uuid)
7
+ %meta(name='dtb:depth' content='1')
8
+ %meta(name='dtb:totalPageCount' content='0')
9
+ %meta(name='dtb:maxPageNumber' content='0')
10
+
11
+ %docTitle
12
+ %text= title
13
+
14
+ %navMap
15
+ %navPoint(id='navpoint-1' playOrder=1)
16
+ %navLabel
17
+ %text= title
18
+ %content(src=title_file)
19
+
20
+ - chapters.each_with_index do |content, id|
21
+ - idx = Integer(id) + 1
22
+ - order = idx + 1
23
+ %navPoint(id="navpoint-#{order}" playOrder=order)
24
+ %navLabel
25
+ %text= "Chapter #{idx}"
26
+ %content(src="chapter-#{idx}.html")
@@ -0,0 +1,91 @@
1
+ require 'spec_helper'
2
+
3
+ module Mint
4
+ describe CommandLine do
5
+ describe "#options" do
6
+ it "provides default options" do
7
+ CommandLine.options['template']['long'].should == 'template'
8
+ CommandLine.options['layout']['long'].should == 'layout'
9
+ CommandLine.options['style']['long'].should == 'style'
10
+ end
11
+ end
12
+
13
+ describe "#parser" do
14
+ it "provides a default option parser" do
15
+ fake_argv = ['--layout', 'pro']
16
+
17
+ options = {}
18
+ CommandLine.parser {|k, p| options[k] = p }.parse!(fake_argv)
19
+ options[:layout].should == 'pro'
20
+ end
21
+
22
+ it "provides an option parser based on a formatted hash" do
23
+ fake_argv = ['--novel', 'novel']
24
+ formatted_options = {
25
+ # Option keys must be formatted as strings, so we
26
+ # use hash-bang syntax
27
+ novel: {
28
+ 'short' => 'n',
29
+ 'long' => 'novel',
30
+ 'parameter' => true,
31
+ 'description' => ''
32
+ }
33
+ }
34
+
35
+ options = {}
36
+
37
+ CommandLine.parser(formatted_options) do |k, p|
38
+ options[k] = p
39
+ end.parse!(fake_argv)
40
+
41
+ options[:novel].should == 'novel'
42
+ end
43
+ end
44
+
45
+ describe "#configuration" do
46
+ context "when no config syntax file is loaded" do
47
+ it "returns nil" do
48
+ CommandLine.configuration(nil).should be_nil
49
+ end
50
+ end
51
+
52
+ context "when a config syntax file is loaded but there is no .config file" do
53
+ it "returns a default set of options" do
54
+ expected_map = {
55
+ layout: 'default',
56
+ style: 'default',
57
+ destination: nil,
58
+ style_destination: nil
59
+ }
60
+
61
+ CommandLine.configuration.should == expected_map
62
+ end
63
+ end
64
+
65
+ context "when a config syntax file is loaded and there is a .config file" do
66
+ before do
67
+ FileUtils.mkdir_p '.mint/config'
68
+ File.open('.mint/config/config.yaml', 'w') do |file|
69
+ file << 'layout: pro'
70
+ end
71
+ end
72
+
73
+ after do
74
+ File.delete '.mint/config/config.yaml'
75
+ end
76
+
77
+ it "merges all specified options with precedence according to scope" do
78
+ CommandLine.configuration[:layout].should == 'pro'
79
+ end
80
+ end
81
+ end
82
+
83
+ it "displays the sum of all configuration files with other options added"
84
+ it "prints a help message"
85
+ it "installs a template file to the correct scope"
86
+ it "pulls up a named template file in the user's editor"
87
+ it "writes options to the correct file for the scope specified"
88
+ it "sets and stores a scoped configuration variable"
89
+ it "publishes a set of files"
90
+ end
91
+ end