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,73 @@
|
|
1
|
+
Feature: Publish document with varying options at the command line
|
2
|
+
As a writer
|
3
|
+
I want to create a document at the command line from plain text
|
4
|
+
So that I can view a typeset version in a web browser
|
5
|
+
|
6
|
+
Background:
|
7
|
+
Given a file named "content.md" with:
|
8
|
+
"""
|
9
|
+
Header
|
10
|
+
======
|
11
|
+
|
12
|
+
This is a test. It is theoretically formatted in
|
13
|
+
the Markdown templating language.
|
14
|
+
"""
|
15
|
+
|
16
|
+
And a file named "style.sass" with:
|
17
|
+
"""
|
18
|
+
p
|
19
|
+
margin: 0
|
20
|
+
"""
|
21
|
+
|
22
|
+
And a file named "layout.haml" with:
|
23
|
+
"""
|
24
|
+
%html
|
25
|
+
%head
|
26
|
+
%link(rel='stylesheet' href=stylesheet)
|
27
|
+
|
28
|
+
%body
|
29
|
+
#container= content
|
30
|
+
"""
|
31
|
+
|
32
|
+
Scenario: Publish document with defaults
|
33
|
+
When I run `mint publish content.md`
|
34
|
+
Then a file named "content.md" should exist
|
35
|
+
And a file named "content.html" should exist
|
36
|
+
|
37
|
+
Scenario Outline: Publish document with named template, layout & style
|
38
|
+
When I run `mint publish <template> <layout> <style> content.md`
|
39
|
+
Then a file named "content.html" should exist
|
40
|
+
And the file "content.html" should contain "This is a test"
|
41
|
+
And a file named "<style file>" should exist
|
42
|
+
And the file "content.html" should match /templates.*style.css/
|
43
|
+
And the file "content.html" should contain "<style file>"
|
44
|
+
|
45
|
+
Examples:
|
46
|
+
| template | layout | style | style file |
|
47
|
+
| | | | ../../templates/default/css/style.css |
|
48
|
+
| -t pro | | | ../../templates/pro/css/style.css |
|
49
|
+
| | -l pro | -s pro | ../../templates/pro/css/style.css |
|
50
|
+
|
51
|
+
Scenario: Publish document with non-existent template
|
52
|
+
When I run `mint publish -t nonexistent content.md`
|
53
|
+
Then the stderr should contain "Template 'nonexistent' does not exist."
|
54
|
+
|
55
|
+
Scenario: Publish document in directory
|
56
|
+
When I run `mint publish content.md -d compiled`
|
57
|
+
Then a file named "compiled/content.html" should exist
|
58
|
+
|
59
|
+
Scenario: Publish document in subdirectory
|
60
|
+
When I run `mint publish content.md -d compiled/chapter-1`
|
61
|
+
Then a file named "compiled/chapter-1/content.html" should exist
|
62
|
+
|
63
|
+
Scenario: Publish document with default style and explicit style destination
|
64
|
+
When I run `mint publish content.md -n styles`
|
65
|
+
Then a file named "styles/style.css" should exist
|
66
|
+
|
67
|
+
Scenario: Publish document with hand-crafted style and explicit style destination
|
68
|
+
When I run `mint publish content.md -n styles -s style.sass`
|
69
|
+
Then a file named "styles/style.css" should exist
|
70
|
+
|
71
|
+
Scenario: Publish document with hand-crafted layout
|
72
|
+
When I run `mint publish content.md -l layout.haml`
|
73
|
+
Then the file "content.html" should match /id=['"]container['"]/
|
data/features/support/env.rb
CHANGED
data/lib/mint.rb
CHANGED
data/lib/mint/commandline.rb
CHANGED
@@ -144,7 +144,7 @@ module Mint
|
|
144
144
|
config_directory = Mint.path_for_scope(scope, true)
|
145
145
|
config_file = config_directory + Mint.files[:config]
|
146
146
|
Helpers.ensure_directory config_directory
|
147
|
-
Helpers.update_yaml opts, config_file
|
147
|
+
Helpers.update_yaml! opts, config_file
|
148
148
|
end
|
149
149
|
|
150
150
|
# Tries to set a config option (at the specified scope) per
|
@@ -184,7 +184,7 @@ module Mint
|
|
184
184
|
# @param [Hash, #[]] commandline_options a structured set of configuration options
|
185
185
|
# that will guide Mint.publish!
|
186
186
|
# @return [void]
|
187
|
-
def self.
|
187
|
+
def self.publish!(files, commandline_options)
|
188
188
|
documents = []
|
189
189
|
options = configuration_with commandline_options
|
190
190
|
|
@@ -194,7 +194,7 @@ module Mint
|
|
194
194
|
# change detection
|
195
195
|
render_style = true
|
196
196
|
files.each do |file|
|
197
|
-
Document.new(file, options).publish!
|
197
|
+
Document.new(file, options).publish! :render_style => render_style
|
198
198
|
render_style = false
|
199
199
|
end
|
200
200
|
end
|
data/lib/mint/document.rb
CHANGED
@@ -4,9 +4,12 @@ require 'mint/style'
|
|
4
4
|
|
5
5
|
module Mint
|
6
6
|
class Document < Resource
|
7
|
+
METADATA_DELIM = "\n\n"
|
8
|
+
|
7
9
|
# Implicit readers are paired with explicit accessors. This
|
8
10
|
# allows for processing variables before storing them.
|
9
11
|
attr_reader :content, :layout, :style
|
12
|
+
attr_accessor :metadata
|
10
13
|
|
11
14
|
# Passes content through a renderer before assigning it to be
|
12
15
|
# the Document's content
|
@@ -15,7 +18,16 @@ module Mint
|
|
15
18
|
# from a templating language into HTML
|
16
19
|
# @return [void]
|
17
20
|
def content=(content)
|
18
|
-
|
21
|
+
tempfile = Helpers.generate_temp_file! content
|
22
|
+
original_content = File.read content
|
23
|
+
|
24
|
+
metadata, text = Document.parse_metadata_from original_content
|
25
|
+
self.metadata = metadata
|
26
|
+
intermediate_content = Mint.before_render text
|
27
|
+
|
28
|
+
File.open(tempfile, 'w') {|file| file << intermediate_content }
|
29
|
+
|
30
|
+
@renderer = Mint.renderer tempfile
|
19
31
|
@content = @renderer.render
|
20
32
|
end
|
21
33
|
|
@@ -71,7 +83,7 @@ module Mint
|
|
71
83
|
# The style_destination attribute is lazy. It's exposed via
|
72
84
|
# virtual attributes like #style_destination_file.
|
73
85
|
attr_reader :style_destination
|
74
|
-
|
86
|
+
|
75
87
|
# @param [String] style_destination the subdirectory into
|
76
88
|
# which styles will be rendered or copied
|
77
89
|
# @return [void]
|
@@ -153,12 +165,14 @@ module Mint
|
|
153
165
|
|
154
166
|
# Renders content in the context of layout and returns as a String.
|
155
167
|
def render(args={})
|
156
|
-
layout.render self, args
|
168
|
+
intermediate_content = layout.render self, args
|
169
|
+
Mint.after_render(intermediate_content)
|
157
170
|
end
|
158
171
|
|
159
172
|
# Writes all rendered content where a) possible, b) required,
|
160
173
|
# and c) specified. Outputs to specified file.
|
161
|
-
def publish!(
|
174
|
+
def publish!(opts={})
|
175
|
+
options = { :render_style => true }.merge(opts)
|
162
176
|
FileUtils.mkdir_p self.destination_directory
|
163
177
|
File.open(self.destination_file, 'w+') do |f|
|
164
178
|
f << self.render
|
@@ -168,12 +182,14 @@ module Mint
|
|
168
182
|
# b) it actually needs rendering (i.e., it's in template form and
|
169
183
|
# not raw, browser-parseable CSS) or it if it doesn't need
|
170
184
|
# rendering but there is an explicit style_destination.
|
171
|
-
if render_style
|
185
|
+
if options[:render_style]
|
172
186
|
FileUtils.mkdir_p style_destination_directory
|
173
187
|
File.open(self.style_destination_file, 'w+') do |f|
|
174
188
|
f << self.style.render
|
175
189
|
end
|
176
190
|
end
|
191
|
+
|
192
|
+
Mint.after_publish(self, opts)
|
177
193
|
end
|
178
194
|
|
179
195
|
# Convenience methods for views
|
@@ -184,5 +200,30 @@ module Mint
|
|
184
200
|
Helpers.normalize_path(self.style_destination_file,
|
185
201
|
self.destination_directory).to_s
|
186
202
|
end
|
203
|
+
|
204
|
+
protected
|
205
|
+
|
206
|
+
def self.metadata_chunk(text)
|
207
|
+
text.split(METADATA_DELIM).first
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.metadata_from(text)
|
211
|
+
raw_metadata = YAML.load metadata_chunk(text)
|
212
|
+
raw_metadata.is_a?(String) ? {} : raw_metadata
|
213
|
+
rescue
|
214
|
+
{}
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.parse_metadata_from(text)
|
218
|
+
metadata = metadata_from text
|
219
|
+
new_text =
|
220
|
+
if !metadata.empty?
|
221
|
+
text.sub metadata_chunk(text) + METADATA_DELIM, ''
|
222
|
+
else
|
223
|
+
text
|
224
|
+
end
|
225
|
+
|
226
|
+
[metadata, new_text]
|
227
|
+
end
|
187
228
|
end
|
188
229
|
end
|
data/lib/mint/helpers.rb
CHANGED
@@ -1,8 +1,23 @@
|
|
1
1
|
require 'pathname'
|
2
|
+
require 'tempfile'
|
2
3
|
require 'yaml'
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
3
5
|
|
4
6
|
module Mint
|
5
7
|
module Helpers
|
8
|
+
def self.underscore(obj, opts={})
|
9
|
+
namespaces = obj.to_s.split('::').map do |namespace|
|
10
|
+
if opts[:ignore_prefix]
|
11
|
+
namespace[0..1].downcase + namespace[2..-1]
|
12
|
+
else
|
13
|
+
namespace
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
string = opts[:namespaces] ? namespaces.join('::') : namespaces.last
|
18
|
+
string.underscore
|
19
|
+
end
|
20
|
+
|
6
21
|
# Transforms a String into a URL-ready slug. Properly handles
|
7
22
|
# ampersands, non-alphanumeric characters, extra hyphens and spaces.
|
8
23
|
#
|
@@ -41,12 +56,14 @@ module Mint
|
|
41
56
|
#
|
42
57
|
# @param [Hash, #[]] map a potentially nested Hash containing symbolizable keys
|
43
58
|
# @return [Hash] a version of map where all keys are symbols
|
44
|
-
def self.symbolize_keys(map)
|
59
|
+
def self.symbolize_keys(map, opts={})
|
60
|
+
transform = lambda {|x| opts[:downcase] ? x.downcase : x }
|
61
|
+
|
45
62
|
map.reduce(Hash.new) do |syms,(k,v)|
|
46
|
-
syms[k.to_sym] =
|
63
|
+
syms[transform[k].to_sym] =
|
47
64
|
case v
|
48
65
|
when Hash
|
49
|
-
self.symbolize_keys(v)
|
66
|
+
self.symbolize_keys(v, opts)
|
50
67
|
else
|
51
68
|
v
|
52
69
|
end
|
@@ -54,6 +71,38 @@ module Mint
|
|
54
71
|
end
|
55
72
|
end
|
56
73
|
|
74
|
+
def self.listify(list)
|
75
|
+
if list.length > 2
|
76
|
+
list[0..-2].join(', ') + ' & ' + list.last
|
77
|
+
else
|
78
|
+
list.join(' & ')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.standardize(metadata, opts={})
|
83
|
+
table = opts[:table] || {}
|
84
|
+
metadata.reduce({}) do |hash, (key,value)|
|
85
|
+
if table[key] && table[key].length == 2
|
86
|
+
standard_key, standard_type = table[key]
|
87
|
+
standard_value =
|
88
|
+
case standard_type
|
89
|
+
when :array
|
90
|
+
[*value]
|
91
|
+
when :string
|
92
|
+
value
|
93
|
+
else
|
94
|
+
# If key/type were not in table
|
95
|
+
value
|
96
|
+
end
|
97
|
+
|
98
|
+
hash[standard_key] = standard_value
|
99
|
+
else
|
100
|
+
hash[key] = value
|
101
|
+
end
|
102
|
+
hash
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
57
106
|
# Returns the relative path to to_directory from from_directory.
|
58
107
|
# If to_directory and from_directory have no parents in common besides
|
59
108
|
# /, returns the absolute directory of to_directory. Assumes no symlinks.
|
@@ -77,12 +126,24 @@ module Mint
|
|
77
126
|
# @param [Hash, #[]] new_opts a set of options to add to the Yaml file
|
78
127
|
# @param [Pathname, #exist] file a file to read from and write to
|
79
128
|
# @return [void]
|
80
|
-
def self.update_yaml(new_opts, file)
|
129
|
+
def self.update_yaml!(new_opts, file)
|
81
130
|
curr_opts = file.exist? ? YAML.load_file(file) : {}
|
82
131
|
|
83
132
|
File.open file, 'w' do |f|
|
84
133
|
YAML.dump(curr_opts.merge(new_opts), f)
|
85
134
|
end
|
86
135
|
end
|
136
|
+
|
137
|
+
def self.generate_temp_file!(file)
|
138
|
+
basename = File.basename file
|
139
|
+
extension = File.extname file
|
140
|
+
content = File.read file
|
141
|
+
|
142
|
+
tempfile = Tempfile.new([basename, extension])
|
143
|
+
tempfile << content
|
144
|
+
tempfile.flush
|
145
|
+
tempfile.close
|
146
|
+
tempfile.path
|
147
|
+
end
|
87
148
|
end
|
88
149
|
end
|
data/lib/mint/mint.rb
CHANGED
@@ -141,11 +141,10 @@ module Mint
|
|
141
141
|
find_files = lambda {|x| Pathname.glob "#{x.to_s}.*" }
|
142
142
|
acceptable = lambda {|x| x.to_s =~ /#{Mint.formats.join '|'}/ }
|
143
143
|
|
144
|
-
|
145
|
-
select(&acceptable).select(&:exist?).first.
|
146
|
-
|
147
|
-
|
148
|
-
template
|
144
|
+
Mint.path(true).map(&file_name).map(&find_files).flatten.
|
145
|
+
select(&acceptable).select(&:exist?).first.tap do |template|
|
146
|
+
raise TemplateNotFoundException unless template
|
147
|
+
end.to_s
|
149
148
|
end
|
150
149
|
|
151
150
|
# Checks (non-rigorously) to see if the file is somewhere on the
|
@@ -185,7 +184,7 @@ module Mint
|
|
185
184
|
#
|
186
185
|
# @param [Document] document a Mint document
|
187
186
|
# @return [void]
|
188
|
-
def self.publish!(document)
|
189
|
-
document.publish!
|
187
|
+
def self.publish!(document, opts={})
|
188
|
+
document.publish! opts
|
190
189
|
end
|
191
190
|
end
|
data/lib/mint/plugin.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'mint/document'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Mint
|
5
|
+
def self.plugins
|
6
|
+
@@plugins ||= Set.new
|
7
|
+
@@plugins.to_a
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.activated_plugins
|
11
|
+
@@activated_plugins ||= Set.new
|
12
|
+
@@activated_plugins.to_a
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.register_plugin!(plugin)
|
16
|
+
@@plugins ||= Set.new
|
17
|
+
@@plugins << plugin
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.activate_plugin!(plugin)
|
21
|
+
@@activated_plugins ||= Set.new
|
22
|
+
@@activated_plugins << plugin
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.clear_plugins!
|
26
|
+
defined?(@@plugins) && @@plugins.clear
|
27
|
+
defined?(@@activated_plugins) && @@activated_plugins.clear
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.template_directory(plugin)
|
31
|
+
Mint.root + '/plugins/templates/' + plugin.underscore
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.config_directory(plugin)
|
35
|
+
Mint.root + '/plugins/config/' + plugin.underscore
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.commandline_options_file(plugin)
|
39
|
+
plugin.config_directory + '/syntax.yml'
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.commandline_name(plugin)
|
43
|
+
plugin.underscore
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.before_render(plain_text, opts={})
|
47
|
+
active_plugins = opts[:plugins] || Mint.activated_plugins
|
48
|
+
active_plugins.reduce(plain_text) do |intermediate, plugin|
|
49
|
+
plugin.before_render(intermediate)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.after_render(html_text, opts={})
|
54
|
+
active_plugins = opts[:plugins] || Mint.activated_plugins
|
55
|
+
active_plugins.reduce(html_text) do |intermediate, plugin|
|
56
|
+
plugin.after_render(intermediate)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.after_publish(document, opts={})
|
61
|
+
active_plugins = opts[:plugins] || Mint.activated_plugins
|
62
|
+
active_plugins.each do |plugin|
|
63
|
+
plugin.after_publish(document)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Plugin
|
68
|
+
def self.inherited(plugin)
|
69
|
+
Mint.register_plugin! plugin
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.underscore(opts={})
|
73
|
+
opts[:ignore_prefix] ||= true
|
74
|
+
Helpers.underscore self.name, :ignore_prefix => opts[:ignore_prefix]
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.template_directory
|
78
|
+
Mint.template_directory(self)
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.config_directory
|
82
|
+
Mint.config_directory(self)
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.commandline_options_file
|
86
|
+
Mint.commandline_options_file(self)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.commandline_name
|
90
|
+
Mint.commandline_name(self)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Supports:
|
94
|
+
# - Change raw text
|
95
|
+
#
|
96
|
+
# Use cases:
|
97
|
+
# - Add footnote syntax on top of Markdown
|
98
|
+
# - Perform text analysis for use in later callbacks (?)
|
99
|
+
def self.before_render(text_document)
|
100
|
+
text_document
|
101
|
+
end
|
102
|
+
|
103
|
+
# Supports:
|
104
|
+
# - Change preview HTML
|
105
|
+
#
|
106
|
+
# Use cases:
|
107
|
+
# - Transform elements based on position or other HTML attributes
|
108
|
+
# For example: Add a class to the first paragraph of a document if it is
|
109
|
+
# italicized
|
110
|
+
#
|
111
|
+
# Questions:
|
112
|
+
# - Could I allow jQuery use here?
|
113
|
+
def self.after_render(html_document)
|
114
|
+
html_document
|
115
|
+
end
|
116
|
+
|
117
|
+
# Supports:
|
118
|
+
# - Change file, filesystem once written
|
119
|
+
# - Automatic cleanup of intermediate files, including all edge cases
|
120
|
+
# currently covered by transformation library. (For example, if I generated
|
121
|
+
# a CSS file but am ultimately generating a PDF, I would want to have
|
122
|
+
# an automatic way to delete that CSS file.)
|
123
|
+
#
|
124
|
+
# Use cases:
|
125
|
+
# - Zip set of documents into ePub and create manifest
|
126
|
+
# - Change file extension
|
127
|
+
# - Generate PDF from emitted HTML and get rid of intermediate files
|
128
|
+
# - Generate .doc (or any other OO UNO format) and get rid of intermediate files
|
129
|
+
#
|
130
|
+
# NOTE: Unlike the other two callbacks, this doesn't use the result of
|
131
|
+
# the callback expression for anything. This callback is purely for
|
132
|
+
# side effects like rearranging the file system.
|
133
|
+
def self.after_publish(document)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|