Skip to content

Commit

Permalink
rebuild the cli without thor
Browse files Browse the repository at this point in the history
  • Loading branch information
jneen committed Sep 17, 2013
1 parent c0da914 commit 20edfc6
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 76 deletions.
11 changes: 7 additions & 4 deletions bin/rougify
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ load ROOT_DIR.join('lib/rouge.rb')
load ROOT_DIR.join('lib/rouge/cli.rb')

begin
Rouge::CLI.start
rescue => e
$stderr.puts "rouge: #{e.message}"
exit 1
Rouge::CLI.parse(ARGV).run
rescue Rouge::CLI::Error => e
puts e.message
exit e.status
rescue Interrupt
$stderr.puts "\nrouge: interrupted"
exit 2
end
336 changes: 266 additions & 70 deletions lib/rouge/cli.rb
Original file line number Diff line number Diff line change
@@ -1,100 +1,296 @@
# not required by the main lib.
# to use this module, require 'rouge/cli'.

# stdlib
require 'optparse'
module Rouge
class CLI
def self.doc
return enum_for(:doc) unless block_given?

# gems
require 'thor'
yield %|usage: rougify [command] [args...]|
yield %||
yield %|where <command> is one of:|
yield %| highlight #{Highlight.desc}|
yield %| help #{Help.desc}|
yield %| style #{Style.desc}|
yield %| list #{List.desc}|
yield %||
yield %|See `rougify help <command>` for more info.|
end

module Rouge
class CLI < Thor
default_task :highlight
class Error < StandardError
attr_reader :message, :status
def initialize(message, status=1)
@message = message
@status = status
end
end

def self.start(argv=ARGV, *a)
if argv.include? '-v' or argv.include? '--version'
puts Rouge.version
exit 0
def self.parse(argv=ARGV)
argv = normalize_syntax(argv)

mode = argv.shift

klass = class_from_arg(mode)
return klass.parse(argv) if klass

case mode
when '-h', '--help', 'help', '-help'
Help.parse(argv)
else
argv.unshift(mode) if mode
Highlight.parse(argv)
end
end

def initialize(options={})
end

def error!(msg, status=1)
raise Error.new(msg, status)
end

def self.class_from_arg(arg)
case arg
when 'help'
Help
when 'highlight', 'hi'
Highlight
when 'style'
Style
when 'list'
List
end
end

unless %w(highlight style list --help -h help).include?(argv.first)
argv.unshift 'highlight'
class Help < CLI
def self.desc
"print help info"
end

super(argv, *a)
def self.doc
return enum_for(:doc) unless block_given?

yield %|usage: rougify help <command>|
yield %||
yield %|print help info for <command>.|
end

def self.parse(argv)
opts = { :mode => CLI }
until argv.empty?
arg = argv.shift
klass = class_from_arg(arg)
if klass
opts[:mode] = klass
next
end
end
new(opts)
end

def initialize(opts={})
@mode = opts[:mode]
end

def run
@mode.doc.each(&method(:puts))
end
end

desc 'highlight [FILE]', 'highlight some code'
option :input_file, :aliases => '-i', :desc => 'the file to operate on'
option :lexer, :aliases => '-l',
:desc => ('Which lexer to use. If not provided, rougify will try to ' +
'guess based on --mimetype, the filename, and the file ' +
'contents.')
option :formatter, :aliases => '-f', :default => 'terminal256',
:desc => ('Which formatter to use.')
option :mimetype, :aliases => '-m',
:desc => ('a mimetype that Rouge will use to guess the correct lexer. ' +
'This is ignored if --lexer is specified.')
option :lexer_opts, :aliases => '-L', :type => :hash, :default => {},
:desc => ('a hash of options to pass to the lexer.')
option :formatter_opts, :aliases => '-F', :type => :hash, :default => {},
:desc => ('a hash of options to pass to the formatter.')
def highlight(file=nil)
filename = options[:file] || file
source = filename ? File.read(filename) : $stdin.read

if options[:lexer].nil?
lexer_class = Lexer.guess(
:filename => filename,
:mimetype => options[:mimetype],
:source => source,
)
else
lexer_class = Lexer.find(options[:lexer])
raise "unknown lexer: #{options[:lexer]}" unless lexer_class
class Highlight < CLI
def self.desc
"highlight code"
end

def self.doc
return enum_for(:doc) unless block_given?

yield %[usage: rougify highlight <filename> [options...]]
yield %[ rougify highlight [options...]]
yield %[]
yield %[--input-file|-i <filename> specify a file to read, or - to use stdin]
yield %[]
yield %[--lexer|-l <lexer> specify the lexer to use.]
yield %[ If not provided, rougify will try to guess]
yield %[ based on --mimetype, the filename, and the]
yield %[ file contents.]
yield %[]
yield %[--mimetype|-m <mimetype> specify a mimetype for lexer guessing]
yield %[]
yield %[--lexer-opts|-L <opts> specify lexer options in CGI format]
yield %[ (opt1=val1&opt2=val2)]
yield %[]
yield %[--formatter-opts|-F <opts> specify formatter options in CGI format]
yield %[ (opt1=val1&opt2=val2)]
end

def self.parse(argv)
opts = {
:formatter => 'terminal256',
:input_file => '-',
:lexer_opts => {},
:formatter_opts => {},
}

until argv.empty?
arg = argv.shift
case arg
when '--input-file', '-i'
opts[:input_file] = argv.shift
when '--mimetype', '-m'
opts[:mimetype] = argv.shift
when '--lexer', '-l'
opts[:lexer] = argv.shift
when '--formatter', '-f'
opts[:formatter] = argv.shift
when '--lexer-opts', '-L'
opts[:lexer_opts] = parse_cgi(argv.shift)
when '--formatter-opts', '-F'
opts[:formatter_opts] = parse_cgi(argv.shift)
when /^--/
error! "unknown option #{arg.inspect}"
else
opts[:input_file] = arg
end
end

new(opts)
end

formatter_class = Formatter.find(options[:formatter])
def initialize(opts={})
@input = case opts[:input_file]
when '-'
$stdin.read
else
begin
File.read(opts[:input_file])
rescue => e
error! "unable to read #{opts[:input_file].inspect}: #{e.message}"
end
end

if opts[:lexer]
lexer_class = Lexer.find(opts[:lexer]) \
or error! "unkown lexer #{opts[:lexer].inspect}"
else
lexer_class = Lexer.guess(
:filename => opts[:input_file],
:mimetype => opts[:mimetype],
:source => @input,
)
end

# only HTML is supported for now
formatter = formatter_class.new(normalize_hash_keys(options[:formatter_opts]))
lexer = lexer_class.new(normalize_hash_keys(options[:lexer_opts]))
@lexer = lexer_class.new(opts[:lexer_opts])

formatter.format(lexer.lex(source), &method(:print))
formatter_class = Formatter.find(opts[:formatter]) \
or error! "unknown formatter #{opts[:formatter]}"

@formatter = formatter_class.new(opts[:formatter_opts])
end

def run
@formatter.format(@lexer.lex(@input)) do |chunk|
print chunk
end
end

private
def self.parse_cgi(str)
pairs = CGI.parse(str).map { |k, v| v.first }
Hash[pairs]
end
end

desc 'style THEME', 'render THEME as css'
option :scope, :desc => "a css selector to scope the styles to"
def style(theme_name='thankful_eyes')
theme = Theme.find(theme_name)
raise "unknown theme: #{theme_name}" unless theme
class Style < CLI
def self.desc
"print CSS styles"
end

def self.doc
return enum_for(:doc) unless block_given?

yield %|usage: rougify style [<theme-name>] [<options>]|
yield %||
yield %|Print CSS styles for the given theme. Extra options are|
yield %|passed to the theme. Theme defaults to thankful_eyes.|
yield %||
yield %|options:|
yield %| --scope (default: .highlight) a css selector to scope by|
end

def self.parse(argv)
opts = { :theme_name => 'thankful_eyes' }

theme.new(options).render(&method(:puts))
until argv.empty?
arg = argv.shift
case arg
when /--(\w+)=(\S+)/
opts[$1.tr('-', '_').to_sym] = $2
when /--(\w+)/
opts[$1.tr('-', '_').to_sym] = argv.shift
else
opts[:theme_name] = arg
end
end

new(opts)
end

def initialize(opts)
theme_class = Theme.find(opts.delete(:theme_name)) \
or error! "unknown theme: #{theme_name}"

@theme = theme_class.new(opts)
end

def run
@theme.render(&method(:puts))
end
end

desc 'list', 'list the available lexers, formatters, and styles'
def list
puts "== Available Lexers =="
all_lexers = Lexer.all
max_len = all_lexers.map { |l| l.tag.size }.max
class List < CLI
def self.desc
"list available lexers"
end

def self.doc
return enum_for(:doc) unless block_given?

yield %|usage: rouge list|
yield %||
yield %|print a list of all available lexers with their descriptions.|
end

Lexer.all.each do |lexer|
desc = "#{lexer.desc}"
if lexer.aliases.any?
desc << " [aliases: #{lexer.aliases.join(',')}]"
def self.parse(argv)
new
end

def run
puts "== Available Lexers =="

Lexer.all.each do |lexer|
desc = "#{lexer.desc}"
if lexer.aliases.any?
desc << " [aliases: #{lexer.aliases.join(',')}]"
end
puts "%s: %s" % [lexer.tag, desc]
puts
end
puts "%s: %s" % [lexer.tag, desc]
puts
end
end

private
# TODO: does Thor do this for me?
def normalize_hash_keys(hash)
out = {}
hash.each do |k, v|
new_key = k.tr('-', '_').to_sym
out[new_key] = v
def self.normalize_syntax(argv)
out = []
argv.each do |arg|
case arg
when /^(--\w+)=(.*)$/
out << $1 << $2
when /^(-\w)(.+)$/
out << $1 << $2
else
out << arg
end
end

out
Expand Down
2 changes: 0 additions & 2 deletions rouge.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,4 @@ Gem::Specification.new do |s|
s.files = Dir['Gemfile', 'LICENSE', 'rouge.gemspec', 'lib/**/*.rb', 'bin/rougify', 'lib/rouge/demos/*']
s.executables = %w(rougify)
s.license = 'MIT (see LICENSE file)'

s.add_dependency 'thor'
end
Loading

0 comments on commit 20edfc6

Please sign in to comment.