mint 0.2.9 → 0.5.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.
@@ -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