This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rouge
|
||||
module Guessers
|
||||
class Disambiguation < Guesser
|
||||
include Util
|
||||
include Lexers
|
||||
|
||||
def initialize(filename, source)
|
||||
@filename = File.basename(filename)
|
||||
@source = source
|
||||
end
|
||||
|
||||
def filter(lexers)
|
||||
return lexers if lexers.size == 1
|
||||
return lexers if lexers.size == Lexer.all.size
|
||||
|
||||
@analyzer = TextAnalyzer.new(get_source(@source))
|
||||
|
||||
self.class.disambiguators.each do |disambiguator|
|
||||
next unless disambiguator.match?(@filename)
|
||||
|
||||
filtered = disambiguator.decide!(self)
|
||||
return filtered if filtered
|
||||
end
|
||||
|
||||
return lexers
|
||||
end
|
||||
|
||||
def contains?(text)
|
||||
return @analyzer.include?(text)
|
||||
end
|
||||
|
||||
def matches?(re)
|
||||
return !!(@analyzer =~ re)
|
||||
end
|
||||
|
||||
@disambiguators = []
|
||||
def self.disambiguate(*patterns, &decider)
|
||||
@disambiguators << Disambiguator.new(patterns, &decider)
|
||||
end
|
||||
|
||||
def self.disambiguators
|
||||
@disambiguators
|
||||
end
|
||||
|
||||
class Disambiguator
|
||||
include Util
|
||||
|
||||
def initialize(patterns, &decider)
|
||||
@patterns = patterns
|
||||
@decider = decider
|
||||
end
|
||||
|
||||
def decide!(guesser)
|
||||
out = guesser.instance_eval(&@decider)
|
||||
case out
|
||||
when Array then out
|
||||
when nil then nil
|
||||
else [out]
|
||||
end
|
||||
end
|
||||
|
||||
def match?(filename)
|
||||
@patterns.any? { |p| test_glob(p, filename) }
|
||||
end
|
||||
end
|
||||
|
||||
disambiguate '*.cfg' do
|
||||
next CiscoIos if matches?(/\A\s*(version|banner|interface)\b/)
|
||||
|
||||
INI
|
||||
end
|
||||
|
||||
disambiguate '*.pl' do
|
||||
next Perl if contains?('my $')
|
||||
next Prolog if contains?(':-')
|
||||
next Prolog if matches?(/\A\w+(\(\w+\,\s*\w+\))*\./)
|
||||
end
|
||||
|
||||
disambiguate '*.h' do
|
||||
next ObjectiveC if matches?(/@(end|implementation|protocol|property)\b/)
|
||||
next ObjectiveC if contains?('@"')
|
||||
next Cpp if matches?(/^\s*(?:catch|class|constexpr|namespace|private|
|
||||
protected|public|template|throw|try|using)\b/x)
|
||||
|
||||
C
|
||||
end
|
||||
|
||||
disambiguate '*.m' do
|
||||
next ObjectiveC if matches?(/@(end|implementation|protocol|property)\b/)
|
||||
next ObjectiveC if contains?('@"')
|
||||
|
||||
# Objective-C dereferenced pointers and Mathematica comments are similar.
|
||||
# Disambiguate for Mathematica by looking for any amount of whitespace (or no whitespace)
|
||||
# followed by "(*" (e.g. `(* comment *)`).
|
||||
next Mathematica if matches?(/^\s*\(\*/)
|
||||
|
||||
# Disambiguate for objc by looking for a deref'd pointer in a statement (e.g. `if (*foo == 0)`).
|
||||
# This pattern is less specific than the Mathematica pattern, so its positioned after it.
|
||||
next ObjectiveC if matches?(/^\s*(if|while|for|switch|do)\s*\([^)]*\*[^)]*\)/)
|
||||
|
||||
next Mathematica if contains?(':=')
|
||||
|
||||
next Mason if matches?(/<%(def|method|text|doc|args|flags|attr|init|once|shared|perl|cleanup|filter)([^>]*)(>)/)
|
||||
|
||||
next Matlab if matches?(/^\s*?%/)
|
||||
# Matlab cell array creation: data = {
|
||||
next Matlab if matches?(/^\s*[a-zA-Z]\w*\s*=\s*\{/)
|
||||
|
||||
next Mason if matches? %r!(</?%|<&)!
|
||||
end
|
||||
|
||||
disambiguate '*.php' do
|
||||
# PHP always takes precedence over Hack
|
||||
PHP
|
||||
end
|
||||
|
||||
disambiguate '*.hh' do
|
||||
next Cpp if matches?(/^\s*#include/)
|
||||
next Hack if matches?(/^<\?hh/)
|
||||
next Hack if matches?(/(\(|, ?)\$\$/)
|
||||
|
||||
Cpp
|
||||
end
|
||||
|
||||
disambiguate '*.plist' do
|
||||
next XML if matches?(/\A<\?xml\b/)
|
||||
|
||||
Plist
|
||||
end
|
||||
|
||||
disambiguate '*.sc' do
|
||||
next Python if matches?(/^#/)
|
||||
next SuperCollider if matches?(/(?:^~|;$)/)
|
||||
|
||||
next Python
|
||||
end
|
||||
|
||||
disambiguate 'Messages' do
|
||||
next MsgTrans if matches?(/^[^\s:]+:[^\s:]+/)
|
||||
|
||||
next PlainText
|
||||
end
|
||||
|
||||
disambiguate '*.cls' do
|
||||
next TeX if matches?(/\A\s*(?:\\|%)/)
|
||||
next OpenEdge if matches?(/(no\-undo|BLOCK\-LEVEL|ROUTINE\-LEVEL|&ANALYZE\-SUSPEND)/i)
|
||||
next Apex
|
||||
end
|
||||
|
||||
disambiguate '*.pp' do
|
||||
next Puppet if matches?(/(::)?([a-z]\w*::)/)
|
||||
next Pascal if matches?(/^(function|begin|var)\b/)
|
||||
next Pascal if matches?(/\b(end(;|\.))/)
|
||||
|
||||
Puppet
|
||||
end
|
||||
|
||||
disambiguate '*.p' do
|
||||
next Prolog if contains?(':-')
|
||||
next Prolog if matches?(/\A\w+(\(\w+\,\s*\w+\))*\./)
|
||||
next OpenEdge
|
||||
end
|
||||
|
||||
disambiguate '*.st' do
|
||||
next IecST if matches?(/^\s*END_/i)
|
||||
next Smalltalk
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rouge
|
||||
module Guessers
|
||||
class Filename < Guesser
|
||||
attr_reader :fname
|
||||
def initialize(filename)
|
||||
@filename = filename
|
||||
end
|
||||
|
||||
# returns a list of lexers that match the given filename with
|
||||
# equal specificity (i.e. number of wildcards in the pattern).
|
||||
# This helps disambiguate between, e.g. the Nginx lexer, which
|
||||
# matches `nginx.conf`, and the Conf lexer, which matches `*.conf`.
|
||||
# In this case, nginx will win because the pattern has no wildcards,
|
||||
# while `*.conf` has one.
|
||||
def filter(lexers)
|
||||
mapping = {}
|
||||
lexers.each do |lexer|
|
||||
mapping[lexer.name] = lexer.filenames || []
|
||||
end
|
||||
|
||||
GlobMapping.new(mapping, @filename).filter(lexers)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,45 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rouge
|
||||
module Guessers
|
||||
# This class allows for custom behavior
|
||||
# with glob -> lexer name mappings
|
||||
class GlobMapping < Guesser
|
||||
include Util
|
||||
|
||||
def self.by_pairs(mapping, filename)
|
||||
glob_map = {}
|
||||
mapping.each do |(glob, lexer_name)|
|
||||
lexer = Lexer.find(lexer_name)
|
||||
|
||||
# ignore unknown lexers
|
||||
next unless lexer
|
||||
|
||||
glob_map[lexer.name] ||= []
|
||||
glob_map[lexer.name] << glob
|
||||
end
|
||||
|
||||
new(glob_map, filename)
|
||||
end
|
||||
|
||||
attr_reader :glob_map, :filename
|
||||
def initialize(glob_map, filename)
|
||||
@glob_map = glob_map
|
||||
@filename = filename
|
||||
end
|
||||
|
||||
def filter(lexers)
|
||||
basename = File.basename(filename)
|
||||
|
||||
collect_best(lexers) do |lexer|
|
||||
(@glob_map[lexer.name] || []).map do |pattern|
|
||||
if test_glob(pattern, basename)
|
||||
# specificity is better the fewer wildcards there are
|
||||
-pattern.scan(/[*?\[]/).size
|
||||
end
|
||||
end.compact.min
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rouge
|
||||
module Guessers
|
||||
class Mimetype < Guesser
|
||||
attr_reader :mimetype
|
||||
def initialize(mimetype)
|
||||
@mimetype = mimetype
|
||||
end
|
||||
|
||||
def filter(lexers)
|
||||
lexers.select { |lexer| lexer.mimetypes.include? @mimetype }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rouge
|
||||
module Guessers
|
||||
class Modeline < Guesser
|
||||
include Util
|
||||
|
||||
# [jneen] regexen stolen from linguist
|
||||
EMACS_MODELINE = /-\*-\s*(?:(?!mode)[\w-]+\s*:\s*(?:[\w+-]+)\s*;?\s*)*(?:mode\s*:)?\s*([\w+-]+)\s*(?:;\s*(?!mode)[\w-]+\s*:\s*[\w+-]+\s*)*;?\s*-\*-/i
|
||||
|
||||
# First form vim modeline
|
||||
# [text]{white}{vi:|vim:|ex:}[white]{options}
|
||||
# ex: 'vim: syntax=ruby'
|
||||
VIM_MODELINE_1 = /(?:vim|vi|ex):\s*(?:ft|filetype|syntax)=(\w+)\s?/i
|
||||
|
||||
# Second form vim modeline (compatible with some versions of Vi)
|
||||
# [text]{white}{vi:|vim:|Vim:|ex:}[white]se[t] {options}:[text]
|
||||
# ex: 'vim set syntax=ruby:'
|
||||
VIM_MODELINE_2 = /(?:vim|vi|Vim|ex):\s*se(?:t)?.*\s(?:ft|filetype|syntax)=(\w+)\s?.*:/i
|
||||
|
||||
MODELINES = [EMACS_MODELINE, VIM_MODELINE_1, VIM_MODELINE_2]
|
||||
|
||||
def initialize(source, opts={})
|
||||
@source = source
|
||||
@lines = opts[:lines] || 5
|
||||
end
|
||||
|
||||
def filter(lexers)
|
||||
# don't bother reading the stream if we've already decided
|
||||
return lexers if lexers.size == 1
|
||||
|
||||
source_text = get_source(@source)
|
||||
|
||||
lines = source_text.split(/\n/)
|
||||
|
||||
search_space = (lines.first(@lines) + lines.last(@lines)).join("\n")
|
||||
|
||||
matches = MODELINES.map { |re| re.match(search_space) }.compact
|
||||
return lexers unless matches.any?
|
||||
|
||||
match_set = Set.new(matches.map { |m| m[1] })
|
||||
lexers.select { |l| match_set.include?(l.tag) || l.aliases.any? { |a| match_set.include?(a) } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,31 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rouge
|
||||
module Guessers
|
||||
class Source < Guesser
|
||||
include Util
|
||||
|
||||
attr_reader :source
|
||||
def initialize(source)
|
||||
@source = source
|
||||
end
|
||||
|
||||
def filter(lexers)
|
||||
# don't bother reading the input if
|
||||
# we've already filtered to 1
|
||||
return lexers if lexers.size == 1
|
||||
|
||||
source_text = get_source(@source)
|
||||
|
||||
Lexer.assert_utf8!(source_text)
|
||||
|
||||
source_text = TextAnalyzer.new(source_text)
|
||||
|
||||
collect_best(lexers) do |lexer|
|
||||
next unless lexer.detectable?
|
||||
lexer.detect?(source_text) ? 1 : nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rouge
|
||||
module Guessers
|
||||
module Util
|
||||
module SourceNormalizer
|
||||
UTF8_BOM = "\xEF\xBB\xBF"
|
||||
UTF8_BOM_RE = /\A#{UTF8_BOM}/
|
||||
|
||||
# @param [String,nil] source
|
||||
# @return [String,nil]
|
||||
def self.normalize(source)
|
||||
source.sub(UTF8_BOM_RE, '').gsub(/\r\n/, "\n")
|
||||
end
|
||||
end
|
||||
|
||||
def test_glob(pattern, path)
|
||||
File.fnmatch?(pattern, path, File::FNM_DOTMATCH | File::FNM_CASEFOLD)
|
||||
end
|
||||
|
||||
# @param [String,IO] source
|
||||
# @return [String]
|
||||
def get_source(source)
|
||||
if source.respond_to?(:to_str)
|
||||
SourceNormalizer.normalize(source.to_str)
|
||||
elsif source.respond_to?(:read)
|
||||
SourceNormalizer.normalize(source.read)
|
||||
else
|
||||
raise ArgumentError, "Invalid source: #{source.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user