mint 0.8.0 → 0.10.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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -26
  3. data/README.md +138 -95
  4. data/bin/mint +2 -81
  5. data/config/templates/base/navigation.css +136 -0
  6. data/config/templates/base/print.css +152 -0
  7. data/config/templates/{reset.css → base/reset.css} +1 -1
  8. data/config/templates/base/style.css +117 -137
  9. data/config/templates/base/utilities.css +136 -0
  10. data/config/templates/base/variables.css +124 -0
  11. data/config/templates/basic/style.css +151 -0
  12. data/config/templates/default/layout.erb +33 -3
  13. data/config/templates/default/style.css +95 -164
  14. data/config/templates/magazine/style.css +383 -0
  15. data/config/templates/nord/style.css +105 -220
  16. data/config/templates/nord-dark/style.css +82 -263
  17. data/lib/mint/commandline/parse.rb +144 -0
  18. data/lib/mint/commandline/publish.rb +46 -0
  19. data/lib/mint/commandline/run.rb +30 -0
  20. data/lib/mint/config.rb +162 -0
  21. data/lib/mint/{css.rb → css_dsl.rb} +9 -9
  22. data/lib/mint/css_parser.rb +96 -0
  23. data/lib/mint/document.rb +251 -348
  24. data/lib/mint/document_tree.rb +163 -0
  25. data/lib/mint/exceptions.rb +2 -3
  26. data/lib/mint/helpers.rb +23 -180
  27. data/lib/mint/layout.rb +26 -9
  28. data/lib/mint/renderers/css_renderer.rb +32 -0
  29. data/lib/mint/renderers/erb_renderer.rb +11 -0
  30. data/lib/mint/renderers/markdown_renderer.rb +45 -0
  31. data/lib/mint/style.rb +21 -31
  32. data/lib/mint/template.rb +30 -0
  33. data/lib/mint/version.rb +1 -1
  34. data/lib/mint/workspace.rb +171 -0
  35. data/lib/mint.rb +44 -12
  36. data/man/mint.1 +88 -47
  37. data/spec/cli/README.md +13 -13
  38. data/spec/cli/argument_parsing_spec.rb +103 -131
  39. data/spec/cli/bin_integration_spec.rb +23 -243
  40. data/spec/cli/full_workflow_integration_spec.rb +99 -442
  41. data/spec/cli/original_style_integration_spec.rb +58 -0
  42. data/spec/cli/publish_workflow_spec.rb +72 -70
  43. data/spec/commandline_path_integration_spec.rb +230 -0
  44. data/spec/config_file_integration_spec.rb +362 -0
  45. data/spec/{css_spec.rb → css_dsl_spec.rb} +7 -3
  46. data/spec/css_parser_spec.rb +207 -0
  47. data/spec/document_spec.rb +37 -242
  48. data/spec/flattened_path_spec.rb +150 -0
  49. data/spec/layout_spec.rb +42 -3
  50. data/spec/mint_spec.rb +22 -217
  51. data/spec/path_handling_spec.rb +237 -0
  52. data/spec/run_cli_tests.rb +1 -1
  53. data/spec/spec_helper.rb +3 -10
  54. data/spec/style_spec.rb +31 -56
  55. data/spec/support/cli_helpers.rb +7 -10
  56. data/spec/support/matchers.rb +1 -1
  57. data/spec/template_spec.rb +31 -0
  58. data/spec/workspace_spec.rb +177 -0
  59. metadata +78 -93
  60. data/bin/mint-epub +0 -20
  61. data/config/templates/default/css/style.css +0 -205
  62. data/config/templates/garden/layout.erb +0 -38
  63. data/config/templates/garden/style.css +0 -303
  64. data/config/templates/newspaper/layout.erb +0 -16
  65. data/config/templates/nord/layout.erb +0 -11
  66. data/config/templates/nord-dark/layout.erb +0 -11
  67. data/config/templates/protocol/layout.erb +0 -9
  68. data/config/templates/protocol/style.css +0 -25
  69. data/config/templates/zen/layout.erb +0 -11
  70. data/config/templates/zen/style.css +0 -114
  71. data/lib/mint/command_line.rb +0 -360
  72. data/lib/mint/css_template.rb +0 -37
  73. data/lib/mint/markdown_template.rb +0 -47
  74. data/lib/mint/mint.rb +0 -313
  75. data/lib/mint/plugin.rb +0 -136
  76. data/lib/mint/plugins/epub.rb +0 -293
  77. data/lib/mint/resource.rb +0 -101
  78. data/plugins/templates/epub/layouts/container.haml +0 -5
  79. data/plugins/templates/epub/layouts/content.haml +0 -35
  80. data/plugins/templates/epub/layouts/layout.haml +0 -6
  81. data/plugins/templates/epub/layouts/title.haml +0 -11
  82. data/plugins/templates/epub/layouts/toc.haml +0 -26
  83. data/spec/cli/configuration_management_spec.rb +0 -363
  84. data/spec/cli/template_management_spec.rb +0 -300
  85. data/spec/helpers_spec.rb +0 -249
  86. data/spec/plugin_spec.rb +0 -449
  87. data/spec/resource_spec.rb +0 -135
data/lib/mint/document.rb CHANGED
@@ -1,385 +1,288 @@
1
- require "mint/resource"
2
- require "mint/layout"
3
- require "mint/style"
1
+ require "pathname"
2
+ require "yaml"
3
+ require "active_support/core_ext/string/output_safety"
4
+
5
+ require_relative "./renderers/markdown_renderer"
6
+ require_relative "./renderers/erb_renderer"
7
+ require_relative "./renderers/css_renderer"
8
+ require_relative "./helpers"
9
+ require_relative "./document_tree"
4
10
 
5
11
  module Mint
6
- class Document < Resource
12
+ class Document
7
13
  METADATA_DELIM = "\n\n"
14
+
15
+ attr_reader :title, :destination_path
16
+
17
+ # @param [Pathname] working_directory path by which relative links should be resolved
18
+ # @param [Pathname] source_path path to markdown file (relative to working_directory)
19
+ # @param [Pathname] destination_path path to output file (relative to destination_directory_path)
20
+ # @param [Pathname] destination_directory_path path to destination directory
21
+ # @param [Pathname] layout_path path to layout file (relative to working_directory)
22
+ # @param [Pathname] style_path path to style file (relative to working_directory)
23
+ # @param [Pathname] style_destination_path path to style destination file
24
+ # @param [Symbol] style_mode style mode (:inline, :external, :original)
25
+ # @param [Boolean] insert_title_heading whether to inject title as H1 heading
26
+ # @param [Boolean] show_navigation whether to show navigation
27
+ # @param [Integer] navigation_depth navigation depth (optional)
28
+ # @param [Array<Hash>] navigation_data array of navigation items with :path and :title
29
+ # @param [String] navigation_title title for navigation panel (optional)
30
+ # @param [Proc] transform_links proc to transform link basenames; yields the basename of the link
31
+ # @param [Boolean] render_style whether to render style
32
+ def initialize(working_directory:,
33
+ source_path:,
34
+ destination_path:,
35
+ destination_directory_path:,
36
+ layout_path:,
37
+ style_path:,
38
+ style_destination_path:,
39
+ style_mode:,
40
+ insert_title_heading:,
41
+ transform_links: Proc.new,
42
+ render_style: true)
43
+ @working_directory = working_directory
44
+ @source_path = source_path
45
+ @destination_path = destination_path
46
+ @destination_directory_path = destination_directory_path
47
+ @layout_path = layout_path
48
+ @style_path = style_path
49
+ @style_destination_path = style_destination_path
50
+ @style_mode = style_mode
51
+ @insert_title_heading = insert_title_heading
52
+ @transform_links = transform_links
53
+ @render_style = render_style
54
+ @title = guess_title
55
+ end
8
56
 
9
- # Creates a new Mint Document object. Can be block initialized.
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)
57
+ # Publishes the markdown document to HTML
58
+ #
59
+ # Reads the markdown source file, transforms links, renders to HTML,
60
+ # applies the layout template, and writes to the destination directory.
61
+ # The destination path is resolved at publish time by combining
62
+ # destination_directory_path + destination_path.
63
+ #
64
+ # @return [Pathname] the destination path relative to working_directory
65
+ def publish!(show_navigation: nil, navigation: nil, navigation_depth: nil, navigation_title: nil)
66
+ if @style_mode == :external && @render_style
67
+ create_external_stylesheet
68
+ end
69
+
70
+ # Read and parse Markdown into metadata + content
71
+ source_content = File.read(@source_path)
72
+ metadata, body = parse_metadata_from(source_content)
25
73
 
26
- @all_files = all_files if all_files && all_files.any?
27
-
28
- style_mode = :external if style_destination && style_mode == :inline
29
-
30
- # Loads source and destination, which will be used for
31
- # all source_* and destination_* virtual attributes.
32
- super(source, root: root, destination: destination, context: context, name: name)
33
- self.type = :document
34
-
35
- # Each of these should invoke explicitly defined method
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
74
+ # Transform Markdown links, taking output format into account
75
+ body_with_rewritten_links = transform_markdown_links(body, &@transform_links)
76
+
77
+ # Render Markdown to HTML
78
+ rendered_content = Renderers::Markdown.render(body_with_rewritten_links)
79
+
80
+ # Create layout variables, for use in the layout template
81
+ layout_variables = {
82
+ working_directory: @working_directory,
83
+ current_path: @source_path.to_s,
84
+ metadata: metadata,
85
+ title: @title,
86
+ insert_title_heading: @insert_title_heading,
87
+ content: rendered_content.html_safe,
88
+ stylesheet_tag: render_stylesheet_tag(@style_path, @style_mode),
89
+ files: navigation ? generate_navigation_tree(navigation: navigation) : [],
90
+ show_navigation: show_navigation,
91
+ navigation_title: navigation_title
92
+ }
93
+
94
+ # Render the layout
95
+ layout_content = File.read(@layout_path)
96
+ rendered_content = Renderers::Erb.render(layout_content, layout_variables)
97
+
98
+ # Write the rendered content to the destination path
99
+ full_destination_path = @destination_directory_path.absolute? ?
100
+ @destination_directory_path + @destination_path :
101
+ @working_directory + @destination_directory_path + @destination_path
102
+
103
+ full_destination_path.dirname.mkpath
104
+ full_destination_path.open("w+") do |f|
105
+ f << rendered_content
50
106
  end
51
107
 
52
- # Individual layout/style options can override the above
53
- self.layout = layout if layout
54
- self.style = style if style
55
-
56
- # The template option will override layout and style choices
57
- self.template = template if template
58
-
59
- # Yield self to block after all other parameters are loaded,
60
- # so we only have to tweak. (We don't have to give up our
61
- # defaults or re-test blocks beyond them being tweaked.)
62
- yield self if block
63
- end
64
-
65
- # Renders content in the context of layout and returns as a String.
66
- def render(args={})
67
- intermediate_content = layout.render self, args
68
- Mint.after_render(intermediate_content, {})
69
- end
70
-
71
- # Writes all rendered content where a) possible, b) required,
72
- # and c) specified. Outputs to specified file.
73
- def publish!(opts={})
74
- options = { :render_style => true }.merge(opts)
75
- super
76
-
77
- if @style_mode == :external && options[:render_style]
78
- FileUtils.mkdir_p style_destination_directory
79
- File.open(self.style_destination_file, "w+") do |f|
80
- f << self.style.render
81
- end
108
+ # Return the destination path used, for use in verbose output
109
+ begin
110
+ full_destination_path.relative_path_from(@working_directory)
111
+ rescue ArgumentError
112
+ # If, for some reason, the paths don't share a common prefix,
113
+ # return the full path to avoid an error
114
+ full_destination_path
82
115
  end
83
-
84
- Mint.after_publish(self, opts)
85
- end
86
-
87
- # Implicit readers are paired with explicit accessors. This
88
- # allows for processing variables before storing them.
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
116
  end
95
-
96
- # Passes content through a renderer before assigning it to be
97
- # the Document's content
117
+
118
+ # Transforms Markdown links from .md extensions
98
119
  #
99
- # @param [File, #read, #basename] content the content to be rendered
100
- # from a templating language into HTML
101
- # @return [void]
102
- def content=(content)
103
- tempfile = Helpers.generate_temp_file! content
104
- original_content = File.read content
105
-
106
- @metadata, text = Document.parse_metadata_from original_content
107
- text_with_links = Helpers.transform_markdown_links text
108
- intermediate_content = Mint.before_render text_with_links, {}
109
-
110
- File.open(tempfile, "w") do |file|
111
- file << intermediate_content
120
+ # @param [String] text the Markdown text containing links to Markdown documents
121
+ # @yield [String] block used to transform the basename of each Markdown link found
122
+ # @return [String] the text with transformed links
123
+ def transform_markdown_links(text, &block)
124
+ text.gsub(/(\[([^\]]*)\]\(([^)]*\.md)\))/) do |match|
125
+ link_text = $2
126
+ link_url = $3
127
+
128
+ # Only transform relative links (not absolute URLs)
129
+ if link_url !~ /^https?:\/\//
130
+ # Preserve directory structure in links
131
+ dirname = File.dirname(link_url)
132
+ basename = File.basename(link_url, ".*")
133
+
134
+ new_filename = yield basename
135
+
136
+ new_url = if dirname == "."
137
+ new_filename
138
+ else
139
+ File.join(dirname, new_filename)
140
+ end
141
+
142
+ "[#{link_text}](#{new_url})"
143
+ else
144
+ match
145
+ end
112
146
  end
113
-
114
- @renderer = Mint.renderer tempfile
115
- @content = @renderer.render
116
147
  end
117
148
 
118
- # Sets layout to an existing Layout object or looks it up by name
149
+ # Generates navigation tree data for use in layout templates
119
150
  #
120
- # @param [String, Layout, #render] layout a Layout object or name
121
- # of a layout to be looked up
122
- # @return [void]
123
- def layout=(layout)
124
- @layout =
125
- if layout.respond_to? :render
126
- layout
127
- else
128
- layout_file = Mint.lookup_layout layout
129
- Layout.new(layout_file, root: self.root, destination: self.destination, context: self.context)
130
- end
151
+ # @return [Array<Hash>] array of navigation items with keys:
152
+ # - :title (String) - display title for the item
153
+ # - :html_path (String) - path to the HTML file (for files) or nil (for directories)
154
+ # - :source_path (String) - path to the source file relative to working directory
155
+ # - :depth (Integer) - nesting depth in the tree
156
+ # - :is_directory (Boolean) - true if this is a directory entry (optional key)
157
+ def generate_navigation_tree(navigation:)
158
+ # Build DocumentTree with documents (path + title pairs)
159
+ document_tree = DocumentTree.new(navigation)
160
+ reoriented_tree = document_tree.reorient(@destination_path)
161
+
162
+ # Serialize to flat array for ERB template consumption
163
+ reoriented_tree.serialize(max_depth: @navigation_depth)
131
164
  end
132
165
 
133
- # Sets layout to an existing Style object or looks it up by name
134
- #
135
- # @param [String, Style, #render] layout a Layout object or name
136
- # of a layout to be looked up
137
- # @return [void]
138
- def style=(style)
139
- @style =
140
- if style.respond_to? :render
141
- style
166
+ private
167
+
168
+ # Calculates the relative path of a target pathname to a reference pathname; this is an
169
+ # extension of Pathname#relative_path_from that handles the case where the reference pathname
170
+ # is a file and the target pathname is a directory. In this case, the relative path should be
171
+ # the target pathname.
172
+ #
173
+ # @param [Pathname] target_pathname the pathname to calculate the relative path for
174
+ # @param [Pathname] reference_pathname the pathname to use as the reference
175
+ # @return [Pathname] the relative path of the target pathname to the reference pathname
176
+ def calculate_relative_path(target_pathname, reference_pathname)
177
+ # Determine if reference is a file based on extension (since the file may not exist on disk yet)
178
+ reference_is_file = !reference_pathname.extname.empty?
179
+ reference_dir = reference_is_file ? reference_pathname.dirname : reference_pathname
180
+
181
+ begin
182
+ relative_path = target_pathname.relative_path_from(reference_dir)
183
+
184
+ # Prepend './' to relative paths that don't start with '../'
185
+ # to make it clear they are relative paths within the same directory tree
186
+ relative_str = relative_path.to_s
187
+ if relative_str.start_with?('../')
188
+ relative_path
142
189
  else
143
- style_file = Mint.lookup_style style
144
- Style.new(style_file, root: self.root, destination: self.destination, context: self.context)
190
+ Pathname.new("./#{relative_str}")
145
191
  end
146
- end
147
-
148
- # Overrides layout and style settings with named template.
149
- #
150
- # @param [String] template the name of the template to set as
151
- # layout and string
152
- def template=(template)
153
- if template
154
- self.layout = template
155
- self.style = template
192
+ rescue
193
+ target_pathname
156
194
  end
157
195
  end
158
-
159
- # Explanation of style_destination:
160
- #
161
- # I'm going to maintain a document's official style_destination
162
- # outside of its style object. If a document has no
163
- # style_destination defined when it needs one, the document will
164
- # use the original style's source directory.
165
- #
166
- # This decision eliminates edge cases, including the case where
167
- # we want to generate, but not move, a document's style. It also
168
- # lets us keep style information separate from document-specific
169
- # information. (Without this separation, funky things happen when
170
- # you assign a new style template to an existing document -- if
171
- # you had specified a custom style_destination before changing
172
- # the template, that custom destination would be overridden.)
173
- #
174
- # The style_destination attribute is lazy. It's exposed via
175
- # virtual attributes like #style_destination_file.
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
196
+
197
+ def metadata_chunk(text)
198
+ text.split(METADATA_DELIM).first
182
199
  end
183
-
184
- # @param [String] style_destination the subdirectory into
185
- # which styles will be rendered or copied
186
- # @return [void]
187
- def style_destination=(style_destination)
188
- @style_destination = style_destination
189
- end
190
-
191
- # Exposes style_destination as a Pathname object.
192
- #
193
- # @return [Pathname]
194
- def style_destination_file_path
195
- if style_destination
196
- path = Pathname.new style_destination
197
- dir = path.absolute? ?
198
- path : destination_directory_path + path
199
- dir + style.name
200
+
201
+ def metadata_from(text)
202
+ raw_metadata = YAML.load(metadata_chunk(text))
203
+
204
+ case raw_metadata
205
+ when String, false, nil
206
+ {}
200
207
  else
201
- style.destination_file_path
208
+ raw_metadata
202
209
  end
210
+ rescue Psych::SyntaxError, Exception
211
+ {}
203
212
  end
204
213
 
205
- # Exposes style_destination as a String.
206
- #
207
- # @return [String]
208
- def style_destination_file
209
- style_destination_file_path.to_s
210
- end
211
-
212
- # Exposes style_destination directory as a Pathname object.
213
- #
214
- # @return [Pathname]
215
- def style_destination_directory_path
216
- style_destination_file_path.dirname
217
- end
218
-
219
- # Exposes style_destination directory as a String.
220
- #
221
- # @return [String]
222
- def style_destination_directory
223
- style_destination_directory_path.to_s
224
- end
225
-
226
- # Convenience methods for views
227
-
228
- # Returns a relative path from the document to its stylesheet. Can
229
- # be called directly from inside a layout template.
230
- def stylesheet
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,
234
- self.destination_directory).to_s
235
- end
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
214
+ # Renders a stylesheet tag (either <style> or <link>) based on the style mode
215
+ #
216
+ # @param [Pathname] style_path the path to the style file
217
+ # @param [Symbol] style_mode the style mode (:inline, :external, :original)
218
+ # @return [String] the stylesheet tag
219
+ def render_stylesheet_tag(style_path, style_mode)
220
+ return "" unless style_path&.exist?
221
+
222
+ case style_mode
223
+ when :inline
224
+ "<style>#{Renderers::Css.render_file(style_path)}</style>"
246
225
  when :external
247
- "<link rel=\"stylesheet\" href=\"#{stylesheet}\">".html_safe
248
- else # :inline (default)
249
- "<style>#{self.style.render}</style>".html_safe
226
+ full_destination_path = @destination_directory_path.absolute? ?
227
+ @destination_directory_path + @destination_path :
228
+ @working_directory + @destination_directory_path + @destination_path
229
+ absolute_stylesheet_path = external_stylesheet_destination_path
230
+ unless absolute_stylesheet_path.absolute?
231
+ absolute_stylesheet_path = @working_directory + absolute_stylesheet_path
232
+ end
233
+ relative_path_to_stylesheet = absolute_stylesheet_path.relative_path_from(full_destination_path.dirname)
234
+ "<link rel=\"stylesheet\" href=\"#{relative_path_to_stylesheet}\">"
235
+ when :original
236
+ full_destination_path = @destination_directory_path.absolute? ?
237
+ @destination_directory_path + @destination_path :
238
+ @working_directory + @destination_directory_path + @destination_path
239
+ absolute_style_path = style_path.absolute? ? style_path : @working_directory + style_path
240
+ relative_path_to_original = absolute_style_path.relative_path_from(full_destination_path.dirname)
241
+ "<link rel=\"stylesheet\" href=\"#{relative_path_to_original}\">"
242
+ else
243
+ style_content = Renderers::Css.render_file(style_path)
244
+ "<style>#{style_content}</style>"
250
245
  end
251
246
  end
252
-
253
- # Parses styles defined in YAML metadata in content, including it
254
- # in output CSS style
247
+
248
+ # Returns the destination path for the external stylesheet
255
249
  #
256
- # TODO: Implement injection of these styles
257
- def inline_styles
258
- CSS.parse(metadata)
250
+ # @return [Pathname] the destination path for the external stylesheet
251
+ def external_stylesheet_destination_path
252
+ @style_destination_path + "style.css"
259
253
  end
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
254
+
255
+ # Creates an external stylesheet if the style mode is :external
256
+ def create_external_stylesheet
257
+ return unless @style_mode == :external
258
+ style_output_file = external_stylesheet_destination_path
259
+ style_output_file.dirname.mkpath
260
+ style_output_file.open("w+") do |f|
261
+ f << Renderers::Css.render_file(@style_path)
277
262
  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
263
  end
303
-
304
- # Functions
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(' ')
264
+
265
+ # Extracts title from Markdown file name
266
+ #
267
+ # @param [Pathname] file_path path to Markdown file
268
+ # @return [String] extracted title
269
+ def guess_title
270
+ Helpers.extract_title_from_file(@source_path)
319
271
  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
272
+
273
+ # Parse YAML metadata from markdown content
274
+ #
275
+ # @param [String] text markdown content
276
+ # @return [Array] array of [metadata_hash, content_without_metadata]
277
+ def parse_metadata_from(text)
278
+ metadata = metadata_from(text)
279
+ new_text = if !metadata.empty?
280
+ text.sub(metadata_chunk(text) + METADATA_DELIM, "")
281
+ else
282
+ text
343
283
  end
344
284
 
345
- destination
346
- end
347
-
348
- class << self
349
- def metadata_chunk(text)
350
- text.split(METADATA_DELIM).first
351
- end
352
-
353
- def metadata_from(text)
354
- raw_metadata = YAML.load metadata_chunk(text)
355
-
356
- case raw_metadata
357
- when String
358
- {}
359
- when false
360
- {}
361
- when nil
362
- {}
363
- else
364
- raw_metadata
365
- end
366
- rescue Psych::SyntaxError
367
- {}
368
- rescue Exception
369
- {}
370
- end
371
-
372
- def parse_metadata_from(text)
373
- metadata = metadata_from text
374
- new_text =
375
- if !metadata.empty?
376
- text.sub metadata_chunk(text) + METADATA_DELIM, ""
377
- else
378
- text
379
- end
380
-
381
- [metadata, new_text]
382
- end
285
+ [metadata, new_text]
383
286
  end
384
287
  end
385
- end
288
+ end