482 lines
12 KiB
Ruby
482 lines
12 KiB
Ruby
# -*- coding: utf-8 -*- #
|
|
# frozen_string_literal: true
|
|
|
|
# not required by the main lib.
|
|
# to use this module, require 'rouge/cli'.
|
|
|
|
module Rouge
|
|
class FileReader
|
|
attr_reader :input
|
|
def initialize(input)
|
|
@input = input
|
|
end
|
|
|
|
def file
|
|
case input
|
|
when '-'
|
|
IO.new($stdin.fileno, 'rt:bom|utf-8')
|
|
when String
|
|
File.new(input, 'rt:bom|utf-8')
|
|
when ->(i){ i.respond_to? :read }
|
|
input
|
|
end
|
|
end
|
|
|
|
def read
|
|
@read ||= begin
|
|
file.read
|
|
rescue => e
|
|
$stderr.puts "unable to open #{input}: #{e.message}"
|
|
exit 1
|
|
ensure
|
|
file.close
|
|
end
|
|
end
|
|
end
|
|
|
|
class CLI
|
|
def self.doc
|
|
return enum_for(:doc) unless block_given?
|
|
|
|
yield %|usage: rougify {global options} [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 %| guess #{Guess.desc}|
|
|
yield %| version #{Version.desc}|
|
|
yield %||
|
|
yield %|global options:|
|
|
yield %[ --require|-r <fname> require <fname> after loading rouge]
|
|
yield %||
|
|
yield %|See `rougify help <command>` for more info.|
|
|
end
|
|
|
|
class Error < StandardError
|
|
attr_reader :message, :status
|
|
def initialize(message, status=1)
|
|
@message = message
|
|
@status = status
|
|
end
|
|
end
|
|
|
|
def self.parse(argv=ARGV)
|
|
argv = normalize_syntax(argv)
|
|
|
|
while (head = argv.shift)
|
|
case head
|
|
when '-h', '--help', 'help', '-help'
|
|
return Help.parse(argv)
|
|
when '--require', '-r'
|
|
require argv.shift
|
|
else
|
|
break
|
|
end
|
|
end
|
|
|
|
klass = class_from_arg(head)
|
|
return klass.parse(argv) if klass
|
|
|
|
argv.unshift(head) if head
|
|
Highlight.parse(argv)
|
|
end
|
|
|
|
def initialize(options={})
|
|
end
|
|
|
|
def self.error!(msg, status=1)
|
|
raise Error.new(msg, status)
|
|
end
|
|
|
|
def error!(*a)
|
|
self.class.error!(*a)
|
|
end
|
|
|
|
def self.class_from_arg(arg)
|
|
case arg
|
|
when 'version', '--version', '-v'
|
|
Version
|
|
when 'help', nil
|
|
Help
|
|
when 'highlight', 'hi'
|
|
Highlight
|
|
when 'style'
|
|
Style
|
|
when 'list'
|
|
List
|
|
when 'guess'
|
|
Guess
|
|
end
|
|
end
|
|
|
|
class Version < CLI
|
|
def self.desc
|
|
"print the rouge version number"
|
|
end
|
|
|
|
def self.parse(*); new; end
|
|
|
|
def run
|
|
puts Rouge.version
|
|
end
|
|
end
|
|
|
|
class Help < CLI
|
|
def self.desc
|
|
"print help info"
|
|
end
|
|
|
|
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
|
|
|
|
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 %[--formatter|-f <opts> specify the output formatter to use.]
|
|
yield %[ If not provided, rougify will default to]
|
|
yield %[ terminal256.]
|
|
yield %[]
|
|
yield %[--theme|-t <theme> specify the theme to use for highlighting]
|
|
yield %[ the file. (only applies to some formatters)]
|
|
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)]
|
|
yield %[]
|
|
yield %[--require|-r <filename> require a filename or library before]
|
|
yield %[ highlighting]
|
|
yield %[]
|
|
yield %[--escape allow the use of escapes between <! and !>]
|
|
yield %[]
|
|
yield %[--escape-with <l> <r> allow the use of escapes between custom]
|
|
yield %[ delimiters. implies --escape]
|
|
end
|
|
|
|
def self.parse(argv)
|
|
opts = {
|
|
:formatter => 'terminal256',
|
|
:theme => 'thankful_eyes',
|
|
:css_class => 'codehilite',
|
|
:input_file => '-',
|
|
:lexer_opts => {},
|
|
:formatter_opts => {},
|
|
:requires => [],
|
|
}
|
|
|
|
until argv.empty?
|
|
arg = argv.shift
|
|
case arg
|
|
when '-r', '--require'
|
|
opts[:requires] << argv.shift
|
|
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-preset', '-f'
|
|
opts[:formatter] = argv.shift
|
|
when '--theme', '-t'
|
|
opts[:theme] = argv.shift
|
|
when '--css-class', '-c'
|
|
opts[:css_class] = argv.shift
|
|
when '--lexer-opts', '-L'
|
|
opts[:lexer_opts] = parse_cgi(argv.shift)
|
|
when '--escape'
|
|
opts[:escape] = ['<!', '!>']
|
|
when '--escape-with'
|
|
opts[:escape] = [argv.shift, argv.shift]
|
|
when /^--/
|
|
error! "unknown option #{arg.inspect}"
|
|
else
|
|
opts[:input_file] = arg
|
|
end
|
|
end
|
|
|
|
new(opts)
|
|
end
|
|
|
|
def input_stream
|
|
@input_stream ||= FileReader.new(@input_file)
|
|
end
|
|
|
|
def input
|
|
@input ||= input_stream.read
|
|
end
|
|
|
|
def lexer_class
|
|
@lexer_class ||= Lexer.guess(
|
|
:filename => @input_file,
|
|
:mimetype => @mimetype,
|
|
:source => input_stream,
|
|
)
|
|
end
|
|
|
|
def raw_lexer
|
|
lexer_class.new(@lexer_opts)
|
|
end
|
|
|
|
def escape_lexer
|
|
Rouge::Lexers::Escape.new(
|
|
start: @escape[0],
|
|
end: @escape[1],
|
|
lang: raw_lexer,
|
|
)
|
|
end
|
|
|
|
def lexer
|
|
@lexer ||= @escape ? escape_lexer : raw_lexer
|
|
end
|
|
|
|
attr_reader :input_file, :lexer_name, :mimetype, :formatter, :escape
|
|
|
|
def initialize(opts={})
|
|
Rouge::Lexer.enable_debug!
|
|
|
|
opts[:requires].each do |r|
|
|
require r
|
|
end
|
|
|
|
@input_file = opts[:input_file]
|
|
|
|
if opts[:lexer]
|
|
@lexer_class = Lexer.find(opts[:lexer]) \
|
|
or error! "unknown lexer #{opts[:lexer].inspect}"
|
|
else
|
|
@lexer_name = opts[:lexer]
|
|
@mimetype = opts[:mimetype]
|
|
end
|
|
|
|
@lexer_opts = opts[:lexer_opts]
|
|
|
|
theme = Theme.find(opts[:theme]).new or error! "unknown theme #{opts[:theme]}"
|
|
|
|
@formatter = case opts[:formatter]
|
|
when 'terminal256' then Formatters::Terminal256.new(theme)
|
|
when 'html' then Formatters::HTML.new
|
|
when 'html-pygments' then Formatters::HTMLPygments.new(Formatters::HTML.new, opts[:css_class])
|
|
when 'html-inline' then Formatters::HTMLInline.new(theme)
|
|
when 'html-line-table' then Formatters::HTMLLineTable.new(Formatters::HTML.new)
|
|
when 'html-table' then Formatters::HTMLTable.new(Formatters::HTML.new)
|
|
when 'null', 'raw', 'tokens' then Formatters::Null.new
|
|
when 'tex' then Formatters::Tex.new
|
|
else
|
|
error! "unknown formatter preset #{opts[:formatter]}"
|
|
end
|
|
|
|
@escape = opts[:escape]
|
|
end
|
|
|
|
def run
|
|
Formatter.enable_escape! if @escape
|
|
formatter.format(lexer.lex(input), &method(:print))
|
|
end
|
|
|
|
private_class_method
|
|
def self.parse_cgi(str)
|
|
pairs = CGI.parse(str).map { |k, v| [k.to_sym, v.first] }
|
|
Hash[pairs]
|
|
end
|
|
end
|
|
|
|
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. To select a mode (light/dark) for the|
|
|
yield %|theme, append '.light' or '.dark' to the <theme-name>|
|
|
yield %|respectively. Theme defaults to thankful_eyes.|
|
|
yield %||
|
|
yield %|options:|
|
|
yield %| --scope (default: .highlight) a css selector to scope by|
|
|
yield %| --tex (default: false) render as TeX|
|
|
yield %| --tex-prefix (default: RG) a command prefix for TeX|
|
|
yield %| implies --tex if specified|
|
|
yield %||
|
|
yield %|available themes:|
|
|
yield %| #{Theme.registry.keys.sort.join(', ')}|
|
|
end
|
|
|
|
def self.parse(argv)
|
|
opts = {
|
|
:theme_name => 'thankful_eyes',
|
|
:tex => false,
|
|
:tex_prefix => 'RG'
|
|
}
|
|
|
|
until argv.empty?
|
|
arg = argv.shift
|
|
case arg
|
|
when '--tex'
|
|
opts[:tex] = true
|
|
when '--tex-prefix'
|
|
opts[:tex] = true
|
|
opts[:tex_prefix] = argv.shift
|
|
when /--(\w+)/
|
|
opts[$1.tr('-', '_').to_sym] = argv.shift
|
|
else
|
|
opts[:theme_name] = arg
|
|
end
|
|
end
|
|
|
|
new(opts)
|
|
end
|
|
|
|
def initialize(opts)
|
|
theme_name = opts.delete(:theme_name)
|
|
theme_class = Theme.find(theme_name) \
|
|
or error! "unknown theme: #{theme_name}"
|
|
|
|
@theme = theme_class.new(opts)
|
|
if opts[:tex]
|
|
tex_prefix = opts[:tex_prefix]
|
|
@theme = TexThemeRenderer.new(@theme, prefix: tex_prefix)
|
|
end
|
|
end
|
|
|
|
def run
|
|
@theme.render(&method(:puts))
|
|
end
|
|
end
|
|
|
|
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
|
|
|
|
def self.parse(argv)
|
|
new
|
|
end
|
|
|
|
def run
|
|
puts "== Available Lexers =="
|
|
|
|
Lexer.all.sort_by(&:tag).each do |lexer|
|
|
desc = String.new("#{lexer.desc}")
|
|
if lexer.aliases.any?
|
|
desc << " [aliases: #{lexer.aliases.join(',')}]"
|
|
end
|
|
puts "%s: %s" % [lexer.tag, desc]
|
|
|
|
lexer.option_docs.keys.sort.each do |option|
|
|
puts " ?#{option}= #{lexer.option_docs[option]}"
|
|
end
|
|
|
|
puts
|
|
end
|
|
end
|
|
end
|
|
|
|
class Guess < CLI
|
|
def self.desc
|
|
"guess the languages of file"
|
|
end
|
|
|
|
def self.parse(args)
|
|
new(input_file: args.shift)
|
|
end
|
|
|
|
attr_reader :input_file, :input_source
|
|
|
|
def initialize(opts)
|
|
@input_file = opts[:input_file] || '-'
|
|
@input_source = FileReader.new(@input_file).read
|
|
end
|
|
|
|
def lexers
|
|
Lexer.guesses(
|
|
filename: input_file,
|
|
source: input_source,
|
|
)
|
|
end
|
|
|
|
def run
|
|
lexers.each do |l|
|
|
puts "{ tag: #{l.tag.inspect}, title: #{l.title.inspect}, desc: #{l.desc.inspect} }"
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
private_class_method
|
|
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
|
|
end
|
|
end
|
|
end
|