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,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['"]/
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift File.expand_path('../../../lib', __FILE__)
4
4
 
5
5
  module ArubaOverrides
6
6
  def detect_ruby(cmd)
7
- if cmd =~ /^mint /
7
+ if cmd =~ /^mint/
8
8
  "ruby -I ../../lib -S ../../bin/#{cmd}"
9
9
  else
10
10
  super(cmd)
@@ -8,3 +8,4 @@ require 'mint/version'
8
8
  require 'mint/css'
9
9
  require 'mint/commandline'
10
10
  require 'mint/exceptions'
11
+ require 'mint/plugin'
@@ -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.mint(files, commandline_options)
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!(render_style)
197
+ Document.new(file, options).publish! :render_style => render_style
198
198
  render_style = false
199
199
  end
200
200
  end
@@ -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
- @renderer = Mint.renderer content
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!(render_style=true)
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
@@ -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
@@ -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
- template = Mint.path(true).map(&file_name).map(&find_files).flatten.
145
- select(&acceptable).select(&:exist?).first.to_s
146
- raise TemplateNotFoundException unless template
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
@@ -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