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
@@ -0,0 +1,162 @@
1
+ require "toml"
2
+
3
+ module Mint
4
+ class Config
5
+ attr_accessor :files
6
+ attr_accessor :stdin_mode
7
+ attr_accessor :help
8
+ attr_accessor :verbose
9
+ attr_accessor :layout_name
10
+ attr_accessor :style_name
11
+ attr_accessor :style_mode
12
+ attr_accessor :output_file_format
13
+ attr_accessor :working_directory # This can't be set by the user
14
+ attr_accessor :destination_directory
15
+ attr_accessor :style_destination_directory
16
+ attr_accessor :preserve_structure
17
+ attr_accessor :insert_title_heading
18
+ attr_accessor :autodrop
19
+ attr_accessor :navigation
20
+ attr_accessor :navigation_depth
21
+ attr_accessor :navigation_title
22
+
23
+ DEFAULT_STDIN_MODE = false
24
+ DEFAULT_VERBOSE = false
25
+ DEFAULT_LAYOUT_NAME = "default"
26
+ DEFAULT_STYLE_NAME = "default"
27
+ DEFAULT_STYLE_MODE = :inline
28
+ DEFAULT_OUTPUT_FILE_FORMAT = '%{name}.%{ext}'
29
+ DEFAULT_WORKING_DIRECTORY = lambda { Pathname.getwd.expand_path }
30
+ DEFAULT_DESTINATION_DIRECTORY = lambda { Pathname.getwd.expand_path }
31
+ DEFAULT_PRESERVE_STRUCTURE = true
32
+ DEFAULT_INSERT_TITLE_HEADING = false
33
+ DEFAULT_AUTODROP = true
34
+ DEFAULT_NAVIGATION = false
35
+ DEFAULT_NAVIGATION_DEPTH = 3
36
+ DEFAULT_NAVIGATION_TITLE = nil
37
+
38
+ def initialize(options = {})
39
+ @stdin_mode = options[:stdin_mode] if options.key?(:stdin_mode)
40
+ @help = options[:help]
41
+ @verbose = options[:verbose] if options.key?(:verbose)
42
+ @layout_name = options[:layout_name] || options[:template_name]
43
+ @style_name = options[:style_name] || options[:template_name]
44
+ @style_mode = options[:style_mode]
45
+ @output_file_format = options[:output_file_format]
46
+ @working_directory = options[:working_directory]&.expand_path
47
+ @destination_directory = options[:destination_directory]
48
+ @style_destination_directory = options[:style_destination_directory]
49
+ @preserve_structure = options[:preserve_structure] if options.key?(:preserve_structure)
50
+ @insert_title_heading = options[:insert_title_heading] if options.key?(:insert_title_heading)
51
+ @autodrop = options[:autodrop] if options.key?(:autodrop)
52
+ @navigation = options[:navigation] if options.key?(:navigation)
53
+ @navigation_depth = options[:navigation_depth] if options.key?(:navigation_depth)
54
+ @navigation_title = options[:navigation_title]
55
+ end
56
+
57
+ def to_h
58
+ {
59
+ stdin_mode: @stdin_mode,
60
+ help: @help,
61
+ verbose: @verbose,
62
+ layout_name: @layout_name,
63
+ style_name: @style_name,
64
+ style_mode: @style_mode,
65
+ output_file_format: @output_file_format,
66
+ working_directory: @working_directory,
67
+ destination_directory: @destination_directory,
68
+ style_destination_directory: @style_destination_directory,
69
+ preserve_structure: @preserve_structure,
70
+ insert_title_heading: @insert_title_heading,
71
+ autodrop: @autodrop,
72
+ navigation: @navigation,
73
+ navigation_depth: @navigation_depth,
74
+ navigation_title: @navigation_title
75
+ }
76
+ end
77
+
78
+ def merge(config = Config.new)
79
+ Config.new(to_h.merge(config.to_h.reject {|_, v| v.nil? }))
80
+ end
81
+
82
+ def self.ensure_config(config)
83
+ case config
84
+ when Config
85
+ config
86
+ when Hash
87
+ Config.new(config)
88
+ else
89
+ raise ArgumentError, "config must be a Config object or Hash"
90
+ end
91
+ end
92
+
93
+ def self.load_file(file)
94
+ toml_config = TOML.load_file(file)
95
+ mapped_config = map_toml_keys_to_config(toml_config) if toml_config.is_a?(Hash)
96
+ Config.new(mapped_config || {})
97
+ end
98
+
99
+ def self.defaults
100
+ dest_dir = DEFAULT_DESTINATION_DIRECTORY.call
101
+ Config.new({
102
+ stdin_mode: DEFAULT_STDIN_MODE,
103
+ verbose: DEFAULT_VERBOSE,
104
+ layout_name: DEFAULT_LAYOUT_NAME,
105
+ style_name: DEFAULT_STYLE_NAME,
106
+ style_mode: DEFAULT_STYLE_MODE,
107
+ output_file_format: DEFAULT_OUTPUT_FILE_FORMAT,
108
+ working_directory: DEFAULT_WORKING_DIRECTORY.call,
109
+ destination_directory: dest_dir,
110
+ style_destination_directory: Pathname.new('.'),
111
+ preserve_structure: DEFAULT_PRESERVE_STRUCTURE,
112
+ insert_title_heading: DEFAULT_INSERT_TITLE_HEADING,
113
+ autodrop: DEFAULT_AUTODROP,
114
+ navigation: DEFAULT_NAVIGATION,
115
+ navigation_depth: DEFAULT_NAVIGATION_DEPTH,
116
+ navigation_title: DEFAULT_NAVIGATION_TITLE
117
+ })
118
+ end
119
+
120
+ def self.with_defaults(overrides = {})
121
+ defaults.merge(new(overrides))
122
+ end
123
+
124
+ private
125
+
126
+ def self.map_toml_keys_to_config(toml_config)
127
+ mapping = {
128
+ 'verbose' => :verbose,
129
+ 'template' => :template_name,
130
+ 'layout' => :layout_name,
131
+ 'style' => :style_name,
132
+ 'output-file' => :output_file_format,
133
+ 'destination' => :destination_directory,
134
+ 'style-mode' => :style_mode,
135
+ 'style-destination' => :style_destination_directory,
136
+ 'preserve-structure' => :preserve_structure,
137
+ 'insert-title-heading' => :insert_title_heading,
138
+ 'autodrop' => :autodrop,
139
+ 'navigation' => :navigation,
140
+ 'navigation-depth' => :navigation_depth,
141
+ 'navigation-title' => :navigation_title
142
+ }
143
+
144
+ mapped = {}
145
+ toml_config.each do |key, value|
146
+ mapped_key = mapping[key] || key.to_sym
147
+
148
+ # Handle special conversions
149
+ case mapped_key
150
+ when :destination_directory
151
+ mapped[mapped_key] = Pathname.new(value) if value
152
+ when :style_mode
153
+ mapped[mapped_key] = value.to_sym if value
154
+ else
155
+ mapped[mapped_key] = value
156
+ end
157
+ end
158
+
159
+ mapped
160
+ end
161
+ end
162
+ end
@@ -2,18 +2,17 @@ require "sass-embedded"
2
2
 
3
3
  module Mint
4
4
  module CSS
5
- def self.container
6
- "container"
7
- end
5
+ CONTAINER = "container"
8
6
 
9
- # Maps a "DSL" onto actual CSS. Translates this ...
7
+ # Allows for a human-readable DSL to be used to generate CSS. Translates the following:
10
8
  #
11
9
  # ---
12
10
  # Font: Helvetica
13
11
  # Margin: 1in
14
12
  # Orientation: Landscape
13
+ # ---
15
14
  #
16
- # ... into something like:
15
+ # ... into something like this:
17
16
  #
18
17
  # #container {
19
18
  # font-family: Helvetica;
@@ -43,13 +42,14 @@ module Mint
43
42
  indent: "p+p { text-indent: %s }",
44
43
  bullet: "li { list-style-type: %s }",
45
44
  bullet_image: "li { list-style-image: url(%s) }",
46
- after_paragraph: "margin-bottom",
47
- before_paragraph: "margin-top"
45
+ after_paragraph: "p { margin-bottom: %s }",
46
+ before_paragraph: "p { margin-top: %s }"
48
47
  }
49
48
  end
50
49
 
51
50
  def self.stylify(key, value)
52
- selector = mappings[Helpers.symbolize key]
51
+ symbol_key = key.to_s.downcase.gsub(' ', '_').to_sym
52
+ selector = mappings[symbol_key]
53
53
 
54
54
  if selector.nil?
55
55
  ""
@@ -62,7 +62,7 @@ module Mint
62
62
 
63
63
  def self.parse(style)
64
64
  css = style.map {|k,v| stylify(k, v) }.join("\n ")
65
- container_scope = "##{container}\n #{css.strip}\n"
65
+ container_scope = "##{CONTAINER}\n #{css.strip}\n"
66
66
 
67
67
  # Suppress warnings by capturing $stderr
68
68
  original_stderr = $stderr
@@ -1,9 +1,9 @@
1
1
  require "pathname"
2
+ require "set"
2
3
 
3
4
  module Mint
4
5
  # Parses CSS files to extract @import statements and calculate relative paths
5
6
  class CssParser
6
-
7
7
  # Extracts @import statements from CSS content
8
8
  #
9
9
  # @param [String] css_content the CSS content to parse
@@ -11,58 +11,78 @@ module Mint
11
11
  def self.extract_imports(css_content)
12
12
  imports = []
13
13
 
14
- # Match @import statements with various formats:
14
+ # Remove CSS comments first to avoid matching commented @import statements
15
+ # Remove /* ... */ style comments
16
+ css_without_comments = css_content.gsub(/\/\*.*?\*\//m, '')
17
+
18
+ # Matched formats include:
15
19
  # @import "file.css";
16
20
  # @import 'file.css';
17
21
  # @import url("file.css");
18
22
  # @import url('file.css');
19
- css_content.scan(/@import\s+(?:url\()?['"]([^'"]+)['"](?:\))?;?/i) do |match|
23
+ css_without_comments.scan(/@import\s+(?:url\()?['"]([^'"]+)['"](?:\))?;?/i) do |match|
20
24
  imports << match[0]
21
25
  end
22
26
 
23
27
  imports
24
28
  end
25
29
 
26
- # Resolves all CSS files (main + imports) and calculates their paths relative to HTML output
30
+ # Recursively resolves a CSS file and its imports
27
31
  #
28
- # @param [String] main_css_path absolute path to the main CSS file
29
- # @param [String] html_output_path absolute path to the HTML output file
32
+ # @param [Pathname] css_file absolute path to the CSS file as Pathname
33
+ # @param [Pathname] html_dir directory of the HTML output file as Pathname
34
+ # @param [Set] visited set of already processed files to prevent circular imports
30
35
  # @return [Array<String>] array of relative paths from HTML to CSS files
31
- def self.resolve_css_files(main_css_path, html_output_path)
36
+ def self.resolve_css_file_recursive(css_file, html_dir, visited = Set.new)
32
37
  css_files = []
33
- main_css_file = Pathname.new(main_css_path)
34
- html_file = Pathname.new(html_output_path)
35
38
 
36
- # Add the main CSS file
37
- css_files << main_css_file.relative_path_from(html_file.dirname).to_s
39
+ # Prevent circular imports
40
+ return css_files if visited.include?(css_file.to_s)
41
+ visited.add(css_file.to_s)
38
42
 
39
- # Only process if main CSS file exists and is a .css file
40
- return css_files unless main_css_file.exist? && main_css_file.extname == '.css'
43
+ return css_files unless css_file.exist? && css_file.extname == '.css'
41
44
 
42
45
  begin
43
- css_content = File.read(main_css_path)
46
+ css_content = File.read(css_file)
44
47
  imports = extract_imports(css_content)
45
48
 
49
+ # Recursively process imported files first (they should load before the file that imports them)
46
50
  imports.each do |import_path|
47
- # Resolve import path relative to the main CSS file's directory
48
- import_file = main_css_file.dirname + import_path
49
-
50
- # Only include if it's a .css file and exists
51
- if import_file.exist? && import_file.extname == '.css'
52
- # Calculate path relative to HTML output
53
- relative_import = import_file.relative_path_from(html_file.dirname).to_s
54
- css_files << relative_import
55
- end
51
+ import_file = (css_file.dirname + import_path).expand_path
52
+ css_files.concat(resolve_css_file_recursive(import_file, html_dir, visited))
56
53
  end
57
54
 
58
- rescue => e
59
- # If we can't read the CSS file, just return the main file
55
+ # Add this file after its imports
56
+ relative_path = css_file.relative_path_from(html_dir).to_s
57
+ css_files << relative_path unless css_files.include?(relative_path)
58
+
59
+ rescue => _
60
+ # If we can't read the CSS file, skip it
60
61
  # This allows the system to gracefully handle missing or unreadable files
61
62
  end
62
63
 
63
64
  css_files
64
65
  end
65
66
 
67
+ # Resolves all CSS files (main + imports) and calculates their paths relative to HTML output
68
+ #
69
+ # @param [String] main_css_path absolute path to the main CSS file
70
+ # @param [String] html_output_path absolute path to the HTML output file
71
+ # @return [Array<String>] array of relative paths from HTML to CSS files
72
+ def self.resolve_css_files(main_css_path, html_output_path)
73
+ main_css_file = Pathname.new(main_css_path).expand_path
74
+ html_file = Pathname.new(html_output_path).expand_path
75
+ html_dir = html_file.dirname
76
+
77
+ # If the file doesn't exist or isn't CSS, return just the relative path
78
+ unless main_css_file.exist? && main_css_file.extname == '.css'
79
+ return [main_css_file.relative_path_from(html_dir).to_s]
80
+ end
81
+
82
+ # Use recursive resolution
83
+ resolve_css_file_recursive(main_css_file, html_dir)
84
+ end
85
+
66
86
  # Generates HTML link tags for CSS files
67
87
  #
68
88
  # @param [Array<String>] css_file_paths array of relative paths to CSS files