Module: RedQuilt::CLI

Defined in:
lib/red_quilt/cli.rb

Overview

Entry point for the ‘redquilt` executable. Defined as a module-level function so tests can drive it without shelling out.

CLI.run takes an argv-style array and an optional set of IO objects (stdin / stdout / stderr) for testability. It returns an Integer exit code: 0 on success, 1 on usage errors.

Constant Summary collapse

USAGE =
<<~USAGE
  Usage: redquilt [options] [file]

  Reads Markdown from FILE (or stdin if FILE is omitted) and writes the
  result to stdout.

  Options:
USAGE
DEFAULTS =
{
  format: :html,
  allow_html: false,
  disallow_raw_html: false,
  extended_autolinks: false,
  lint: false,
  diagnostics: false,
  diagnostics_only: false,
  standalone: true,
  auto_title: false,
  title: nil,
  lang: "en",
  css: nil,
  theme: :default,
}.freeze
THEMES =
%i[none default].freeze
FORMATS =
%i[html ast json].freeze

Class Method Summary collapse

Class Method Details

.parse_options(argv, stderr:) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
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
134
135
136
137
138
139
140
141
142
# File 'lib/red_quilt/cli.rb', line 74

def self.parse_options(argv, stderr:)
  options = DEFAULTS.dup
  parser = OptionParser.new do |opts|
    opts.banner = USAGE
    opts.on("--format FORMAT", FORMATS, "Output format: html (default), ast, json") do |f|
      options[:format] = f
    end
    opts.on("--allow-html", "Pass raw HTML through to the output") do
      options[:allow_html] = true
    end
    opts.on("--disallow-raw-html",
            "Filter dangerous tags (script, iframe, ...) even with --allow-html (GFM)") do
      options[:disallow_raw_html] = true
    end
    opts.on("--extended-autolinks",
            "Linkify bare URLs and email addresses (GFM)") do
      options[:extended_autolinks] = true
    end
    opts.on("--lint",
            "Emit lint-style diagnostics (empty_link, missing_alt, heading_level_skip)") do
      options[:lint] = true
    end
    opts.on("--[no-]standalone",
            "Wrap (or not) the rendered HTML in a full document (default: on)") do |v|
      options[:standalone] = v
    end
    opts.on("--auto-title",
            "Use the first heading's text as <title> (standalone only)") do
      options[:auto_title] = true
    end
    opts.on("--title TITLE", "Explicit <title> text (standalone only)") do |t|
      options[:title] = t
    end
    opts.on("--lang LANG", "html lang attribute (standalone only; default \"en\")") do |l|
      options[:lang] = l
    end
    opts.on("--css URL", "Add a stylesheet link (standalone only)") do |u|
      options[:css] = u
    end
    opts.on("--theme THEME", THEMES,
            "Embedded stylesheet: default (the default) or none (bare HTML)") do |t|
      options[:theme] = t
    end
    opts.on("--diagnostics", "Also print diagnostics to stderr") do
      options[:diagnostics] = true
    end
    opts.on("--diagnostics-only", "Print diagnostics only (suppress normal output)") do
      options[:diagnostics_only] = true
    end
    opts.on("-h", "--help", "Show this help") do
      stderr.puts opts
      return 0
    end
    opts.on("-v", "--version", "Show version") do
      stderr.puts "redquilt #{RedQuilt::VERSION}"
      return 0
    end
  end

  begin
    parser.parse!(argv)
  rescue OptionParser::ParseError => e
    stderr.puts "redquilt: #{e.message}"
    stderr.puts parser
    return 1
  end

  options
end

.read_source(argv, stdin:, stderr:) ⇒ Object



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/red_quilt/cli.rb', line 144

def self.read_source(argv, stdin:, stderr:)
  if argv.empty?
    stdin.read
  elsif argv.size == 1
    path = argv.first
    unless File.file?(path)
      stderr.puts "redquilt: no such file: #{path}"
      return nil
    end
    File.read(path)
  else
    stderr.puts "redquilt: too many arguments: #{argv.inspect}"
    nil
  end
end

.render_html(doc, options) ⇒ Object



160
161
162
163
164
165
166
167
168
169
170
# File 'lib/red_quilt/cli.rb', line 160

def self.render_html(doc, options)
  title = options[:title]
  title = doc.first_heading_text.to_s if title.nil? && options[:auto_title]
  doc.to_html(
    standalone: options[:standalone],
    title: title,
    lang: options[:lang],
    css: options[:css],
    theme: options[:theme],
  )
end

.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr) ⇒ Object



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/red_quilt/cli.rb', line 42

def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
  options = parse_options(argv, stderr: stderr)
  return options if options.is_a?(Integer)

  source = read_source(argv, stdin: stdin, stderr: stderr)
  return 1 unless source

  doc = RedQuilt.parse(source,
                       allow_html: options[:allow_html],
                       disallow_raw_html: options[:disallow_raw_html],
                       extended_autolinks: options[:extended_autolinks],
                       lint: options[:lint])

  unless options[:diagnostics_only]
    case options[:format]
    when :html
      stdout.write(render_html(doc, options))
    when :ast
      require "pp"
      PP.pp(doc.to_ast, stdout)
    when :json
      stdout.puts doc.to_json
    end
  end

  if options[:diagnostics] || options[:diagnostics_only]
    write_diagnostics(doc.diagnostics, stderr)
  end

  doc.diagnostics.any? { |d| d.severity == :error } ? 1 : 0
end

.write_diagnostics(diagnostics, stderr) ⇒ Object



172
173
174
175
176
177
178
179
180
# File 'lib/red_quilt/cli.rb', line 172

def self.write_diagnostics(diagnostics, stderr)
  if diagnostics.empty?
    stderr.puts "redquilt: no diagnostics"
    return
  end
  diagnostics.each do |d|
    stderr.puts "[#{d.severity}] #{d.rule}: #{d.message}"
  end
end