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.
- 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
|