mint 0.2.9 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +45 -283
- data/bin/mint +10 -10
- data/bin/mint-epub +23 -0
- data/features/plugins/epub.feature +23 -0
- data/features/publish.feature +73 -0
- data/features/support/env.rb +1 -1
- data/lib/mint.rb +1 -0
- data/lib/mint/commandline.rb +3 -3
- data/lib/mint/document.rb +46 -5
- data/lib/mint/helpers.rb +65 -4
- data/lib/mint/mint.rb +6 -7
- data/lib/mint/plugin.rb +136 -0
- data/lib/mint/plugins/epub.rb +292 -0
- data/lib/mint/version.rb +1 -1
- data/plugins/templates/epub/container.haml +5 -0
- data/plugins/templates/epub/content.haml +36 -0
- data/plugins/templates/epub/layout.haml +6 -0
- data/plugins/templates/epub/title.haml +11 -0
- data/plugins/templates/epub/toc.haml +26 -0
- data/spec/commandline_spec.rb +91 -0
- data/spec/document_spec.rb +48 -9
- data/spec/helpers_spec.rb +231 -0
- data/spec/layout_spec.rb +6 -0
- data/spec/mint_spec.rb +94 -0
- data/spec/plugin_spec.rb +457 -0
- data/spec/plugins/epub_spec.rb +242 -0
- data/spec/resource_spec.rb +135 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/style_spec.rb +69 -0
- metadata +103 -34
- data/features/mint_document.feature +0 -48
- data/features/step_definitions/mint_steps.rb +0 -1
@@ -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
|
data/lib/mint/version.rb
CHANGED
@@ -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,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
|