herb 0.9.7-arm-linux-gnu → 0.10.0-arm-linux-gnu

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 +4 -4
  2. data/README.md +1 -0
  3. data/ext/herb/extconf.rb +1 -0
  4. data/ext/herb/extension.c +108 -0
  5. data/herb.gemspec +1 -1
  6. data/lib/herb/3.2/herb.so +0 -0
  7. data/lib/herb/3.3/herb.so +0 -0
  8. data/lib/herb/3.4/herb.so +0 -0
  9. data/lib/herb/4.0/herb.so +0 -0
  10. data/lib/herb/action_view/render_analyzer.rb +1057 -0
  11. data/lib/herb/ast/erb_render_node.rb +155 -0
  12. data/lib/herb/bootstrap.rb +0 -1
  13. data/lib/herb/cli.rb +253 -19
  14. data/lib/herb/colors.rb +18 -0
  15. data/lib/herb/configuration.rb +49 -13
  16. data/lib/herb/defaults.yml +3 -0
  17. data/lib/herb/dev/runner.rb +445 -0
  18. data/lib/herb/dev/server.rb +207 -0
  19. data/lib/herb/dev/server_entry.rb +128 -0
  20. data/lib/herb/diff_operation.rb +34 -0
  21. data/lib/herb/diff_result.rb +59 -0
  22. data/lib/herb/engine/compiler.rb +56 -3
  23. data/lib/herb/engine/validators/render_validator.rb +92 -0
  24. data/lib/herb/engine.rb +58 -4
  25. data/lib/herb/html/util.rb +16 -0
  26. data/lib/herb/project.rb +1 -6
  27. data/lib/herb/version.rb +1 -1
  28. data/lib/herb.rb +41 -5
  29. data/sig/herb/action_view/render_analyzer.rbs +122 -0
  30. data/sig/herb/ast/erb_render_node.rbs +29 -0
  31. data/sig/herb/colors.rbs +12 -0
  32. data/sig/herb/configuration.rbs +20 -1
  33. data/sig/herb/dev/runner.rbs +59 -0
  34. data/sig/herb/dev/server.rbs +50 -0
  35. data/sig/herb/dev/server_entry.rbs +51 -0
  36. data/sig/herb/diff_operation.rbs +34 -0
  37. data/sig/herb/diff_result.rbs +34 -0
  38. data/sig/herb/engine/compiler.rbs +6 -0
  39. data/sig/herb/engine/validators/render_validator.rbs +21 -0
  40. data/sig/herb/engine.rbs +15 -0
  41. data/sig/herb/html/util.rbs +13 -0
  42. data/sig/herb.rbs +12 -2
  43. data/sig/herb_c_extension.rbs +1 -1
  44. data/sig/vendor/did_you_mean.rbs +6 -0
  45. data/sig/vendor/parallel.rbs +4 -0
  46. data/src/analyze/action_view/attribute_extraction_helpers.c +3 -2
  47. data/src/diff/herb_diff.c +137 -0
  48. data/src/diff/herb_diff_attributes.c +207 -0
  49. data/src/diff/herb_diff_children.c +518 -0
  50. data/src/diff/herb_diff_helpers.c +114 -0
  51. data/src/diff/herb_diff_nodes.c +707 -0
  52. data/src/diff/herb_hash.c +42 -0
  53. data/src/diff/herb_hash_index_map.c +47 -0
  54. data/src/diff/herb_hash_map.c +104 -0
  55. data/src/diff/herb_hash_tree.c +680 -0
  56. data/src/include/diff/herb_diff.h +118 -0
  57. data/src/include/diff/herb_hash.h +25 -0
  58. data/src/include/diff/herb_hash_index_map.h +32 -0
  59. data/src/include/diff/herb_hash_map.h +30 -0
  60. data/src/include/herb.h +1 -0
  61. data/src/include/version.h +1 -1
  62. data/templates/javascript/packages/core/src/config.ts.erb +43 -0
  63. data/templates/rust/src/ast/nodes.rs.erb +1 -1
  64. data/templates/rust/src/config.rs.erb +50 -0
  65. data/templates/src/diff/herb_diff_helpers.c.erb +38 -0
  66. data/templates/src/diff/herb_diff_nodes.c.erb +224 -0
  67. data/templates/src/diff/herb_hash_tree.c.erb +147 -0
  68. data/templates/template.rb +4 -4
  69. metadata +40 -4
  70. data/lib/herb/3.0/herb.so +0 -0
  71. data/lib/herb/3.1/herb.so +0 -0
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "did_you_mean"
4
+
5
+ module Herb
6
+ module AST
7
+ class ERBRenderNode < Node
8
+ PARTIAL_EXTENSIONS = Herb::PARTIAL_EXTENSIONS
9
+
10
+ def static_partial?
11
+ keywords&.partial && !keywords&.partial&.value&.empty?
12
+ end
13
+
14
+ def dynamic?
15
+ !static_partial? && (keywords&.object || keywords&.renderable)
16
+ end
17
+
18
+ def partial_path
19
+ keywords&.partial&.value
20
+ end
21
+
22
+ def template_name
23
+ keywords&.template_path&.value
24
+ end
25
+
26
+ def layout_name
27
+ keywords&.layout&.value
28
+ end
29
+
30
+ def local_names
31
+ keywords&.locals&.map { |local| local.name&.value }&.compact || []
32
+ end
33
+
34
+ def resolve(view_root: nil, source_directory: nil)
35
+ name = partial_path || template_name
36
+
37
+ return nil unless name
38
+
39
+ view_root = Pathname.new(view_root) unless view_root.nil? || view_root.is_a?(Pathname)
40
+
41
+ candidates = candidate_paths(name, view_root, source_directory)
42
+ candidates.find(&:exist?)
43
+ end
44
+
45
+ def candidate_paths(name = nil, view_root = nil, source_directory = nil)
46
+ name ||= partial_path || template_name
47
+
48
+ return [] unless name
49
+
50
+ view_root = Pathname.new(view_root) unless view_root.nil? || view_root.is_a?(Pathname)
51
+
52
+ directory = File.dirname(name) if name.include?("/")
53
+ base = name.include?("/") ? File.basename(name) : name
54
+ source_directory = Pathname.new(source_directory) if source_directory && !source_directory.is_a?(Pathname)
55
+
56
+ PARTIAL_EXTENSIONS.flat_map do |extension|
57
+ paths = [] #: Array[Pathname]
58
+
59
+ if directory
60
+ paths << view_root.join(directory, "_#{base}#{extension}") if view_root
61
+ else
62
+ paths << source_directory.join("_#{base}#{extension}") if source_directory
63
+ paths << view_root.join("_#{base}#{extension}") if view_root
64
+ end
65
+
66
+ paths
67
+ end
68
+ end
69
+
70
+ def similar_partials(view_root: nil, source_directory: nil, limit: 3)
71
+ name = partial_path || template_name
72
+
73
+ return [] unless name
74
+
75
+ suggestions = [] #: Array[String]
76
+
77
+ if view_root
78
+ view_root = Pathname.new(view_root) unless view_root.is_a?(Pathname)
79
+
80
+ if view_root.directory?
81
+ all_partials = Dir[File.join(view_root, "**", Herb::PARTIAL_GLOB_PATTERN)].map do |file|
82
+ relative = Pathname.new(file).relative_path_from(view_root).to_s
83
+ relative.sub(%r{(^|/)_}, '\1').sub(/\..*\z/, "")
84
+ end
85
+
86
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: all_partials)
87
+ suggestions = spell_checker.correct(name).first(limit)
88
+ end
89
+ elsif source_directory
90
+ source_directory = Pathname.new(source_directory) unless source_directory.is_a?(Pathname)
91
+
92
+ if source_directory.directory?
93
+ local_partials = Dir[File.join(source_directory, Herb::PARTIAL_GLOB_PATTERN)].map do |file|
94
+ File.basename(file).sub(/\A_/, "").sub(/\..*\z/, "")
95
+ end
96
+
97
+ unless local_partials.empty?
98
+ spell_checker = DidYouMean::SpellChecker.new(dictionary: local_partials)
99
+ suggestions = spell_checker.correct(name).first(limit)
100
+ end
101
+ end
102
+ end
103
+
104
+ if suggestions.empty?
105
+ suggestions.concat(find_non_partial_matches(name, view_root, source_directory))
106
+ end
107
+
108
+ suggestions
109
+ end
110
+
111
+ def find_non_partial_matches(name = nil, view_root = nil, source_directory = nil)
112
+ name ||= partial_path || template_name
113
+
114
+ return [] unless name
115
+
116
+ matches = [] #: Array[String]
117
+
118
+ PARTIAL_EXTENSIONS.each do |extension|
119
+ if name.include?("/")
120
+ next unless view_root
121
+
122
+ view_root = Pathname.new(view_root) unless view_root.is_a?(Pathname)
123
+ directory = File.dirname(name)
124
+ base = File.basename(name)
125
+ non_partial_path = view_root.join(directory, "#{base}#{extension}")
126
+
127
+ if non_partial_path.exist?
128
+ matches << "#{name}#{extension} exists as a template, not a partial. Rename to _#{base}#{extension} to use it with render"
129
+ end
130
+ else
131
+ if source_directory
132
+ source_directory = Pathname.new(source_directory) unless source_directory.is_a?(Pathname)
133
+ non_partial_path = source_directory.join("#{name}#{extension}")
134
+
135
+ if non_partial_path.exist?
136
+ matches << "#{name}#{extension} exists as a template, not a partial. Rename to _#{name}#{extension} to use it with render"
137
+ end
138
+ end
139
+
140
+ if view_root
141
+ view_root = Pathname.new(view_root) unless view_root.is_a?(Pathname)
142
+ non_partial_path = view_root.join("#{name}#{extension}")
143
+
144
+ if non_partial_path.exist?
145
+ matches << "#{name}#{extension} exists as a template, not a partial. Rename to _#{name}#{extension} to use it with render"
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ matches.uniq
152
+ end
153
+ end
154
+ end
155
+ end
@@ -18,7 +18,6 @@ module Herb
18
18
 
19
19
  def self.generate_templates
20
20
  require "pathname"
21
- require "set"
22
21
  require_relative "../../templates/template"
23
22
 
24
23
  Dir.chdir(ROOT_PATH) do
data/lib/herb/cli.rb CHANGED
@@ -8,7 +8,7 @@ require "optparse"
8
8
  class Herb::CLI
9
9
  include Herb::Colors
10
10
 
11
- attr_accessor :json, :silent, :log_file, :no_timing, :local, :escape, :no_escape, :freeze, :debug, :tool, :strict, :analyze, :track_whitespace, :verbose, :isolate, :arena_stats, :leak_check, :action_view_helpers, :trim
11
+ attr_accessor :json, :silent, :log_file, :no_timing, :local, :escape, :no_escape, :freeze, :debug, :tool, :strict, :analyze, :track_whitespace, :verbose, :isolate, :arena_stats, :leak_check, :action_view_helpers, :trim, :optimize
12
12
 
13
13
  def initialize(args)
14
14
  @args = args
@@ -17,7 +17,7 @@ class Herb::CLI
17
17
 
18
18
  def call
19
19
  options
20
- @file = @args[1]
20
+ @file = @args[1] unless @command == "dev"
21
21
 
22
22
  if silent
23
23
  if result.failed?
@@ -90,23 +90,29 @@ class Herb::CLI
90
90
  bundle exec herb [command] [options]
91
91
 
92
92
  Commands:
93
- bundle exec herb lex [file] Lex a file.
94
- bundle exec herb parse [file] Parse a file.
95
- bundle exec herb compile [file] Compile ERB template to Ruby code.
96
- bundle exec herb render [file] Compile and render ERB template to final output.
97
- bundle exec herb analyze [path] Analyze a project by passing a directory to the root of the project
98
- bundle exec herb report [file] Generate a Markdown bug report for a file
99
- bundle exec herb config [path] Show configuration and file patterns for a project
100
- bundle exec herb ruby [file] Extract Ruby from a file.
101
- bundle exec herb html [file] Extract HTML from a file.
102
- bundle exec herb playground [file] Open the content of the source file in the playground
103
- bundle exec herb version Prints the versions of the Herb gem and the libherb library.
104
-
105
- bundle exec herb lint [patterns] Lint templates (delegates to @herb-tools/linter)
106
- bundle exec herb format [patterns] Format templates (delegates to @herb-tools/formatter)
107
- bundle exec herb highlight [file] Syntax highlight templates (delegates to @herb-tools/highlighter)
108
- bundle exec herb print [file] Print AST (delegates to @herb-tools/printer)
109
- bundle exec herb lsp Start the language server (delegates to @herb-tools/language-server)
93
+ bundle exec herb lex [file] Lex a file.
94
+ bundle exec herb parse [file] Parse a file.
95
+ bundle exec herb compile [file] Compile ERB template to Ruby code.
96
+ bundle exec herb render [file] Compile and render ERB template to final output.
97
+ bundle exec herb analyze [path] Analyze a project by passing a directory to the root of the project.
98
+ bundle exec herb report [file] Generate a Markdown bug report for a file.
99
+ bundle exec herb config [path] Show configuration and file patterns for a project.
100
+ bundle exec herb ruby [file] Extract Ruby from a file.
101
+ bundle exec herb html [file] Extract HTML from a file.
102
+ bundle exec herb diff [old] [new] Diff two files and show the minimal set of AST differences.
103
+ bundle exec herb playground [file] Open the content of the source file in the playground.
104
+ bundle exec herb dev Start the dev server and watch for file changes.
105
+ bundle exec herb version Prints the versions of the Herb gem and the libherb library.
106
+
107
+ bundle exec herb actionview check [path] Check if render calls resolve to valid partial files.
108
+ bundle exec herb actionview graph [path] Show render dependency graph for a project or file.
109
+ bundle exec herb actionview render [file] Render ERB template using ActionView helpers.
110
+
111
+ bundle exec herb lint [patterns] Lint templates (delegates to @herb-tools/linter)
112
+ bundle exec herb format [patterns] Format templates (delegates to @herb-tools/formatter)
113
+ bundle exec herb highlight [file] Syntax highlight templates (delegates to @herb-tools/highlighter)
114
+ bundle exec herb print [file] Print AST (delegates to @herb-tools/printer)
115
+ bundle exec herb lsp Start the language server (delegates to @herb-tools/language-server)
110
116
 
111
117
  stdin:
112
118
  Commands that accept [file] also accept input via stdin:
@@ -197,6 +203,19 @@ class Herb::CLI
197
203
  system(%(open "#{url}##{hash}"))
198
204
  exit(0)
199
205
  end
206
+ when "dev"
207
+ case @args[1]
208
+ when "stop" then dev_stop
209
+ when "restart" then dev_restart
210
+ when "status" then dev_status
211
+ else
212
+ @file = @args[1]
213
+ run_dev_server
214
+ end
215
+ when "actionview"
216
+ run_actionview_command
217
+ when "diff"
218
+ diff_files
200
219
  when "lint"
201
220
  run_node_tool("herb-lint", "@herb-tools/linter")
202
221
  when "format"
@@ -306,6 +325,10 @@ class Herb::CLI
306
325
  self.trim = true
307
326
  end
308
327
 
328
+ parser.on("--optimize", "Enable compile-time optimizations for Action View helpers (for compile/render commands) (default: false)") do
329
+ self.optimize = true
330
+ end
331
+
309
332
  parser.on("--tool TOOL", "Show config for specific tool: linter, formatter (for config command)") do |t|
310
333
  self.tool = t.to_sym
311
334
  end
@@ -342,6 +365,143 @@ class Herb::CLI
342
365
  nil
343
366
  end
344
367
 
368
+ def run_actionview_command
369
+ subcommand = @args[1]
370
+ @file = @args[2]
371
+
372
+ target_path = @file ? File.expand_path(@file) : Dir.pwd
373
+ target_directory = File.directory?(target_path) ? target_path : File.dirname(target_path)
374
+ config = Herb::Configuration.new(target_directory)
375
+
376
+ if !(subcommand == "help" || subcommand.nil?) && (config.framework != "actionview")
377
+ project = config.project_root || target_directory
378
+ abort <<~MESSAGE
379
+ Herb also works outside of ActionView, but the `herb actionview` commands require the project to be explicitly configured for ActionView.
380
+
381
+ The project at '#{project}' is not configured to use ActionView (current framework: '#{config.framework}').
382
+
383
+ To enable ActionView support, add the following to your `.herb.yml`:
384
+
385
+ framework: actionview
386
+ MESSAGE
387
+ end
388
+
389
+ case subcommand
390
+ when "check"
391
+ require_relative "action_view/render_analyzer"
392
+
393
+ path = File.expand_path(@file || ".")
394
+
395
+ unless File.directory?(path)
396
+ puts "Not a directory: '#{path}'."
397
+ exit(1)
398
+ end
399
+
400
+ analyzer = Herb::ActionView::RenderAnalyzer.new(path)
401
+ has_issues = analyzer.check!
402
+
403
+ exit(has_issues ? 1 : 0)
404
+ when "graph"
405
+ require_relative "action_view/render_analyzer"
406
+
407
+ path = @file || "."
408
+
409
+ unless File.directory?(path) || File.file?(path)
410
+ puts "Not a file or directory: '#{path}'."
411
+ exit(1)
412
+ end
413
+
414
+ path = File.expand_path(path)
415
+ project_root = File.directory?(path) ? path : config.project_root&.to_s || File.dirname(path)
416
+ analyzer = Herb::ActionView::RenderAnalyzer.new(project_root)
417
+
418
+ if File.file?(path)
419
+ analyzer.graph_file!(path)
420
+ else
421
+ analyzer.graph!
422
+ end
423
+
424
+ exit(0)
425
+ when "render"
426
+ @file = @args[2]
427
+ actionview_render
428
+ when nil, "help"
429
+ puts <<~HELP
430
+ Herb ActionView Commands
431
+
432
+ Usage:
433
+ bundle exec herb actionview [subcommand] [options]
434
+
435
+ Subcommands:
436
+ check [path] Check if render calls resolve to valid partial files
437
+ graph [path] Show render dependency graph for a project or file
438
+ render [file] Render ERB template using ActionView helpers
439
+
440
+ Examples:
441
+ bundle exec herb actionview check
442
+ bundle exec herb actionview graph
443
+ bundle exec herb actionview graph app/views/posts/show.html.erb
444
+ bundle exec herb actionview render app/views/posts/show.html.erb
445
+
446
+ HELP
447
+ exit(0)
448
+ else
449
+ puts "Unknown actionview subcommand: '#{subcommand}'"
450
+ puts "Run 'herb actionview help' for available subcommands."
451
+ exit(1)
452
+ end
453
+ end
454
+
455
+ def actionview_render
456
+ require "action_view"
457
+
458
+ source = file_content
459
+
460
+ lookup_context = ActionView::LookupContext.new([])
461
+ view = ActionView::Base.with_empty_template_cache.new(lookup_context, {}, nil)
462
+ handler = ActionView::Template::Handlers::ERB.new
463
+
464
+ template = ActionView::Template.new(
465
+ source,
466
+ @file || "(eval)",
467
+ handler,
468
+ locals: [],
469
+ format: :html
470
+ )
471
+
472
+ rendered = template.render(view, {})
473
+
474
+ if json
475
+ puts({ success: true, output: rendered, source: source }.to_json)
476
+ elsif silent
477
+ puts "Success"
478
+ else
479
+ puts rendered
480
+ end
481
+
482
+ exit(0)
483
+ rescue LoadError
484
+ puts "Error: ActionView is required for 'herb actionview render'."
485
+ puts ""
486
+ puts "Add it to your Gemfile:"
487
+ puts " gem 'actionview'"
488
+ puts ""
489
+ puts "Or install it directly:"
490
+ puts " gem install actionview"
491
+ exit(1)
492
+ rescue StandardError => e
493
+ if json
494
+ puts({ success: false, error: e.message, source: source }.to_json)
495
+ elsif silent
496
+ puts "Failed"
497
+ else
498
+ puts "Error: #{e.class}: #{e.message}"
499
+ puts e.backtrace.first(5).map { |line| " #{line}" }.join("\n")
500
+ end
501
+
502
+ exit(1)
503
+ end
504
+
345
505
  def node_available?
346
506
  system("which node > /dev/null 2>&1")
347
507
  end
@@ -435,6 +595,77 @@ class Herb::CLI
435
595
  project.print_file_report(@file)
436
596
  end
437
597
 
598
+ def dev_stop
599
+ require_relative "dev/runner"
600
+ Herb::Dev::Runner.new.stop
601
+ end
602
+
603
+ def dev_restart
604
+ require_relative "dev/runner"
605
+ Herb::Dev::Runner.new(path: @file || ".").restart
606
+ end
607
+
608
+ def dev_status
609
+ require_relative "dev/runner"
610
+ Herb::Dev::Runner.new.status
611
+ end
612
+
613
+ def run_dev_server
614
+ require_relative "dev/runner"
615
+ Herb::Dev::Runner.new(path: @file || ".").run
616
+ end
617
+
618
+ def diff_files
619
+ old_file = @args[1]
620
+ new_file = @args[2]
621
+
622
+ if old_file.nil? || new_file.nil?
623
+ puts "Usage: herb diff <old_file> <new_file> [options]"
624
+ exit(1)
625
+ end
626
+
627
+ unless File.exist?(old_file)
628
+ puts "File doesn't exist: #{old_file}"
629
+ exit(1)
630
+ end
631
+
632
+ unless File.exist?(new_file)
633
+ puts "File doesn't exist: #{new_file}"
634
+ exit(1)
635
+ end
636
+
637
+ old_content = File.read(old_file)
638
+ new_content = File.read(new_file)
639
+
640
+ diff_result = Herb.diff(old_content, new_content)
641
+
642
+ if json
643
+ require "json"
644
+ puts JSON.pretty_generate(diff_result.to_hash)
645
+ elsif diff_result.identical?
646
+ puts "Trees are identical."
647
+ else
648
+ operations = diff_result.operations
649
+ puts "#{operations.size} difference#{"s" unless operations.size == 1} found:\n\n"
650
+
651
+ operations.each_with_index do |operation, index|
652
+ puts " #{index + 1}. #{operation.type} at path [#{operation.path.join(", ")}]"
653
+
654
+ if operation.old_node
655
+ puts " old: #{operation.old_node.type}"
656
+ end
657
+
658
+ if operation.new_node
659
+ puts " new: #{operation.new_node.type}"
660
+ end
661
+
662
+ puts
663
+ end
664
+ end
665
+
666
+ exit(0)
667
+ end
668
+
438
669
  def compile_template
439
670
  require_relative "engine"
440
671
 
@@ -450,6 +681,7 @@ class Herb::CLI
450
681
  options[:debug_filename] = @file if @file
451
682
  end
452
683
 
684
+ options[:optimize] = true if optimize
453
685
  options[:trim] = true if trim
454
686
  options[:validate_ruby] = true
455
687
  engine = Herb::Engine.new(file_content, options)
@@ -538,7 +770,9 @@ class Herb::CLI
538
770
  options[:debug_filename] = @file if @file
539
771
  end
540
772
 
773
+ options[:optimize] = true if optimize
541
774
  options[:trim] = true if trim
775
+
542
776
  engine = Herb::Engine.new(file_content, options)
543
777
  compiled_code = engine.src
544
778
 
data/lib/herb/colors.rb CHANGED
@@ -5,6 +5,10 @@
5
5
 
6
6
  module Herb
7
7
  module Colors
8
+ HIDE_CURSOR = "\e[?25l"
9
+ SHOW_CURSOR = "\e[?25h"
10
+ CLEAR_SCREEN = "\e[2J\e[H"
11
+
8
12
  module_function
9
13
 
10
14
  #: () -> bool
@@ -78,5 +82,19 @@ module Herb
78
82
 
79
83
  "\e[1m#{string}\e[0m"
80
84
  end
85
+
86
+ #: (String, Integer) -> String
87
+ def fg(string, color)
88
+ return string unless enabled?
89
+
90
+ "\e[38;5;#{color}m#{string}\e[0m"
91
+ end
92
+
93
+ #: (String, Integer, Integer) -> String
94
+ def fg_bg(string, foreground, background)
95
+ return string unless enabled?
96
+
97
+ "\e[38;5;#{foreground};48;5;#{background}m#{string}\e[0m"
98
+ end
81
99
  end
82
100
  end
@@ -5,6 +5,12 @@ require "pathname"
5
5
 
6
6
  module Herb
7
7
  class Configuration
8
+ OPTIONS_PATH = File.expand_path("../../config/options.yml", __dir__ || __FILE__).freeze #: String
9
+ OPTIONS = YAML.safe_load_file(OPTIONS_PATH).freeze #: Hash[String, untyped]
10
+
11
+ VALID_FRAMEWORKS = OPTIONS["framework"]["values"].freeze #: Array[String]
12
+ VALID_TEMPLATE_ENGINES = OPTIONS["template_engine"]["values"].freeze #: Array[String]
13
+
8
14
  CONFIG_FILENAMES = [".herb.yml"].freeze
9
15
 
10
16
  PROJECT_INDICATORS = [
@@ -22,12 +28,13 @@ module Herb
22
28
  DEFAULTS_PATH = File.expand_path("defaults.yml", __dir__ || __FILE__).freeze
23
29
  DEFAULTS = YAML.safe_load_file(DEFAULTS_PATH).freeze
24
30
 
25
- attr_reader :config, :config_path, :project_root
31
+ attr_reader :config, :user_config, :config_path, :project_root
26
32
 
27
33
  def initialize(project_path = nil)
28
34
  @start_path = project_path ? Pathname.new(project_path) : Pathname.pwd
29
35
  @config_path, @project_root = find_config_file
30
- @config = load_config
36
+ @user_config = load_user_config
37
+ @config = deep_merge(DEFAULTS, @user_config)
31
38
  end
32
39
 
33
40
  def [](key)
@@ -42,6 +49,30 @@ module Herb
42
49
  @config["version"]
43
50
  end
44
51
 
52
+ #: () -> String
53
+ def framework
54
+ value = @config["framework"] || "ruby"
55
+
56
+ unless VALID_FRAMEWORKS.include?(value)
57
+ warn "[Herb] Unknown framework: #{value.inspect}. Valid values: #{VALID_FRAMEWORKS.join(", ")}. Defaulting to 'ruby'."
58
+ return "ruby"
59
+ end
60
+
61
+ value
62
+ end
63
+
64
+ #: () -> String
65
+ def template_engine
66
+ value = @config["template_engine"] || "erubi"
67
+
68
+ unless VALID_TEMPLATE_ENGINES.include?(value)
69
+ warn "[Herb] Unknown template_engine: #{value.inspect}. Valid values: #{VALID_TEMPLATE_ENGINES.join(", ")}. Defaulting to 'erubi'."
70
+ return "erubi"
71
+ end
72
+
73
+ value
74
+ end
75
+
45
76
  def files
46
77
  @config["files"] || {}
47
78
  end
@@ -62,6 +93,11 @@ module Herb
62
93
  @config["engine"] || {}
63
94
  end
64
95
 
96
+ #: (String, untyped) -> untyped
97
+ def engine_option(key, default = nil)
98
+ engine.fetch(key.to_s, default)
99
+ end
100
+
65
101
  def enabled_validators(overrides = {})
66
102
  config = dig("engine", "validators") || {}
67
103
 
@@ -221,26 +257,26 @@ module Herb
221
257
  end
222
258
  end
223
259
 
224
- def load_config
225
- return deep_merge(DEFAULTS, {}) unless @config_path&.exist?
260
+ def load_user_config
261
+ return {} unless @config_path&.exist?
226
262
 
227
263
  begin
228
- user_config = YAML.safe_load_file(@config_path, permitted_classes: [Symbol]) || {}
229
- deep_merge(DEFAULTS, user_config)
264
+ YAML.safe_load_file(@config_path, permitted_classes: [Symbol]) || {}
230
265
  rescue Psych::SyntaxError => e
231
266
  warn "Warning: Invalid YAML in #{@config_path}: #{e.message}"
232
- deep_merge(DEFAULTS, {})
267
+
268
+ {}
233
269
  end
234
270
  end
235
271
 
236
272
  def deep_merge(base, override, additive_keys: ["include", "exclude"])
237
- base.merge(override) do |key, old_val, new_val|
238
- if old_val.is_a?(Hash) && new_val.is_a?(Hash)
239
- deep_merge(old_val, new_val, additive_keys: additive_keys)
240
- elsif old_val.is_a?(Array) && new_val.is_a?(Array) && additive_keys.include?(key)
241
- old_val + new_val
273
+ base.merge(override) do |key, old_value, new_value|
274
+ if old_value.is_a?(Hash) && new_value.is_a?(Hash)
275
+ deep_merge(old_value, new_value, additive_keys: additive_keys)
276
+ elsif old_value.is_a?(Array) && new_value.is_a?(Array) && additive_keys.include?(key)
277
+ old_value + new_value
242
278
  else
243
- new_val
279
+ new_value
244
280
  end
245
281
  end
246
282
  end
@@ -1,3 +1,6 @@
1
+ framework: ruby
2
+ template_engine: erubi
3
+
1
4
  files:
2
5
  include:
3
6
  - "**/*.herb"