diff options
Diffstat (limited to 'vendor/bundle/ruby/3.4.0/gems/rouge-4.7.0/lib/rouge/lexer.rb')
| -rw-r--r-- | vendor/bundle/ruby/3.4.0/gems/rouge-4.7.0/lib/rouge/lexer.rb | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/vendor/bundle/ruby/3.4.0/gems/rouge-4.7.0/lib/rouge/lexer.rb b/vendor/bundle/ruby/3.4.0/gems/rouge-4.7.0/lib/rouge/lexer.rb new file mode 100644 index 0000000..e191dfb --- /dev/null +++ b/vendor/bundle/ruby/3.4.0/gems/rouge-4.7.0/lib/rouge/lexer.rb @@ -0,0 +1,537 @@ +# -*- coding: utf-8 -*- # +# frozen_string_literal: true + +# stdlib +require 'strscan' +require 'set' +require 'uri' + +module Rouge + # @abstract + # A lexer transforms text into a stream of `[token, chunk]` pairs. + class Lexer + include Token::Tokens + + @option_docs = {} + + class << self + # Lexes `stream` with the given options. The lex is delegated to a + # new instance. + # + # @see #lex + def lex(stream, opts={}, &b) + new(opts).lex(stream, &b) + end + + # In case #continue_lex is called statically, we simply + # begin a new lex from the beginning, since there is no state. + # + # @see #continue_lex + def continue_lex(*a, &b) + lex(*a, &b) + end + + # Given a name in string, return the correct lexer class. + # @param [String] name + # @return [Class<Rouge::Lexer>,nil] + def find(name) + registry[name.to_s] + end + + # Same as ::find_fancy, except instead of returning an instantiated + # lexer, returns a pair of [lexer_class, options], so that you can + # modify or provide additional options to the lexer. + # + # Please note: the lexer class might be nil! + def lookup_fancy(str, code=nil, default_options={}) + if str && !str.include?('?') && str != 'guess' + lexer_class = find(str) + return [lexer_class, default_options] + end + + name, opts = str ? str.split('?', 2) : [nil, ''] + + # parse the options hash from a cgi-style string + cgi_opts = Hash.new { |hash, key| hash[key] = [] } + URI.decode_www_form(opts || '').each do |k, val| + cgi_opts[k] << val + end + cgi_opts.transform_values! do |vals| + case vals.size + when 0 then true + when 1 then vals[0] + else vals + end + end + + opts = default_options.merge(cgi_opts) + + lexer_class = case name + when 'guess', nil + self.guess(:source => code, :mimetype => opts['mimetype']) + when String + self.find(name) + end + + [lexer_class, opts] + end + + # Find a lexer, with fancy shiny features. + # + # * The string you pass can include CGI-style options + # + # Lexer.find_fancy('erb?parent=tex') + # + # * You can pass the special name 'guess' so we guess for you, + # and you can pass a second argument of the code to guess by + # + # Lexer.find_fancy('guess', "#!/bin/bash\necho Hello, world") + # + # If the code matches more than one lexer then Guesser::Ambiguous + # is raised. + # + # This is used in the Redcarpet plugin as well as Rouge's own + # markdown lexer for highlighting internal code blocks. + # + def find_fancy(str, code=nil, default_options={}) + lexer_class, opts = lookup_fancy(str, code, default_options) + + lexer_class && lexer_class.new(opts) + end + + # Specify or get this lexer's title. Meant to be human-readable. + def title(t=nil) + if t.nil? + t = tag.capitalize + end + @title ||= t + end + + # Specify or get this lexer's description. + def desc(arg=:absent) + if arg == :absent + @desc + else + @desc = arg + end + end + + def option_docs + @option_docs ||= InheritableHash.new(superclass.option_docs) + end + + def option(name, desc) + option_docs[name.to_s] = desc + end + + # Specify or get the path name containing a small demo for + # this lexer (can be overriden by {demo}). + def demo_file(arg=:absent) + return @demo_file = Pathname.new(arg) unless arg == :absent + + @demo_file = Pathname.new(File.join(__dir__, 'demos', tag)) + end + + # Specify or get a small demo string for this lexer + def demo(arg=:absent) + return @demo = arg unless arg == :absent + + @demo = File.read(demo_file, mode: 'rt:bom|utf-8') + end + + # @return a list of all lexers. + def all + @all ||= registry.values.uniq + end + + # Guess which lexer to use based on a hash of info. + # + # This accepts the same arguments as Lexer.guess, but will never throw + # an error. It will return a (possibly empty) list of potential lexers + # to use. + def guesses(info={}) + mimetype, filename, source = info.values_at(:mimetype, :filename, :source) + custom_globs = info[:custom_globs] + + guessers = (info[:guessers] || []).dup + + guessers << Guessers::Mimetype.new(mimetype) if mimetype + guessers << Guessers::GlobMapping.by_pairs(custom_globs, filename) if custom_globs && filename + guessers << Guessers::Filename.new(filename) if filename + guessers << Guessers::Modeline.new(source) if source + guessers << Guessers::Source.new(source) if source + guessers << Guessers::Disambiguation.new(filename, source) if source && filename + + Guesser.guess(guessers, Lexer.all) + end + + # Guess which lexer to use based on a hash of info. + # + # @option info :mimetype + # A mimetype to guess by + # @option info :filename + # A filename to guess by + # @option info :source + # The source itself, which, if guessing by mimetype or filename + # fails, will be searched for shebangs, <!DOCTYPE ...> tags, and + # other hints. + # @param [Proc] fallback called if multiple lexers are detected. + # If omitted, Guesser::Ambiguous is raised. + # + # @see Lexer.detect? + # @see Lexer.guesses + # @return [Class<Rouge::Lexer>] + def guess(info={}, &fallback) + lexers = guesses(info) + + return Lexers::PlainText if lexers.empty? + return lexers[0] if lexers.size == 1 + + if fallback + fallback.call(lexers) + else + raise Guesser::Ambiguous.new(lexers) + end + end + + def guess_by_mimetype(mt) + guess :mimetype => mt + end + + def guess_by_filename(fname) + guess :filename => fname + end + + def guess_by_source(source) + guess :source => source + end + + def enable_debug! + @debug_enabled = true + end + + def disable_debug! + remove_instance_variable :@debug_enabled if defined? @debug_enabled + end + + def debug_enabled? + (defined? @debug_enabled) ? true : false + end + + # Determine if a lexer has a method named +:detect?+ defined in its + # singleton class. + def detectable? + return @detectable if defined?(@detectable) + @detectable = singleton_methods(false).include?(:detect?) + end + + protected + # @private + def register(name, lexer) + # reset an existing list of lexers + @all = nil if defined?(@all) + registry[name.to_s] = lexer + end + + public + # Used to specify or get the canonical name of this lexer class. + # + # @example + # class MyLexer < Lexer + # tag 'foo' + # end + # + # MyLexer.tag # => 'foo' + # + # Lexer.find('foo') # => MyLexer + def tag(t=nil) + return @tag if t.nil? + + @tag = t.to_s + Lexer.register(@tag, self) + end + + # Used to specify alternate names this lexer class may be found by. + # + # @example + # class Erb < Lexer + # tag 'erb' + # aliases 'eruby', 'rhtml' + # end + # + # Lexer.find('eruby') # => Erb + def aliases(*args) + args.map!(&:to_s) + args.each { |arg| Lexer.register(arg, self) } + (@aliases ||= []).concat(args) + end + + # Specify a list of filename globs associated with this lexer. + # + # If a filename glob is associated with more than one lexer, this can + # cause a Guesser::Ambiguous error to be raised in various guessing + # methods. These errors can be avoided by disambiguation. Filename globs + # are disambiguated in one of two ways. Either the lexer will define a + # `self.detect?` method (intended for use with shebangs and doctypes) or a + # manual rule will be specified in Guessers::Disambiguation. + # + # @example + # class Ruby < Lexer + # filenames '*.rb', '*.ruby', 'Gemfile', 'Rakefile' + # end + def filenames(*fnames) + (@filenames ||= []).concat(fnames) + end + + # Specify a list of mimetypes associated with this lexer. + # + # @example + # class Html < Lexer + # mimetypes 'text/html', 'application/xhtml+xml' + # end + def mimetypes(*mts) + (@mimetypes ||= []).concat(mts) + end + + # @private + def assert_utf8!(str) + encoding = str.encoding + return if encoding == Encoding::US_ASCII || encoding == Encoding::UTF_8 || encoding == Encoding::BINARY + + raise EncodingError.new( + "Bad encoding: #{str.encoding.names.join(',')}. " + + "Please convert your string to UTF-8." + ) + end + + private + def registry + @registry ||= {} + end + end + + # -*- instance methods -*- # + + attr_reader :options + # Create a new lexer with the given options. Individual lexers may + # specify extra options. The only current globally accepted option + # is `:debug`. + # + # @option opts :debug + # Prints debug information to stdout. The particular info depends + # on the lexer in question. In regex lexers, this will log the + # state stack at the beginning of each step, along with each regex + # tried and each stream consumed. Try it, it's pretty useful. + def initialize(opts={}) + @options = {} + opts.each { |k, v| @options[k.to_s] = v } + + @debug = Lexer.debug_enabled? && bool_option('debug') + end + + # Returns a new lexer with the given options set. Useful for e.g. setting + # debug flags post hoc, or providing global overrides for certain options + def with(opts={}) + new_options = @options.dup + opts.each { |k, v| new_options[k.to_s] = v } + self.class.new(new_options) + end + + def as_bool(val) + case val + when nil, false, 0, '0', 'false', 'off' + false + when Array + val.empty? ? true : as_bool(val.last) + else + true + end + end + + def as_string(val) + return as_string(val.last) if val.is_a?(Array) + + val ? val.to_s : nil + end + + def as_list(val) + case val + when Array + val.flat_map { |v| as_list(v) } + when String + val.split(',') + else + [] + end + end + + def as_lexer(val) + return as_lexer(val.last) if val.is_a?(Array) + return val.new(@options) if val.is_a?(Class) && val < Lexer + + case val + when Lexer + val + when String + lexer_class = Lexer.find(val) + lexer_class && lexer_class.new(@options) + end + end + + def as_token(val) + return as_token(val.last) if val.is_a?(Array) + case val + when Token + val + else + Token[val] + end + end + + def bool_option(name, &default) + name_str = name.to_s + + if @options.key?(name_str) + as_bool(@options[name_str]) + else + default ? default.call : false + end + end + + def string_option(name, &default) + as_string(@options.delete(name.to_s, &default)) + end + + def lexer_option(name, &default) + as_lexer(@options.delete(name.to_s, &default)) + end + + def list_option(name, &default) + as_list(@options.delete(name.to_s, &default)) + end + + def token_option(name, &default) + as_token(@options.delete(name.to_s, &default)) + end + + def hash_option(name, defaults, &val_cast) + name = name.to_s + out = defaults.dup + + base = @options.delete(name.to_s) + base = {} unless base.is_a?(Hash) + base.each { |k, v| out[k.to_s] = val_cast ? val_cast.call(v) : v } + + @options.keys.each do |key| + next unless key =~ /(\w+)\[(\w+)\]/ and $1 == name + value = @options.delete(key) + + out[$2] = val_cast ? val_cast.call(value) : value + end + + out + end + + # @abstract + # + # Called after each lex is finished. The default implementation + # is a noop. + def reset! + end + + # Given a string, yield [token, chunk] pairs. If no block is given, + # an enumerator is returned. + # + # @option opts :continue + # Continue the lex from the previous state (i.e. don't call #reset!) + # + # @note The use of :continue => true has been deprecated. A warning is + # issued if run with `$VERBOSE` set to true. + # + # @note The use of arbitrary `opts` has never been supported, but we + # previously ignored them with no error. We now warn unconditionally. + def lex(string, opts=nil, &b) + if opts + if (opts.keys - [:continue]).size > 0 + # improper use of options hash + warn('Improper use of Lexer#lex - this method does not receive options.' + + ' This will become an error in a future version.') + end + + if opts[:continue] + warn '`lex :continue => true` is deprecated, please use #continue_lex instead' + return continue_lex(string, &b) + end + end + + return enum_for(:lex, string) unless block_given? + + Lexer.assert_utf8!(string) + reset! + + continue_lex(string, &b) + end + + # Continue the lex from the the current state without resetting + def continue_lex(string, &b) + return enum_for(:continue_lex, string, &b) unless block_given? + + # consolidate consecutive tokens of the same type + last_token = nil + last_val = nil + stream_tokens(string) do |tok, val| + next if val.empty? + + if tok == last_token + last_val << val + next + end + + b.call(last_token, last_val) if last_token + last_token = tok + last_val = val + end + + b.call(last_token, last_val) if last_token + end + + # delegated to {Lexer.tag} + def tag + self.class.tag + end + + # @abstract + # + # Yield `[token, chunk]` pairs, given a prepared input stream. This + # must be implemented. + # + # @param [StringScanner] stream + # the stream + def stream_tokens(stream, &b) + raise 'abstract' + end + + # @abstract + # + # Return true if there is an in-text indication (such as a shebang + # or DOCTYPE declaration) that this lexer should be used. + # + # @param [TextAnalyzer] text + # the text to be analyzed, with a couple of handy methods on it, + # like {TextAnalyzer#shebang?} and {TextAnalyzer#doctype?} + def self.detect?(text) + false + end + end + + module Lexers + BASE_DIR = "#{__dir__}/lexers".freeze + @_loaded_lexers = {} + + def self.load_lexer(relpath) + return if @_loaded_lexers.key?(relpath) + @_loaded_lexers[relpath] = true + Kernel::load File.join(BASE_DIR, relpath) + end + end +end |
