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