107 lines
3.3 KiB
Ruby
Raw Normal View History

2024-12-31 16:50:50 +00:00
# frozen_string_literal: true
module ToRegex
module StringMixin
2024-12-31 16:50:50 +00:00
INLINE_OPTIONS = /[imxnesu]*/i.freeze
REGEXP_DELIMITERS = {
'%r{' => '}'.freeze,
'/' => '/'.freeze
}.freeze
REGEX_FLAGS = {
ignore_case: Regexp::IGNORECASE,
multiline: Regexp::MULTILINE,
extended: Regexp::EXTENDED
}.freeze
class << self
def literal?(str)
2024-12-31 16:50:50 +00:00
REGEXP_DELIMITERS.none? { |start, ending| str.start_with?(start) && str.match?(/#{ending}#{INLINE_OPTIONS}\z/) }
end
end
# Get a regex back
#
# Without :literal or :detect, `"foo".to_regex` will return nil.
#
# @param [optional, Hash] options
# @option options [true,false] :literal Treat meta characters and other regexp codes as just text; always return a regexp
# @option options [true,false] :detect If string starts and ends with valid regexp delimiters, treat it as a regexp; otherwise, interpret it literally
# @option options [true,false] :ignore_case /foo/i
# @option options [true,false] :multiline /foo/m
# @option options [true,false] :extended /foo/x
# @option options [true,false] :lang /foo/[nesu]
def to_regex(options = {})
2024-12-31 16:50:50 +00:00
args = as_regexp(options)
args ? Regexp.new(*args) : nil
end
# Return arguments that can be passed to `Regexp.new`
# @see to_regexp
def as_regexp(options = {})
2024-12-31 16:50:50 +00:00
raise ArgumentError, '[to_regexp] Options must be a Hash' unless options.is_a?(Hash)
str = self
2024-12-31 16:50:50 +00:00
return if options[:detect] && str.empty?
2024-12-31 16:50:50 +00:00
if should_treat_as_literal?(str, options)
content = Regexp.escape(str)
elsif (delim_set = extract_delimiters(str))
content, options = parse_regexp_string(str, delim_set, options)
return unless content
else
return
end
2024-12-31 16:50:50 +00:00
build_regexp_args(content, options)
end
2024-12-31 16:50:50 +00:00
private
def should_treat_as_literal?(str, options)
options[:literal] || (options[:detect] && ToRegex::StringMixin.literal?(str))
end
def extract_delimiters(str)
REGEXP_DELIMITERS.find { |start, _| str.start_with?(start) }
end
def parse_regexp_string(str, delim_set, options)
start_delim, end_delim = delim_set
match = /\A#{start_delim}(.*)#{end_delim}(#{INLINE_OPTIONS})\z/u.match(str)
return unless match
content = match[1].gsub('\\/', '/')
parse_inline_options(match[2], options)
[content, options]
end
def parse_inline_options(inline_options, options)
return unless inline_options
options[:ignore_case] = true if inline_options.include?('i')
options[:multiline] = true if inline_options.include?('m')
options[:extended] = true if inline_options.include?('x')
# 'n', 'N' = none, 'e', 'E' = EUC, 's', 'S' = SJIS, 'u', 'U' = UTF-8
options[:lang] = inline_options.scan(/[nesu]/i).join.downcase
end
def build_regexp_args(content, options)
flags = calculate_flags(options)
lang = normalize_lang_option(options[:lang])
lang.empty? ? [content, flags] : [content, flags, lang]
end
def calculate_flags(options)
REGEX_FLAGS.sum { |key, value| options[key] ? value : 0 }
end
def normalize_lang_option(lang)
return '' unless lang
RUBY_VERSION >= '1.9' ? lang.delete('u') : lang
end
end
end
class String
include ToRegex::StringMixin
2024-12-31 16:50:50 +00:00
end