Class: Herb::CLI

Inherits:
Object
  • Object
show all
Includes:
Colors
Defined in:
lib/herb/cli.rb

Constant Summary

Constants included from Colors

Herb::Colors::CLEAR_SCREEN, Herb::Colors::HIDE_CURSOR, Herb::Colors::SHOW_CURSOR

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Colors

bold, bright_magenta, cyan, dimmed, enabled?, fg, fg_bg, green, magenta, red, white, yellow

Constructor Details

#initialize(args) ⇒ CLI

Returns a new instance of CLI.



13
14
15
16
# File 'lib/herb/cli.rb', line 13

def initialize(args)
  @args = args
  @command = args[0]
end

Instance Attribute Details

#action_view_helpersObject

Returns the value of attribute action_view_helpers.



11
12
13
# File 'lib/herb/cli.rb', line 11

def action_view_helpers
  @action_view_helpers
end

#analyzeObject

Returns the value of attribute analyze.



11
12
13
# File 'lib/herb/cli.rb', line 11

def analyze
  @analyze
end

#arena_statsObject

Returns the value of attribute arena_stats.



11
12
13
# File 'lib/herb/cli.rb', line 11

def arena_stats
  @arena_stats
end

#debugObject

Returns the value of attribute debug.



11
12
13
# File 'lib/herb/cli.rb', line 11

def debug
  @debug
end

#escapeObject

Returns the value of attribute escape.



11
12
13
# File 'lib/herb/cli.rb', line 11

def escape
  @escape
end

#freezeObject

Returns the value of attribute freeze.



11
12
13
# File 'lib/herb/cli.rb', line 11

def freeze
  @freeze
end

#isolateObject

Returns the value of attribute isolate.



11
12
13
# File 'lib/herb/cli.rb', line 11

def isolate
  @isolate
end

#jsonObject

Returns the value of attribute json.



11
12
13
# File 'lib/herb/cli.rb', line 11

def json
  @json
end

#leak_checkObject

Returns the value of attribute leak_check.



11
12
13
# File 'lib/herb/cli.rb', line 11

def leak_check
  @leak_check
end

#localObject

Returns the value of attribute local.



11
12
13
# File 'lib/herb/cli.rb', line 11

def local
  @local
end

#log_fileObject

Returns the value of attribute log_file.



11
12
13
# File 'lib/herb/cli.rb', line 11

def log_file
  @log_file
end

#no_escapeObject

Returns the value of attribute no_escape.



11
12
13
# File 'lib/herb/cli.rb', line 11

def no_escape
  @no_escape
end

#no_timingObject

Returns the value of attribute no_timing.



11
12
13
# File 'lib/herb/cli.rb', line 11

def no_timing
  @no_timing
end

#optimizeObject

Returns the value of attribute optimize.



11
12
13
# File 'lib/herb/cli.rb', line 11

def optimize
  @optimize
end

#silentObject

Returns the value of attribute silent.



11
12
13
# File 'lib/herb/cli.rb', line 11

def silent
  @silent
end

#strictObject

Returns the value of attribute strict.



11
12
13
# File 'lib/herb/cli.rb', line 11

def strict
  @strict
end

#toolObject

Returns the value of attribute tool.



11
12
13
# File 'lib/herb/cli.rb', line 11

def tool
  @tool
end

#track_whitespaceObject

Returns the value of attribute track_whitespace.



11
12
13
# File 'lib/herb/cli.rb', line 11

def track_whitespace
  @track_whitespace
end

#trimObject

Returns the value of attribute trim.



11
12
13
# File 'lib/herb/cli.rb', line 11

def trim
  @trim
end

#verboseObject

Returns the value of attribute verbose.



11
12
13
# File 'lib/herb/cli.rb', line 11

def verbose
  @verbose
end

Instance Method Details

#callObject



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/herb/cli.rb', line 18

def call
  options
  @file = @args[1] unless @command == "dev"

  if silent
    if result.failed?
      puts "Failed"
    else
      puts "Success"
    end
  elsif json
    puts result.value.to_json
  else
    puts result.value.inspect

    print_error_summary(result.errors) if @command == "parse" && result.respond_to?(:errors) && result.errors.any?
  end
end

#directoryObject



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/herb/cli.rb', line 37

def directory
  unless @file
    puts "No directory provided."
    puts
    puts "Usage:"
    puts "  bundle exec herb #{@command} [directory] [options]"
    puts
    puts "Tip: Use `.` for the current directory"
    puts "  bundle exec herb #{@command} . [options]"

    exit(1)
  end

  unless File.exist?(@file)
    puts "Not a file: '#{@file}'."
    puts
  end

  unless File.directory?(@file)
    puts "Not a directory: '#{@file}'."
    puts
  end

  @file
end

#file_contentObject



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/herb/cli.rb', line 63

def file_content
  if @file && @file != "-" && File.exist?(@file)
    File.read(@file)
  elsif @file && @file != "-"
    puts "File doesn't exist: #{@file}"
    exit(1)
  elsif @file == "-" || !$stdin.tty?
    $stdin.read
  else
    puts "No file provided."
    puts
    puts "Usage:"
    puts "  bundle exec herb #{@command} [file] [options]"
    puts
    puts "You can also pipe content via stdin:"
    puts "  echo \"<div>Hello</div>\" | bundle exec herb #{@command}"
    puts "  cat file.html.erb | bundle exec herb #{@command}"
    puts "  bundle exec herb #{@command} -"
    exit(1)
  end
end

#help(exit_code = 0) ⇒ Object



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/herb/cli.rb', line 85

def help(exit_code = 0)
  message = <<~HELP
    Herb 🌿 Powerful and seamless HTML-aware ERB toolchain.

    Usage:
      bundle exec herb [command] [options]

    Commands:
      bundle exec herb lex [file]                 Lex a file.
      bundle exec herb parse [file]               Parse a file.
      bundle exec herb compile [file]             Compile ERB template to Ruby code.
      bundle exec herb render [file]              Compile and render ERB template to final output.
      bundle exec herb analyze [path]             Analyze a project by passing a directory to the root of the project.
      bundle exec herb report [file]              Generate a Markdown bug report for a file.
      bundle exec herb config [path]              Show configuration and file patterns for a project.
      bundle exec herb ruby [file]                Extract Ruby from a file.
      bundle exec herb html [file]                Extract HTML from a file.
      bundle exec herb diff [old] [new]           Diff two files and show the minimal set of AST differences.
      bundle exec herb playground [file]          Open the content of the source file in the playground.
      bundle exec herb dev                        Start the dev server and watch for file changes.
      bundle exec herb version                    Prints the versions of the Herb gem and the libherb library.

      bundle exec herb actionview check [path]    Check if render calls resolve to valid partial files.
      bundle exec herb actionview graph [path]    Show render dependency graph for a project or file.
      bundle exec herb actionview render [file]   Render ERB template using ActionView helpers.

      bundle exec herb lint [patterns]            Lint templates (delegates to @herb-tools/linter)
      bundle exec herb format [patterns]          Format templates (delegates to @herb-tools/formatter)
      bundle exec herb highlight [file]           Syntax highlight templates (delegates to @herb-tools/highlighter)
      bundle exec herb print [file]               Print AST (delegates to @herb-tools/printer)
      bundle exec herb lsp                        Start the language server (delegates to @herb-tools/language-server)

    stdin:
      Commands that accept [file] also accept input via stdin:
        echo "<div>Hello</div>" | bundle exec herb lex
        cat file.html.erb | bundle exec herb parse

      Use `-` to explicitly read from stdin:
        bundle exec herb compile -

    Options:
      #{option_parser.to_s.strip.gsub(/^    /, "  ")}

  HELP

  puts message

  exit(exit_code)
end

#option_parserObject



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/herb/cli.rb', line 243

def option_parser
  @option_parser ||= OptionParser.new do |parser|
    parser.banner = ""

    parser.on_tail("-v", "--version", "Show the version") do
      print_version
    end

    parser.on_tail("-h", "--help", "Show this message") do
      help
      exit(0)
    end

    parser.on("-j", "--json", "Return result in the JSON format") do
      self.json = true
    end

    parser.on("-s", "--silent", "Log no result to stdout") do
      self.silent = true
    end

    parser.on("--verbose", "Show detailed per-file progress (default in CI)") do
      self.verbose = true
    end

    parser.on("--isolate", "Fork each file into its own process for crash isolation (slower)") do
      self.isolate = true
    end

    parser.on("--log-file", "Enable log file generation") do
      self.log_file = true
    end

    parser.on("--no-timing", "Disable timing output") do
      self.no_timing = true
    end

    parser.on("--local", "Use localhost for playground command instead of herb-tools.dev") do
      self.local = true
    end

    parser.on("--escape", "Enable HTML escaping by default (for compile command)") do
      self.escape = true
    end

    parser.on("--no-escape", "Disable HTML escaping by default (for compile command)") do
      self.no_escape = true
    end

    parser.on("--freeze", "Add frozen string literal pragma (for compile command)") do
      self.freeze = true
    end

    parser.on("--debug", "Enable debug mode with ERB expression wrapping (for compile command)") do
      self.debug = true
    end

    parser.on("--strict", "Enable strict mode - report errors for omitted closing tags (for parse/compile/render commands) (default: true)") do
      self.strict = true
    end

    parser.on("--no-strict", "Disable strict mode (for parse/compile/render commands)") do
      self.strict = false
    end

    parser.on("--analyze", "Enable analyze mode (for parse command) (default: true)") do
      self.analyze = true
    end

    parser.on("--no-analyze", "Disable analyze mode (for parse command)") do
      self.analyze = false
    end

    parser.on("--track-whitespace", "Enable whitespace tracking (for parse command) (default: false)") do
      self.track_whitespace = true
    end

    parser.on("--action-view-helpers", "Enable Action View helper detection (for parse command) (default: false)") do
      self.action_view_helpers = true
    end

    parser.on("--trim", "Enable trimming of leading/trailing whitespace (for compile/render commands)") do
      self.trim = true
    end

    parser.on("--optimize", "Enable compile-time optimizations for Action View helpers (for compile/render commands) (default: false)") do
      self.optimize = true
    end

    parser.on("--tool TOOL", "Show config for specific tool: linter, formatter (for config command)") do |t|
      self.tool = t.to_sym
    end

    parser.on("--arena-stats", "Print arena memory statistics (for lex/parse/analyze commands)") do
      self.arena_stats = true
    end

    parser.on("--leak-check", "Check for memory leaks in lex/parse/extract operations (for analyze command)") do
      self.leak_check = true
    end
  end
end

#optionsObject



346
347
348
349
350
# File 'lib/herb/cli.rb', line 346

def options
  return if ["lint", "format", "print", "highlight", "lsp"].include?(@command)

  option_parser.parse!(@args)
end

#resultObject



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/herb/cli.rb', line 135

def result
  @result ||= case @command
              when "analyze"
                path = @file || "."

                if path != "-" && File.file?(path)
                  project = Herb::Project.new(File.dirname(path))
                  project.file_paths = [File.expand_path(path)]
                else
                  unless File.directory?(path)
                    puts "Not a file or directory: '#{path}'."
                    exit(1)
                  end

                  project = Herb::Project.new(path)
                end

                project.no_log_file = log_file ? false : true
                project.no_timing = no_timing
                project.silent = silent
                project.verbose = verbose || ci?
                project.isolate = isolate
                project.validate_ruby = true
                project.arena_stats = arena_stats
                project.leak_check = leak_check
                has_issues = project.analyze!
                exit(has_issues ? 1 : 0)
              when "report"
                generate_report
                exit(0)
              when "config"
                show_config
                exit(0)
              when "parse"
                Herb.parse(file_content, strict: strict.nil? || strict, analyze: analyze.nil? || analyze, track_whitespace: track_whitespace || false, arena_stats: arena_stats, action_view_helpers: action_view_helpers || false)
              when "compile"
                compile_template
              when "render"
                render_template
              when "lex"
                Herb.lex(file_content, arena_stats: arena_stats)
              when "ruby"
                puts Herb.extract_ruby(file_content)
                exit(0)
              when "html"
                puts Herb.extract_html(file_content)
                exit(0)
              when "playground"
                require "bundler/inline"

                gemfile do
                  source "https://rubygems.org"
                  gem "lz_string"
                end

                hash = LZString::UriSafe.compress(file_content)
                local_url = "http://localhost:5173"
                url = "https://herb-tools.dev/playground"

                if local
                  if Dir.pwd.include?("/herb")
                    system(%(npx concurrently "nx dev playground" "sleep 1 && open #{local_url}##{hash}"))
                    exit(0)
                  else
                    puts "This command can currently only be run within the herb repo itself"
                    exit(1)
                  end
                else
                  system(%(open "#{url}##{hash}"))
                  exit(0)
                end
              when "dev"
                case @args[1]
                when "stop" then dev_stop
                when "restart" then dev_restart
                when "status" then dev_status
                else
                  @file = @args[1]
                  run_dev_server
                end
              when "actionview"
                run_actionview_command
              when "diff"
                diff_files
              when "lint"
                run_node_tool("herb-lint", "@herb-tools/linter")
              when "format"
                run_node_tool("herb-format", "@herb-tools/formatter")
              when "print"
                run_node_tool("herb-print", "@herb-tools/printer")
              when "highlight"
                run_node_tool("herb-highlight", "@herb-tools/highlighter")
              when "lsp"
                run_node_tool("herb-language-server", "@herb-tools/language-server")
              when "help"
                help
              when "version"
                print_version
              when String
                puts "Unknown command: '#{@command}'"
                puts

                help(1)
              else
                help(1)
              end
end