mint 0.8.1 → 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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -26
  3. data/README.md +117 -37
  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 +45 -25
  23. data/lib/mint/document.rb +250 -365
  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 +85 -44
  37. data/spec/cli/README.md +2 -2
  38. data/spec/cli/argument_parsing_spec.rb +89 -147
  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 +59 -1
  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 +75 -89
  60. data/bin/mint-epub +0 -20
  61. data/config/templates/garden/layout.erb +0 -38
  62. data/config/templates/garden/style.css +0 -303
  63. data/config/templates/nord/layout.erb +0 -11
  64. data/config/templates/nord-dark/layout.erb +0 -11
  65. data/config/templates/zen/layout.erb +0 -11
  66. data/config/templates/zen/style.css +0 -114
  67. data/lib/mint/command_line.rb +0 -360
  68. data/lib/mint/css_template.rb +0 -37
  69. data/lib/mint/markdown_template.rb +0 -47
  70. data/lib/mint/mint.rb +0 -313
  71. data/lib/mint/plugin.rb +0 -136
  72. data/lib/mint/plugins/epub.rb +0 -293
  73. data/lib/mint/resource.rb +0 -101
  74. data/plugins/templates/epub/layouts/container.haml +0 -5
  75. data/plugins/templates/epub/layouts/content.haml +0 -35
  76. data/plugins/templates/epub/layouts/layout.haml +0 -6
  77. data/plugins/templates/epub/layouts/title.haml +0 -11
  78. data/plugins/templates/epub/layouts/toc.haml +0 -26
  79. data/spec/cli/configuration_management_spec.rb +0 -363
  80. data/spec/cli/template_management_spec.rb +0 -300
  81. data/spec/helpers_spec.rb +0 -249
  82. data/spec/plugin_spec.rb +0 -449
  83. data/spec/resource_spec.rb +0 -135
data/lib/mint/document.rb CHANGED
@@ -1,403 +1,288 @@
1
- require "mint/resource"
2
- require "mint/layout"
3
- require "mint/style"
4
- require "mint/css_parser"
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"
5
10
 
6
11
  module Mint
7
- class Document < Resource
12
+ class Document
8
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
9
56
 
10
- # Creates a new Mint Document object. Can be block initialized.
11
- def initialize(source,
12
- root: nil,
13
- destination: nil,
14
- context: nil,
15
- name: nil,
16
- style_mode: :inline,
17
- style_destination: nil,
18
- layout: nil,
19
- style: nil,
20
- template: nil,
21
- layout_or_style_or_template: [:template, 'default'],
22
- all_files: nil,
23
- &block)
24
-
25
- 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)
26
73
 
27
- @all_files = all_files if all_files && all_files.any?
28
-
29
- style_mode = :external if style_destination && style_mode == :inline
30
-
31
- # Loads source and destination, which will be used for
32
- # all source_* and destination_* virtual attributes.
33
- super(source, root: root, destination: destination, context: context, name: name)
34
- self.type = :document
35
-
36
- # Each of these should invoke explicitly defined method
37
- self.content = source
38
- self.style_mode = style_mode
39
- self.style_destination = style_destination
40
-
41
- if layout_or_style_or_template
42
- type, name = layout_or_style_or_template
43
- case type
44
- when :template
45
- self.template = name
46
- when :layout
47
- self.layout = name
48
- when :style
49
- self.style = name
50
- 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
51
106
  end
52
107
 
53
- # Individual layout/style options can override the above
54
- self.layout = layout if layout
55
- self.style = style if style
56
-
57
- # The template option will override layout and style choices
58
- self.template = template if template
59
-
60
- # Yield self to block after all other parameters are loaded,
61
- # so we only have to tweak. (We don't have to give up our
62
- # defaults or re-test blocks beyond them being tweaked.)
63
- yield self if block
64
- end
65
-
66
- # Renders content in the context of layout and returns as a String.
67
- def render(args={})
68
- intermediate_content = layout.render self, args
69
- Mint.after_render(intermediate_content, {})
70
- end
71
-
72
- # Writes all rendered content where a) possible, b) required,
73
- # and c) specified. Outputs to specified file.
74
- def publish!(opts={})
75
- options = { :render_style => true }.merge(opts)
76
- super
77
-
78
- if @style_mode == :external && options[:render_style]
79
- FileUtils.mkdir_p style_destination_directory
80
- File.open(self.style_destination_file, "w+") do |f|
81
- f << self.style.render
82
- 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
83
115
  end
84
-
85
- Mint.after_publish(self, opts)
86
- end
87
-
88
- # Implicit readers are paired with explicit accessors. This
89
- # allows for processing variables before storing them.
90
- attr_reader :metadata, :layout, :style
91
-
92
- # Returns HTML content marked as safe for template rendering
93
- def content
94
- @content.html_safe
95
116
  end
96
-
97
- # Passes content through a renderer before assigning it to be
98
- # the Document's content
117
+
118
+ # Transforms Markdown links from .md extensions
99
119
  #
100
- # @param [File, #read, #basename] content the content to be rendered
101
- # from a templating language into HTML
102
- # @return [void]
103
- def content=(content)
104
- tempfile = Helpers.generate_temp_file! content
105
- original_content = File.read content
106
-
107
- @metadata, text = Document.parse_metadata_from original_content
108
- text_with_links = Helpers.transform_markdown_links text
109
- intermediate_content = Mint.before_render text_with_links, {}
110
-
111
- File.open(tempfile, "w") do |file|
112
- 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
113
146
  end
114
-
115
- @renderer = Mint.renderer tempfile
116
- @content = @renderer.render
117
147
  end
118
148
 
119
- # Sets layout to an existing Layout object or looks it up by name
149
+ # Generates navigation tree data for use in layout templates
120
150
  #
121
- # @param [String, Layout, #render] layout a Layout object or name
122
- # of a layout to be looked up
123
- # @return [void]
124
- def layout=(layout)
125
- @layout =
126
- if layout.respond_to? :render
127
- layout
128
- else
129
- layout_file = Mint.lookup_layout layout
130
- Layout.new(layout_file, root: self.root, destination: self.destination, context: self.context)
131
- 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)
132
164
  end
133
165
 
134
- # Sets layout to an existing Style object or looks it up by name
135
- #
136
- # @param [String, Style, #render] layout a Layout object or name
137
- # of a layout to be looked up
138
- # @return [void]
139
- def style=(style)
140
- @style =
141
- if style.respond_to? :render
142
- 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
143
189
  else
144
- style_file = Mint.lookup_style style
145
- Style.new(style_file, root: self.root, destination: self.destination, context: self.context)
190
+ Pathname.new("./#{relative_str}")
146
191
  end
147
- end
148
-
149
- # Overrides layout and style settings with named template.
150
- #
151
- # @param [String] template the name of the template to set as
152
- # layout and string
153
- def template=(template)
154
- if template
155
- self.layout = template
156
- self.style = template
192
+ rescue
193
+ target_pathname
157
194
  end
158
195
  end
159
-
160
- # Explanation of style_destination:
161
- #
162
- # I'm going to maintain a document's official style_destination
163
- # outside of its style object. If a document has no
164
- # style_destination defined when it needs one, the document will
165
- # use the original style's source directory.
166
- #
167
- # This decision eliminates edge cases, including the case where
168
- # we want to generate, but not move, a document's style. It also
169
- # lets us keep style information separate from document-specific
170
- # information. (Without this separation, funky things happen when
171
- # you assign a new style template to an existing document -- if
172
- # you had specified a custom style_destination before changing
173
- # the template, that custom destination would be overridden.)
174
- #
175
- # The style_destination attribute is lazy. It's exposed via
176
- # virtual attributes like #style_destination_file.
177
- attr_reader :style_destination, :style_mode
178
-
179
- # @param [Symbol] style_mode how styles should be incorporated (:inline or :external)
180
- # @return [void]
181
- def style_mode=(style_mode)
182
- @style_mode = style_mode
183
- end
184
-
185
- # @param [String] style_destination the subdirectory into
186
- # which styles will be rendered or copied
187
- # @return [void]
188
- def style_destination=(style_destination)
189
- @style_destination = style_destination
196
+
197
+ def metadata_chunk(text)
198
+ text.split(METADATA_DELIM).first
190
199
  end
191
-
192
- # Exposes style_destination as a Pathname object.
193
- #
194
- # @return [Pathname]
195
- def style_destination_file_path
196
- if style_destination
197
- path = Pathname.new style_destination
198
- dir = path.absolute? ?
199
- path : destination_directory_path + path
200
- 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
+ {}
201
207
  else
202
- style.destination_file_path
208
+ raw_metadata
203
209
  end
210
+ rescue Psych::SyntaxError, Exception
211
+ {}
204
212
  end
205
213
 
206
- # Exposes style_destination as a String.
207
- #
208
- # @return [String]
209
- def style_destination_file
210
- style_destination_file_path.to_s
211
- end
212
-
213
- # Exposes style_destination directory as a Pathname object.
214
- #
215
- # @return [Pathname]
216
- def style_destination_directory_path
217
- style_destination_file_path.dirname
218
- end
219
-
220
- # Exposes style_destination directory as a String.
221
- #
222
- # @return [String]
223
- def style_destination_directory
224
- style_destination_directory_path.to_s
225
- end
226
-
227
- # Convenience methods for views
228
-
229
- # Returns a relative path from the document to its stylesheet. Can
230
- # be called directly from inside a layout template.
231
- def stylesheet
232
- tmp_style_dir = Mint.path_for_scope(:user) + "tmp"
233
- tmp_style_file = tmp_style_dir + File.basename(style.name)
234
- Helpers.normalize_path(tmp_style_file.to_s,
235
- self.destination_directory).to_s
236
- end
237
-
238
- # Returns the rendered CSS content for inline inclusion
239
- def inline_stylesheet
240
- self.style.render
241
- end
242
-
243
- # Returns either inline CSS or stylesheet link based on style mode
244
- # Use this helper in layouts instead of stylesheet or inline_stylesheet directly
245
- def stylesheet_tag
246
- 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>"
247
225
  when :external
248
- "<link rel=\"stylesheet\" href=\"#{stylesheet}\">".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}\">"
249
235
  when :original
250
- original_stylesheet_tags.html_safe
251
- else # :inline (default)
252
- "<style>#{self.style.render}</style>".html_safe
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>"
253
245
  end
254
246
  end
255
-
256
- # Generates stylesheet link tags for original mode
257
- # Links directly to original CSS files without processing
258
- def original_stylesheet_tags
259
- return "" unless self.style&.template_path
260
-
261
- main_css_path = self.style.template_path
262
- html_output_path = self.output_file
263
-
264
- # Only process .css files
265
- return "" unless File.extname(main_css_path) == '.css'
266
-
267
- css_file_paths = CssParser.resolve_css_files(main_css_path, html_output_path)
268
- CssParser.generate_link_tags(css_file_paths)
269
- end
270
-
271
- # Parses styles defined in YAML metadata in content, including it
272
- # in output CSS style
247
+
248
+ # Returns the destination path for the external stylesheet
273
249
  #
274
- # TODO: Implement injection of these styles
275
- def inline_styles
276
- 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"
277
253
  end
278
-
279
- # Returns information about all files for navigation in some templates (e.g., garden)
280
- # Available when processing multiple files
281
- def files
282
- return [] unless @all_files
283
-
284
- # Get the base directories
285
- source_base_dir = Pathname.new(root_directory_path).expand_path
286
-
287
- # Calculate where the current file will actually be placed
288
- current_source_path = Pathname.new(source_file_path).expand_path
289
- current_relative_to_source = current_source_path.relative_path_from(source_base_dir)
290
- current_html_filename = current_relative_to_source.to_s.gsub(/\.(#{Mint::MARKDOWN_EXTENSIONS.join('|')})$/i, '.html')
291
-
292
- dest_base = Pathname.new(root_directory_path).expand_path
293
- if destination && !destination.empty?
294
- 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)
295
262
  end
296
-
297
- current_full_path = dest_base + current_html_filename
298
- current_destination_dir = current_full_path.dirname
299
-
300
- @all_files.map do |file|
301
- title = extract_title_from_file(file)
302
-
303
- # Calculate where this target file will be placed
304
- file_path = Pathname.new(file).expand_path
305
- relative_to_source = file_path.relative_path_from(source_base_dir)
306
- html_filename = relative_to_source.to_s.gsub(/\.(#{Mint::MARKDOWN_EXTENSIONS.join('|')})$/i, '.html')
307
-
308
- target_full_path = dest_base + html_filename
309
-
310
- # Calculate the relative path from the current file's destination directory to the target file
311
- relative_link = target_full_path.relative_path_from(current_destination_dir)
312
-
313
- {
314
- source_path: relative_to_source.to_s,
315
- html_path: relative_link.to_s,
316
- title: title,
317
- depth: relative_to_source.to_s.count('/')
318
- }
319
- end.sort_by {|f| f[:source_path] }
320
263
  end
321
-
322
- # Functions
323
-
324
- private
325
-
326
- # Extracts the title from a markdown file, trying H1 first, then filename
327
- def extract_title_from_file(file)
328
- content = File.read(file)
329
-
330
- if content =~ /^#\s+(.+)$/
331
- return $1.strip
332
- end
333
-
334
- File.basename(file, '.*').tr('_-', ' ').split.map(&:capitalize).join(' ')
335
- rescue
336
- 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)
337
271
  end
338
-
339
- # Preserves folder structure when --recursive is used
340
- #
341
- # @param [String] source the source file path
342
- # @param [Hash] options the options hash to modify
343
- def preserve_folder_structure!(source, root, destination)
344
- source_path = Pathname.new(source).expand_path
345
- root_path = Pathname.new(root || Dir.getwd).expand_path
346
-
347
- relative_path = source_path.relative_path_from(root_path)
348
-
349
- relative_dir = relative_path.dirname
350
- filename = relative_path.basename
351
-
352
- # Set destination to preserve directory structure
353
- if relative_dir.to_s != "."
354
- # Combine base destination with relative directory structure
355
- base_destination = destination || ""
356
- if base_destination.empty?
357
- destination = relative_dir.to_s
358
- else
359
- destination = File.join(base_destination, relative_dir.to_s)
360
- 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
361
283
  end
362
284
 
363
- destination
364
- end
365
-
366
- class << self
367
- def metadata_chunk(text)
368
- text.split(METADATA_DELIM).first
369
- end
370
-
371
- def metadata_from(text)
372
- raw_metadata = YAML.load metadata_chunk(text)
373
-
374
- case raw_metadata
375
- when String
376
- {}
377
- when false
378
- {}
379
- when nil
380
- {}
381
- else
382
- raw_metadata
383
- end
384
- rescue Psych::SyntaxError
385
- {}
386
- rescue Exception
387
- {}
388
- end
389
-
390
- def parse_metadata_from(text)
391
- metadata = metadata_from text
392
- new_text =
393
- if !metadata.empty?
394
- text.sub metadata_chunk(text) + METADATA_DELIM, ""
395
- else
396
- text
397
- end
398
-
399
- [metadata, new_text]
400
- end
285
+ [metadata, new_text]
401
286
  end
402
287
  end
403
- end
288
+ end