mint 0.7.3 → 0.8.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.
Files changed (71) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +23 -14
  3. data/LICENSE +22 -0
  4. data/README.md +82 -56
  5. data/bin/mint +47 -10
  6. data/bin/mint-epub +1 -4
  7. data/config/templates/base/style.css +187 -0
  8. data/config/templates/default/css/style.css +126 -79
  9. data/config/templates/default/layout.erb +10 -0
  10. data/config/templates/default/style.css +237 -0
  11. data/config/templates/garden/layout.erb +38 -0
  12. data/config/templates/garden/style.css +303 -0
  13. data/config/templates/newspaper/layout.erb +16 -0
  14. data/config/templates/nord/layout.erb +11 -0
  15. data/config/templates/nord/style.css +339 -0
  16. data/config/templates/nord-dark/layout.erb +11 -0
  17. data/config/templates/nord-dark/style.css +339 -0
  18. data/config/templates/protocol/layout.erb +9 -0
  19. data/config/templates/protocol/style.css +25 -0
  20. data/config/templates/zen/layout.erb +11 -0
  21. data/config/templates/zen/style.css +114 -0
  22. data/lib/mint/command_line.rb +253 -111
  23. data/lib/mint/css.rb +11 -4
  24. data/lib/mint/css_template.rb +37 -0
  25. data/lib/mint/document.rb +193 -43
  26. data/lib/mint/helpers.rb +50 -10
  27. data/lib/mint/layout.rb +2 -3
  28. data/lib/mint/markdown_template.rb +47 -0
  29. data/lib/mint/mint.rb +181 -114
  30. data/lib/mint/plugin.rb +3 -3
  31. data/lib/mint/plugins/epub.rb +1 -2
  32. data/lib/mint/resource.rb +19 -9
  33. data/lib/mint/style.rb +10 -14
  34. data/lib/mint/version.rb +1 -1
  35. data/lib/mint.rb +1 -0
  36. data/man/mint.1 +135 -0
  37. data/spec/cli/README.md +99 -0
  38. data/spec/cli/argument_parsing_spec.rb +207 -0
  39. data/spec/cli/bin_integration_spec.rb +348 -0
  40. data/spec/cli/configuration_management_spec.rb +363 -0
  41. data/spec/cli/full_workflow_integration_spec.rb +527 -0
  42. data/spec/cli/publish_workflow_spec.rb +368 -0
  43. data/spec/cli/template_management_spec.rb +300 -0
  44. data/spec/css_spec.rb +1 -1
  45. data/spec/document_spec.rb +105 -68
  46. data/spec/helpers_spec.rb +42 -42
  47. data/spec/mint_spec.rb +104 -80
  48. data/spec/plugin_spec.rb +86 -88
  49. data/spec/run_cli_tests.rb +95 -0
  50. data/spec/spec_helper.rb +8 -1
  51. data/spec/style_spec.rb +18 -16
  52. data/spec/support/cli_helpers.rb +169 -0
  53. data/spec/support/fixtures/content-2.md +16 -0
  54. data/spec/support/matchers.rb +1 -1
  55. metadata +145 -167
  56. data/config/syntax.yaml +0 -71
  57. data/config/templates/base/style.sass +0 -144
  58. data/config/templates/default/layout.haml +0 -8
  59. data/config/templates/default/style.sass +0 -36
  60. data/config/templates/protocol/layout.haml +0 -7
  61. data/config/templates/protocol/style.sass +0 -20
  62. data/config/templates/zen/css/style.css +0 -145
  63. data/config/templates/zen/layout.haml +0 -7
  64. data/config/templates/zen/style.sass +0 -24
  65. data/features/config.feature +0 -21
  66. data/features/plugins/epub.feature +0 -23
  67. data/features/publish.feature +0 -73
  68. data/features/support/env.rb +0 -15
  69. data/features/templates.feature +0 -79
  70. data/spec/command_line_spec.rb +0 -87
  71. data/spec/plugins/epub_spec.rb +0 -242
@@ -2,64 +2,84 @@ require "pathname"
2
2
  require "yaml"
3
3
  require "optparse"
4
4
  require "fileutils"
5
-
6
5
  require "active_support/core_ext/object/blank"
7
6
 
8
7
  module Mint
9
8
  module CommandLine
10
- # Commandline-related helper methods
11
-
12
- # Returns a map of all options that mint allows by default. Mint will
13
- # consume these arguments, with optional parameters, from
14
- # the commandline. (All other arguments are taken to be
15
- # filenames.)
16
- #
17
- # @return [Hash] a structured set of options that the commandline
18
- # executable accepts
19
- def self.options
20
- options_file = "../../../config/#{Mint.files[:syntax]}"
21
- YAML.load_file File.expand_path(options_file, __FILE__)
22
- end
23
-
24
- # Parses ARGV according to the specified or default commandline syntax
9
+ # Parses ARGV using OptionParser
25
10
  #
26
11
  # @param [Array] argv a list of arguments to parse
27
- # @param [Hash] opts default parsing options (to specify syntax file)
28
12
  # @return [Hash] an object that contains parsed options, remaining arguments,
29
13
  # and a help message
30
- def self.parse(argv, opts={})
31
- opts = { syntax: options }.merge(opts)
14
+ def self.parse(argv)
32
15
  parsed_options = {}
33
16
 
34
17
  parser = OptionParser.new do |cli|
35
18
  cli.banner = "Usage: mint [command] files [options]"
36
19
 
37
- Helpers.symbolize_keys(opts[:syntax]).each do |k,v|
38
- has_param = v[:parameter]
39
-
40
- v[:short] = "-#{v[:short]}"
41
- v[:long] = "--#{v[:long]}"
42
-
43
- if has_param
44
- v[:long] << " PARAM"
45
- cli.on v[:short], v[:long], v[:description] do |p|
46
- parsed_options[k.to_sym] = p
47
- end
48
- else
49
- cli.on v[:short], v[:long], v[:description] do
50
- parsed_options[k.to_sym] = true
51
- end
52
- end
20
+ cli.on "-t", "--template TEMPLATE", "Specify the template (layout + style)" do |t|
21
+ parsed_options[:layout_or_style_or_template] = [:template, t]
22
+ end
23
+
24
+ cli.on "-l", "--layout LAYOUT", "Specify only the layout" do |l|
25
+ parsed_options[:layout_or_style_or_template] = [:layout, l]
26
+ end
27
+
28
+ cli.on "-s", "--style STYLE", "Specify only the style" do |s|
29
+ parsed_options[:layout_or_style_or_template] = [:style, s]
30
+ end
31
+
32
+ cli.on "-w", "--root ROOT", "Specify a root outside the current directory" do |r|
33
+ parsed_options[:root] = r
34
+ end
35
+
36
+ cli.on "-o", "--output-file FORMAT", "Specify the output file format with substitutions: \#{basename}, \#{original_extension}, \#{new_extension}" do |o|
37
+ parsed_options[:output_file] = o
38
+ end
39
+
40
+ cli.on "-d", "--destination DESTINATION", "Specify a destination directory, relative to the root" do |d|
41
+ parsed_options[:destination] = d
42
+ end
43
+
44
+ cli.on "--inline-style", "Inline CSS into the HTML document (default)" do
45
+ parsed_options[:style_mode] = :inline
46
+ end
47
+
48
+ cli.on "--style-destination DESTINATION", "Create stylesheet at specified directory or file path and link it" do |destination|
49
+ parsed_options[:style_mode] = :external
50
+ parsed_options[:style_destination] = destination
51
+ end
52
+
53
+ cli.on "-g", "--global", "Specify config changes on a global level" do
54
+ parsed_options[:scope] = :global
55
+ end
56
+
57
+ cli.on "-u", "--user", "Specify config changes on a user-wide level" do
58
+ parsed_options[:scope] = :user
59
+ end
60
+
61
+ cli.on "-l", "--local", "Specify config changes on a project-specific level" do
62
+ parsed_options[:scope] = :local
63
+ end
64
+
65
+ cli.on "-r", "--recursive", "Recursively find all Markdown files in subdirectories" do
66
+ parsed_options[:recursive] = true
53
67
  end
54
68
  end
55
69
 
56
- transient_argv = argv.dup
70
+ transient_argv = argv.dup
57
71
  parser.parse! transient_argv
58
- { argv: transient_argv, options: parsed_options, help: parser.help }
72
+
73
+ if parsed_options[:style_mode] == :inline && parsed_options[:style_destination]
74
+ raise ArgumentError, "--inline-style and --style-destination cannot be used together"
75
+ end
76
+
77
+ default_options = Mint.default_options.merge(destination: Dir.getwd)
78
+ { argv: transient_argv, options: default_options.merge(parsed_options), help: parser.help }
59
79
  end
60
80
 
61
81
  # Mint built-in commands
62
-
82
+
63
83
  # Prints a help banner
64
84
  #
65
85
  # @param [String, #to_s] message a message to output
@@ -71,137 +91,237 @@ module Mint
71
91
  # Install the named file as a template
72
92
  #
73
93
  # @param [File] file the file to install to the appropriate Mint directory
74
- # @param [Hash] commandline_options a structured set of options, including
75
- # a scope label that the method will use to choose the appropriate
76
- # installation directory
94
+ # @param [String] name the template name to install as
95
+ # @param [Symbol] scope the scope at which to install
77
96
  # @return [void]
78
- def self.install(file, commandline_options={})
79
- opts = { scope: :local }.merge(commandline_options)
80
- scope = [:global, :user].
81
- select {|e| commandline_options[e] }.
82
- first || :local
83
-
97
+ def self.install(file, name, scope = :local)
98
+ if file.nil?
99
+ raise "[error] No file specified for installation"
100
+ end
101
+
84
102
  filename, ext = file.split "."
85
103
 
86
- name = commandline_options[:template] || filename
104
+ template_name = name || filename
87
105
  type = Mint.css_formats.include?(ext) ? :style : :layout
88
- destination = Mint.template_path(name, type, :scope => opts[:scope], :ext => ext)
89
- FileUtils.mkdir_p File.expand_path("#{destination}/..")
90
-
91
- puts "reading file"
92
- puts File.read file
106
+ destination = Mint.template_path(template_name, scope) + "#{type}.#{ext}"
107
+ FileUtils.mkdir_p File.dirname(destination)
93
108
 
94
109
  if File.exist? file
95
110
  FileUtils.cp file, destination
96
111
  else
97
- raise "[error] no such file"
112
+ raise "[error] No such file: #{file}"
98
113
  end
99
114
  end
100
115
 
101
116
  # Uninstall the named template
102
117
  #
103
118
  # @param [String] name the name of the template to be uninstalled
104
- # @param [Hash] commandline_options a structured set of options, including
105
- # a scope label that the method will use to choose the appropriate
106
- # installation directory
119
+ # @param [Symbol] scope the scope from which to uninstall
107
120
  # @return [void]
108
- def self.uninstall(name, commandline_options={})
109
- opts = { scope: :local }.merge(commandline_options)
110
- FileUtils.rm_r Mint.template_path(name, :all, :scope => opts[:scope])
121
+ def self.uninstall(name, scope = :local)
122
+ FileUtils.rm_r Mint.template_path(name, scope)
111
123
  end
112
124
 
113
125
  # List the installed templates
114
126
  #
127
+ # @param [String] filter optional filter pattern
128
+ # @param [Symbol] scope the scope to list templates from
115
129
  # @return [void]
116
- def self.templates(filter=nil, commandline_options={})
117
- scopes = Mint::SCOPE_NAMES.select do |s|
118
- commandline_options[s]
119
- end.presence || Mint::SCOPE_NAMES
120
-
121
- Mint.templates(:scopes => scopes).
122
- grep(Regexp.new(filter || "")).
130
+ def self.templates(filter = "", scope = :local)
131
+ filter = filter.to_s # Convert nil to empty string
132
+ Mint.templates(scope).
133
+ grep(Regexp.new(filter)).
123
134
  sort.
124
135
  each do |template|
125
- print File.basename template
126
- print " [#{template}]" if commandline_options[:verbose]
127
- puts
136
+ puts "#{File.basename template} [#{template}]"
137
+ end
138
+ end
139
+
140
+ # Processes the output file format string with substitutions
141
+ #
142
+ # @param [String] format_string the format string with #{} substitutions
143
+ # @param [String] input_file the original input file path
144
+ # @return [String] the processed output file name
145
+ def self.process_output_format(format_string, input_file)
146
+ basename = File.basename(input_file, ".*")
147
+ original_extension = File.extname(input_file)[1..-1] || ""
148
+
149
+ # TODO: Remove hardcoded new_extension
150
+ new_extension = "html"
151
+
152
+ format_string.
153
+ gsub('#{basename}', basename).
154
+ gsub('#{original_extension}', original_extension).
155
+ gsub('#{new_extension}', new_extension)
156
+ end
157
+
158
+ # Creates a new template directory and file at the specified scope
159
+ #
160
+ # @param [String] name the name of the template to create
161
+ # @param [Symbol] type the type of template (:layout or :style)
162
+ # @param [Symbol] scope the scope at which to create the template
163
+ # @return [String] the path to the created template file
164
+ def self.create_template(name, type, scope)
165
+ content, ext =
166
+ case type
167
+ when :layout
168
+ [default_layout_content, "erb"]
169
+ when :style
170
+ [default_style_content, "css"]
171
+ else
172
+ abort "Invalid template type: #{type}"
128
173
  end
174
+
175
+ template_dir = Mint.template_path(name, scope)
176
+ file_path = "#{template_dir}/#{type}.#{ext}"
177
+ FileUtils.mkdir_p template_dir
178
+ File.write(file_path, content)
179
+ file_path
180
+ end
181
+
182
+ # @return [String] default content for layout templates
183
+ def self.default_layout_content
184
+ <<~LAYOUT_TEMPLATE
185
+ <!DOCTYPE html>
186
+ <html>
187
+ <head>
188
+ <meta charset="utf-8">
189
+ <title>Document</title>
190
+ <% if style %>
191
+ <link rel="stylesheet" href="<%= style %>">
192
+ <% end %>
193
+ </head>
194
+ <body>
195
+ <%= content %>
196
+ </body>
197
+ </html>
198
+ LAYOUT_TEMPLATE
199
+ end
200
+
201
+ # @return [String] default content for style templates
202
+ def self.default_style_content
203
+ <<~STYLE_TEMPLATE
204
+ body {
205
+ font-family: -apple-system, 'Segoe UI', Roboto, sans-serif;
206
+ line-height: 1.25;
207
+ max-width: 960px;
208
+ margin: 0 auto;
209
+ padding: 2rem;
210
+ color: #333;
211
+ }
212
+
213
+ h1, h2, h3, h4, h5, h6 {
214
+ color: #2c3e50;
215
+ }
216
+
217
+ a {
218
+ color: #3498db;
219
+ text-decoration: none;
220
+ }
221
+
222
+ a:hover {
223
+ text-decoration: underline;
224
+ }
225
+
226
+ code {
227
+ background-color: #f8f9fa;
228
+ padding: 0.2em 0.4em;
229
+ border-radius: 3px;
230
+ font-family: 'Monaco', 'Ubuntu Mono', monospace;
231
+ }
232
+ STYLE_TEMPLATE
129
233
  end
130
234
 
131
- # Retrieve named template file (probably a built-in or installed
235
+ # Retrieve named template file (probably a built-in or installed
132
236
  # template) and shell out that file to the user's favorite editor.
133
237
  #
134
- # @param [String] name the name of a layout or style to edit
135
- # @param [Hash] commandline_options a structured set of options, including
136
- # a layout or style flag that the method will use to choose the appropriate
137
- # file to edit
238
+ # @param [String] name the name of a template to edit
239
+ # @param [Symbol] type either :layout or :style
240
+ # @param [Symbol] scope the scope at which to look for/create the template
138
241
  # @return [void]
139
- def self.edit(name, commandline_options={})
140
- layout = commandline_options[:layout]
141
- style = commandline_options[:style]
142
-
143
- # Allow for convenient editing (edit "default" works just as well
144
- # as edit :style => "default")
145
- if style
146
- name, layout_or_style = style, :style
147
- elsif layout
148
- name, layout_or_style = layout, :layout
149
- else
150
- layout_or_style = :style
151
- end
242
+ def self.edit(name, type, scope)
243
+ abort "[error] No template specified" if name.nil? || name.empty?
152
244
 
153
- abort "[error] no template specified" if name.nil? || name.empty?
245
+ begin
246
+ file = case type
247
+ when :layout
248
+ Mint.lookup_layout(name)
249
+ when :style
250
+ Mint.lookup_style(name)
251
+ else
252
+ abort "[error] Invalid template type: #{type}. Use :layout or :style"
253
+ end
254
+ rescue Mint::TemplateNotFoundException
255
+ print "Template '#{name}' does not exist. Create it? [y/N]: "
256
+ response = STDIN.gets.chomp.downcase
257
+
258
+ if response == 'y' || response == 'yes'
259
+ file = create_template(name, type, scope)
260
+ puts "Created template: #{file}"
261
+ else
262
+ abort "Template creation cancelled."
263
+ end
264
+ end
154
265
 
155
- file = Mint.lookup_template name, layout_or_style
156
-
157
266
  editor = ENV["EDITOR"] || "vi"
158
267
  system "#{editor} #{file}"
159
268
  end
160
269
 
161
- # Updates configuration options persistently in the appropriate scope,
270
+ # Updates configuration options persistently in the appropriate scope,
162
271
  # which defaults to local.
163
272
  #
164
- # @param [Hash] opts a structured set of options to set on Mint at the specified
273
+ # @param [Hash] opts a structured set of options to set on Mint at the specified
165
274
  # scope
166
275
  # @param [Symbol] scope the scope at which to apply the set of options
167
276
  # @return [void]
168
277
  def self.configure(opts, scope=:local)
169
- config_directory = Mint.path_for_scope(scope, true)
278
+ config_directory = Mint.path_for_scope(scope)
170
279
  FileUtils.mkdir_p config_directory
171
- Helpers.update_yaml! "#{config_directory}/#{Mint.files[:defaults]}", opts
280
+ Helpers.update_yaml! "#{config_directory}/#{Mint::CONFIG_FILE}", opts
172
281
  end
173
282
 
174
- # Tries to set a config option (at the specified scope) per
283
+ # Tries to set a config option (at the specified scope) per
175
284
  # the user's command.
176
285
  #
177
286
  # @param key the key to set
178
287
  # @param value the value to set key to
179
- # @param [Hash, #[]] commandline_options a structured set of options, including
180
- # a scope label that the method will use to choose the appropriate
181
- # scope
288
+ # @param scope the scope at which to set the configuration
182
289
  # @return [void]
183
- def self.set(key, value, commandline_options={})
184
- commandline_options[:local] = true
185
- scope = [:global, :user, :local].
186
- select {|e| commandline_options[e] }.
187
- first
188
-
290
+ def self.set(key, value, scope = :local)
189
291
  configure({ key => value }, scope)
190
292
  end
191
293
 
192
- # Displays the sum of all active configurations, where local
294
+ # Displays the sum of all active configurations, where local
193
295
  # configurations override global ones.
194
296
  #
195
297
  # @return [void]
196
298
  def self.config
197
299
  puts YAML.dump(Mint.configuration)
198
300
  end
199
-
301
+
302
+ # Recursively discovers Markdown files in the given directories
303
+ #
304
+ # @param [Array] directories the directories to search
305
+ # @return [Array] an array of markdown file paths
306
+ def self.discover_files_recursively(directories)
307
+ markdown_files = []
308
+ directories.each do |dir|
309
+ if File.file?(dir)
310
+ markdown_files << dir if dir =~ /\.(#{Mint::MARKDOWN_EXTENSIONS.join('|')})$/i
311
+ elsif File.directory?(dir)
312
+ Dir.glob("#{dir}/**/*.{#{Mint::MARKDOWN_EXTENSIONS.join(',')}}", File::FNM_CASEFOLD).each do |file|
313
+ markdown_files << file
314
+ end
315
+ end
316
+ end
317
+ markdown_files.sort
318
+ end
319
+
200
320
  # Renders and writes to file all resources described by a document.
201
321
  # Specifically: it publishes a document, using the document's accessors
202
- # to determine file placement and naming, and then renders its style.
203
- # This method will overwrite any existing content in a document's destination
204
- # files. The `render_style` option provides an easy way to stop Mint from
322
+ # to determine file placement and naming, and then renders its style.
323
+ # This method will overwrite any existing content in a document's destination
324
+ # files. The `render_style` option provides an easy way to stop Mint from
205
325
  # rendering a style, even if the document's style is not nil.
206
326
  #
207
327
  # @param [Array, #each] files a group of filenames
@@ -209,9 +329,31 @@ module Mint
209
329
  # that will guide Mint.publish!
210
330
  # @return [void]
211
331
  def self.publish!(files, commandline_options={})
332
+ # TODO: Establish commandline defaults in one place
333
+ # TODO: Use `commandline_options` everywhere instead of `options` and `doc_options`
212
334
  options = { root: Dir.getwd }.merge(Mint.configuration_with commandline_options)
335
+
336
+ if commandline_options[:recursive]
337
+ files = discover_files_recursively(files.empty? ? ["."] : files)
338
+ end
339
+
213
340
  files.each_with_index do |file, idx|
214
- Document.new(file, options).publish!(:render_style => (idx == 0))
341
+ # Pass all files list when processing multiple files (for navigation in templates like garden)
342
+ all_files = files.size > 1 ? files : nil
343
+
344
+ Document.new(file,
345
+ root: options[:root],
346
+ destination: options[:destination],
347
+ context: options[:context],
348
+ name: options[:name],
349
+ style_mode: options[:style_mode],
350
+ style_destination: options[:style_destination],
351
+ layout: options[:layout],
352
+ style: options[:style],
353
+ template: options[:template],
354
+ layout_or_style_or_template: options[:layout_or_style_or_template],
355
+ all_files: all_files
356
+ ).publish!(:render_style => (idx == 0))
215
357
  end
216
358
  end
217
359
  end
data/lib/mint/css.rb CHANGED
@@ -1,4 +1,4 @@
1
- require "sass"
1
+ require "sass-embedded"
2
2
 
3
3
  module Mint
4
4
  module CSS
@@ -21,7 +21,7 @@ module Mint
21
21
  # @page { size: landscape };
22
22
  # }
23
23
  def self.mappings
24
- {
24
+ {
25
25
  font: "font-family",
26
26
  font_size: "font-size",
27
27
  font_color: "color",
@@ -63,8 +63,15 @@ module Mint
63
63
  def self.parse(style)
64
64
  css = style.map {|k,v| stylify(k, v) }.join("\n ")
65
65
  container_scope = "##{container}\n #{css.strip}\n"
66
- engine = Sass::Engine.new(container_scope)
67
- engine.silence_sass_warnings { engine.render }
66
+
67
+ # Suppress warnings by capturing $stderr
68
+ original_stderr = $stderr
69
+ $stderr = StringIO.new
70
+
71
+ result = Sass.compile_string(container_scope, syntax: :indented)
72
+ result.css
73
+ ensure
74
+ $stderr = original_stderr
68
75
  end
69
76
  end
70
77
  end
@@ -0,0 +1,37 @@
1
+ require 'tilt/template'
2
+
3
+ module Mint
4
+ class CSSTemplate < Tilt::Template
5
+ self.default_mime_type = 'text/css'
6
+
7
+ def prepare
8
+ @data = data
9
+ end
10
+
11
+ def evaluate(scope, locals, &block)
12
+ process_imports(@data, File.dirname(file))
13
+ end
14
+
15
+ def process_imports(css_content, base_dir)
16
+ css_content.gsub(/@import\s+["']([^"']+)["'];?/) do |match|
17
+ import_path = $1
18
+
19
+ # If we find a relative path, resolve it
20
+ if import_path.start_with?('../', './')
21
+ full_path = File.expand_path(import_path, base_dir)
22
+ else
23
+ full_path = File.join(base_dir, import_path)
24
+ end
25
+
26
+ full_path += '.css' unless full_path.end_with?('.css')
27
+
28
+ if File.exist?(full_path)
29
+ imported_content = File.read(full_path)
30
+ process_imports(imported_content, File.dirname(full_path))
31
+ else
32
+ match
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end