mint 0.7.4 → 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 +185 -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 +102 -69
  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 +116 -223
  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
data/lib/mint/document.rb CHANGED
@@ -7,51 +7,74 @@ module Mint
7
7
  METADATA_DELIM = "\n\n"
8
8
 
9
9
  # Creates a new Mint Document object. Can be block initialized.
10
- # Accepts source and options. Block initialization occurs after
11
- # all defaults are set, so not all options must be specified.
12
- def initialize(source, opts={})
13
- options = Mint.default_options.merge opts
10
+ def initialize(source,
11
+ root: nil,
12
+ destination: nil,
13
+ context: nil,
14
+ name: nil,
15
+ style_mode: :inline,
16
+ style_destination: nil,
17
+ layout: nil,
18
+ style: nil,
19
+ template: nil,
20
+ layout_or_style_or_template: [:template, 'default'],
21
+ all_files: nil,
22
+ &block)
23
+
24
+ destination = preserve_folder_structure!(source, root, destination)
25
+
26
+ @all_files = all_files if all_files && all_files.any?
27
+
28
+ style_mode = :external if style_destination && style_mode == :inline
14
29
 
15
30
  # Loads source and destination, which will be used for
16
31
  # all source_* and destination_* virtual attributes.
17
- super(source, options)
18
- self.type = :document
32
+ super(source, root: root, destination: destination, context: context, name: name)
33
+ self.type = :document
19
34
 
20
35
  # Each of these should invoke explicitly defined method
21
- self.content = source
22
- self.layout = options[:layout]
23
- self.style = options[:style]
24
- self.style_destination = options[:style_destination]
36
+ self.content = source
37
+ self.style_mode = style_mode
38
+ self.style_destination = style_destination
39
+
40
+ if layout_or_style_or_template
41
+ type, name = layout_or_style_or_template
42
+ case type
43
+ when :template
44
+ self.template = name
45
+ when :layout
46
+ self.layout = name
47
+ when :style
48
+ self.style = name
49
+ end
50
+ end
51
+
52
+ # Individual layout/style options can override the above
53
+ self.layout = layout if layout
54
+ self.style = style if style
25
55
 
26
56
  # The template option will override layout and style choices
27
- self.template = options[:template]
57
+ self.template = template if template
28
58
 
29
59
  # Yield self to block after all other parameters are loaded,
30
60
  # so we only have to tweak. (We don't have to give up our
31
61
  # defaults or re-test blocks beyond them being tweaked.)
32
- yield self if block_given?
62
+ yield self if block
33
63
  end
34
64
 
35
65
  # Renders content in the context of layout and returns as a String.
36
66
  def render(args={})
37
67
  intermediate_content = layout.render self, args
38
- Mint.after_render(intermediate_content)
68
+ Mint.after_render(intermediate_content, {})
39
69
  end
40
70
 
41
71
  # Writes all rendered content where a) possible, b) required,
42
72
  # and c) specified. Outputs to specified file.
43
- def publish!(opts={})
73
+ def publish!(opts={})
44
74
  options = { :render_style => true }.merge(opts)
45
- FileUtils.mkdir_p self.destination_directory
46
- File.open(self.destination_file, "w+") do |f|
47
- f << self.render
48
- end
75
+ super
49
76
 
50
- # Only renders style if a) it's specified by the options path and
51
- # b) it actually needs rendering (i.e., it's in template form and
52
- # not raw, browser-parseable CSS) or it if it doesn't need
53
- # rendering but there is an explicit style_destination.
54
- if options[:render_style]
77
+ if @style_mode == :external && options[:render_style]
55
78
  FileUtils.mkdir_p style_destination_directory
56
79
  File.open(self.style_destination_file, "w+") do |f|
57
80
  f << self.style.render
@@ -63,7 +86,12 @@ module Mint
63
86
 
64
87
  # Implicit readers are paired with explicit accessors. This
65
88
  # allows for processing variables before storing them.
66
- attr_reader :content, :metadata, :layout, :style
89
+ attr_reader :metadata, :layout, :style
90
+
91
+ # Returns HTML content marked as safe for template rendering
92
+ def content
93
+ @content.html_safe
94
+ end
67
95
 
68
96
  # Passes content through a renderer before assigning it to be
69
97
  # the Document's content
@@ -76,7 +104,8 @@ module Mint
76
104
  original_content = File.read content
77
105
 
78
106
  @metadata, text = Document.parse_metadata_from original_content
79
- intermediate_content = Mint.before_render text
107
+ text_with_links = Helpers.transform_markdown_links text
108
+ intermediate_content = Mint.before_render text_with_links, {}
80
109
 
81
110
  File.open(tempfile, "w") do |file|
82
111
  file << intermediate_content
@@ -91,33 +120,29 @@ module Mint
91
120
  # @param [String, Layout, #render] layout a Layout object or name
92
121
  # of a layout to be looked up
93
122
  # @return [void]
94
- def layout=(layout)
95
- @layout =
123
+ def layout=(layout)
124
+ @layout =
96
125
  if layout.respond_to? :render
97
126
  layout
98
127
  else
99
- layout_file = Mint.lookup_template layout, :layout
100
- Layout.new layout_file
128
+ layout_file = Mint.lookup_layout layout
129
+ Layout.new(layout_file, root: self.root, destination: self.destination, context: self.context)
101
130
  end
102
- rescue TemplateNotFoundException
103
- abort "Template '#{layout}' does not exist."
104
131
  end
105
-
132
+
106
133
  # Sets layout to an existing Style object or looks it up by name
107
134
  #
108
135
  # @param [String, Style, #render] layout a Layout object or name
109
136
  # of a layout to be looked up
110
137
  # @return [void]
111
138
  def style=(style)
112
- @style =
139
+ @style =
113
140
  if style.respond_to? :render
114
141
  style
115
142
  else
116
- style_file = Mint.lookup_template style, :style
117
- Style.new style_file
143
+ style_file = Mint.lookup_style style
144
+ Style.new(style_file, root: self.root, destination: self.destination, context: self.context)
118
145
  end
119
- rescue TemplateNotFoundException
120
- abort "Template '#{style}' does not exist."
121
146
  end
122
147
 
123
148
  # Overrides layout and style settings with named template.
@@ -148,8 +173,14 @@ module Mint
148
173
  #
149
174
  # The style_destination attribute is lazy. It's exposed via
150
175
  # virtual attributes like #style_destination_file.
151
- attr_reader :style_destination
152
-
176
+ attr_reader :style_destination, :style_mode
177
+
178
+ # @param [Symbol] style_mode how styles should be incorporated (:inline or :external)
179
+ # @return [void]
180
+ def style_mode=(style_mode)
181
+ @style_mode = style_mode
182
+ end
183
+
153
184
  # @param [String] style_destination the subdirectory into
154
185
  # which styles will be rendered or copied
155
186
  # @return [void]
@@ -163,7 +194,7 @@ module Mint
163
194
  def style_destination_file_path
164
195
  if style_destination
165
196
  path = Pathname.new style_destination
166
- dir = path.absolute? ?
197
+ dir = path.absolute? ?
167
198
  path : destination_directory_path + path
168
199
  dir + style.name
169
200
  else
@@ -197,16 +228,123 @@ module Mint
197
228
  # Returns a relative path from the document to its stylesheet. Can
198
229
  # be called directly from inside a layout template.
199
230
  def stylesheet
200
- Helpers.normalize_path(self.style_destination_file,
231
+ tmp_style_dir = Mint.path_for_scope(:user) + "tmp"
232
+ tmp_style_file = tmp_style_dir + File.basename(style.name)
233
+ Helpers.normalize_path(tmp_style_file.to_s,
201
234
  self.destination_directory).to_s
202
235
  end
203
236
 
237
+ # Returns the rendered CSS content for inline inclusion
238
+ def inline_stylesheet
239
+ self.style.render
240
+ end
241
+
242
+ # Returns either inline CSS or stylesheet link based on style mode
243
+ # Use this helper in layouts instead of stylesheet or inline_stylesheet directly
244
+ def stylesheet_tag
245
+ case @style_mode
246
+ when :external
247
+ "<link rel=\"stylesheet\" href=\"#{stylesheet}\">".html_safe
248
+ else # :inline (default)
249
+ "<style>#{self.style.render}</style>".html_safe
250
+ end
251
+ end
252
+
253
+ # Parses styles defined in YAML metadata in content, including it
254
+ # in output CSS style
255
+ #
256
+ # TODO: Implement injection of these styles
204
257
  def inline_styles
205
258
  CSS.parse(metadata)
206
259
  end
207
260
 
261
+ # Returns information about all files for navigation in some templates (e.g., garden)
262
+ # Available when processing multiple files
263
+ def files
264
+ return [] unless @all_files
265
+
266
+ # Get the base directories
267
+ source_base_dir = Pathname.new(root_directory_path).expand_path
268
+
269
+ # Calculate where the current file will actually be placed
270
+ current_source_path = Pathname.new(source_file_path).expand_path
271
+ current_relative_to_source = current_source_path.relative_path_from(source_base_dir)
272
+ current_html_filename = current_relative_to_source.to_s.gsub(/\.(#{Mint::MARKDOWN_EXTENSIONS.join('|')})$/i, '.html')
273
+
274
+ dest_base = Pathname.new(root_directory_path).expand_path
275
+ if destination && !destination.empty?
276
+ dest_base = dest_base + destination
277
+ end
278
+
279
+ current_full_path = dest_base + current_html_filename
280
+ current_destination_dir = current_full_path.dirname
281
+
282
+ @all_files.map do |file|
283
+ title = extract_title_from_file(file)
284
+
285
+ # Calculate where this target file will be placed
286
+ file_path = Pathname.new(file).expand_path
287
+ relative_to_source = file_path.relative_path_from(source_base_dir)
288
+ html_filename = relative_to_source.to_s.gsub(/\.(#{Mint::MARKDOWN_EXTENSIONS.join('|')})$/i, '.html')
289
+
290
+ target_full_path = dest_base + html_filename
291
+
292
+ # Calculate the relative path from the current file's destination directory to the target file
293
+ relative_link = target_full_path.relative_path_from(current_destination_dir)
294
+
295
+ {
296
+ source_path: relative_to_source.to_s,
297
+ html_path: relative_link.to_s,
298
+ title: title,
299
+ depth: relative_to_source.to_s.count('/')
300
+ }
301
+ end.sort_by {|f| f[:source_path] }
302
+ end
303
+
208
304
  # Functions
209
305
 
306
+ private
307
+
308
+ # Extracts the title from a markdown file, trying H1 first, then filename
309
+ def extract_title_from_file(file)
310
+ content = File.read(file)
311
+
312
+ if content =~ /^#\s+(.+)$/
313
+ return $1.strip
314
+ end
315
+
316
+ File.basename(file, '.*').tr('_-', ' ').split.map(&:capitalize).join(' ')
317
+ rescue
318
+ File.basename(file, '.*').tr('_-', ' ').split.map(&:capitalize).join(' ')
319
+ end
320
+
321
+ # Preserves folder structure when --recursive is used
322
+ #
323
+ # @param [String] source the source file path
324
+ # @param [Hash] options the options hash to modify
325
+ def preserve_folder_structure!(source, root, destination)
326
+ source_path = Pathname.new(source).expand_path
327
+ root_path = Pathname.new(root || Dir.getwd).expand_path
328
+
329
+ relative_path = source_path.relative_path_from(root_path)
330
+
331
+ relative_dir = relative_path.dirname
332
+ filename = relative_path.basename
333
+
334
+ # Set destination to preserve directory structure
335
+ if relative_dir.to_s != "."
336
+ # Combine base destination with relative directory structure
337
+ base_destination = destination || ""
338
+ if base_destination.empty?
339
+ destination = relative_dir.to_s
340
+ else
341
+ destination = File.join(base_destination, relative_dir.to_s)
342
+ end
343
+ end
344
+
345
+ destination
346
+ end
347
+
210
348
  class << self
211
349
  def metadata_chunk(text)
212
350
  text.split(METADATA_DELIM).first
@@ -214,16 +352,20 @@ module Mint
214
352
 
215
353
  def metadata_from(text)
216
354
  raw_metadata = YAML.load metadata_chunk(text)
217
-
355
+
218
356
  case raw_metadata
219
357
  when String
220
358
  {}
221
359
  when false
222
360
  {}
361
+ when nil
362
+ {}
223
363
  else
224
364
  raw_metadata
225
365
  end
226
- rescue
366
+ rescue Psych::SyntaxError
367
+ {}
368
+ rescue Exception
227
369
  {}
228
370
  end
229
371
 
data/lib/mint/helpers.rb CHANGED
@@ -4,7 +4,7 @@ require "yaml"
4
4
  require "active_support/core_ext/string/inflections"
5
5
 
6
6
  module Mint
7
- module Helpers
7
+ module Helpers
8
8
  def self.underscore(obj, opts={})
9
9
  namespaces = obj.to_s.split("::").map do |namespace|
10
10
  if opts[:ignore_prefix]
@@ -38,7 +38,7 @@ module Mint
38
38
  def self.symbolize(obj)
39
39
  slugize(obj).gsub(/-/, "_").to_sym
40
40
  end
41
-
41
+
42
42
  # Transforms a String or Pathname into a fully expanded Pathname.
43
43
  #
44
44
  # @param [String, Pathname] str_or_path a path to be expanded
@@ -59,8 +59,8 @@ module Mint
59
59
  def self.symbolize_keys(map, opts={})
60
60
  transform = lambda {|x| opts[:downcase] ? x.downcase : x }
61
61
 
62
- map.reduce(Hash.new) do |syms,(k,v)|
63
- syms[transform[k].to_sym] =
62
+ map.reduce(Hash.new) do |syms,(k,v)|
63
+ syms[transform[k].to_sym] =
64
64
  case v
65
65
  when Hash
66
66
  self.symbolize_keys(v, opts)
@@ -107,19 +107,19 @@ module Mint
107
107
  Hash[*list1.zip(list2).flatten]
108
108
  end
109
109
 
110
- # Returns the relative path to to_directory from from_directory.
111
- # If to_directory and from_directory have no parents in common besides
110
+ # Returns the relative path to to_directory from from_directory.
111
+ # If to_directory and from_directory have no parents in common besides
112
112
  # /, returns the absolute directory of to_directory. Assumes no symlinks.
113
113
  #
114
114
  # @param [String, Pathname] to_directory the target directory
115
115
  # @param [String, Pathname] from_directory the starting directory
116
- # @return [Pathname] the relative path to to_directory from
116
+ # @return [Pathname] the relative path to to_directory from
117
117
  # from_directory, or an absolute path if they have no parents in common
118
118
  # other than /
119
119
  def self.normalize_path(to_directory, from_directory)
120
120
  to_path, from_path = [to_directory, from_directory].map {|d| pathize d }
121
121
  to_root, from_root = [to_path, from_path].map {|p| p.each_filename.first }
122
- to_root == from_root ?
122
+ to_root == from_root ?
123
123
  to_path.relative_path_from(from_path) :
124
124
  to_path
125
125
  end
@@ -129,15 +129,33 @@ module Mint
129
129
  #
130
130
  # @param [Hash, #[]] new_opts a set of options to add to the Yaml file
131
131
  # @param [Pathname, #exist] file a file to read from and write to
132
- # @return [void]
132
+ # @return [void]
133
133
  def self.update_yaml!(file, opts={})
134
- curr_opts = File.exist?(file) ? YAML.load_file(file) : {}
134
+ curr_opts = if File.exist?(file)
135
+ begin
136
+ YAML.load_file(file) || {}
137
+ rescue Psych::SyntaxError, StandardError
138
+ # Handle corrupted YAML gracefully by treating it as empty
139
+ {}
140
+ end
141
+ else
142
+ {}
143
+ end
135
144
 
136
145
  File.open file, "w" do |f|
137
146
  YAML.dump(curr_opts.merge(opts), f)
138
147
  end
139
148
  end
140
149
 
150
+ def self.create_temp_file!(basename, extension=nil, &block)
151
+ tmp_args = basename && extension ? [basename, extension] : basename
152
+ tempfile = Tempfile.new(tmp_args)
153
+ block.call(tempfile)
154
+ tempfile.flush
155
+ tempfile.close
156
+ tempfile.path
157
+ end
158
+
141
159
  def self.generate_temp_file!(file)
142
160
  basename = File.basename file
143
161
  extension = File.extname file
@@ -149,5 +167,27 @@ module Mint
149
167
  tempfile.close
150
168
  tempfile.path
151
169
  end
170
+
171
+ # Transforms markdown links from .md extensions to .html for digital gardens
172
+ #
173
+ # @param [String] text the markdown text containing links
174
+ # @return [String] the text with transformed links
175
+ def self.transform_markdown_links(text)
176
+ # Transform relative markdown links like [text](path/file.md) to [text](path/file.html)
177
+ text.gsub(/(\[([^\]]*)\]\()([^)]*\.md)(\))/) do |match|
178
+ link_start = $1
179
+ link_text = $2
180
+ link_url = $3
181
+ link_end = $4
182
+
183
+ # Only transform relative links (not absolute URLs)
184
+ if link_url !~ /^https?:\/\//
185
+ new_url = link_url.gsub(/\.md$/, '.html')
186
+ "#{link_start}#{new_url}#{link_end}"
187
+ else
188
+ match
189
+ end
190
+ end
191
+ end
152
192
  end
153
193
  end
data/lib/mint/layout.rb CHANGED
@@ -6,9 +6,8 @@ module Mint
6
6
  # and optional configuration options.
7
7
  #
8
8
  # @param [String] source the absolute or relative file path
9
- # @param [Hash, #[]] opts layout options
10
- def initialize(source, opts=Mint.default_options)
11
- super(source, opts)
9
+ def initialize(source, root: nil, destination: nil, context: nil, name: nil, &block)
10
+ super(source, root: root, destination: destination, context: context, name: name, &block)
12
11
  self.type = :layout
13
12
  end
14
13
  end
@@ -0,0 +1,47 @@
1
+ require 'tilt/template'
2
+ require 'redcarpet'
3
+
4
+ module Mint
5
+ class MarkdownTemplate < Tilt::Template
6
+ self.default_mime_type = 'text/html'
7
+
8
+ def prepare
9
+ @options = options.dup
10
+
11
+ renderer_options = {
12
+ filter_html: false,
13
+ no_images: false,
14
+ no_links: false,
15
+ no_styles: false,
16
+ escape_html: false,
17
+ safe_links_only: false,
18
+ with_toc_data: false,
19
+ hard_wrap: false,
20
+ prettify: false
21
+ }.merge(@options)
22
+
23
+ markdown_options = {
24
+ tables: true,
25
+ autolink: true,
26
+ no_intra_emphasis: true,
27
+ fenced_code_blocks: true,
28
+ lax_html_blocks: false,
29
+ strikethrough: true,
30
+ superscript: false,
31
+ footnotes: false,
32
+ highlight: false,
33
+ quote: false,
34
+ disable_indented_code_blocks: false,
35
+ space_after_headers: false,
36
+ underline: false
37
+ }
38
+
39
+ @renderer = @options.delete(:renderer) || Redcarpet::Render::HTML.new(renderer_options)
40
+ @markdown = Redcarpet::Markdown.new(@renderer, markdown_options)
41
+ end
42
+
43
+ def evaluate(scope, locals, &block)
44
+ @markdown.render(data)
45
+ end
46
+ end
47
+ end