Module: Flexr::Unicode::ReferenceRegexp

Defined in:
lib/flexr/unicode/reference_regexp.rb

Constant Summary collapse

CACHE =

rubocop:disable Style/MutableConstant

{}

Class Method Summary collapse

Class Method Details

.byte_class(ranges) ⇒ Object



60
61
62
# File 'lib/flexr/unicode/reference_regexp.rb', line 60

def byte_class(ranges)
  "[#{ranges.map { |lo, hi| byte_escape(lo, hi) }.join}]"
end

.byte_escape(lo, hi) ⇒ Object



64
65
66
# File 'lib/flexr/unicode/reference_regexp.rb', line 64

def byte_escape(lo, hi)
  lo == hi ? format("\\x%<byte>02X", byte: lo) : format("\\x%<lo>02X-\\x%<hi>02X", lo: lo, hi: hi)
end

.casefold_ranges(ranges) ⇒ Object



101
102
103
# File 'lib/flexr/unicode/reference_regexp.rb', line 101

def casefold_ranges(ranges)
  CaseFold.merge(ranges.flat_map { |lo, hi| CaseFold.ranges(lo, hi) })
end

.codepoint_class(ranges) ⇒ Object



68
69
70
71
72
73
# File 'lib/flexr/unicode/reference_regexp.rb', line 68

def codepoint_class(ranges)
  ranges = scalar_ranges(ranges)
  return "(?!)" if ranges.empty?

  "[#{ranges.sort_by(&:first).map { |lo, hi| codepoint_escape(lo, hi) }.join}]"
end

.codepoint_escape(lo, hi) ⇒ Object



84
85
86
87
88
# File 'lib/flexr/unicode/reference_regexp.rb', line 84

def codepoint_escape(lo, hi)
  first = ::Regexp.escape([lo].pack("U"))
  last = ::Regexp.escape([hi].pack("U"))
  lo == hi ? first : "#{first}-#{last}"
end

.compiled(pattern, encoding:, options: nil, unicode: false) ⇒ Object



19
20
21
22
23
24
25
26
27
28
# File 'lib/flexr/unicode/reference_regexp.rb', line 19

def compiled(pattern, encoding:, options: nil, unicode: false)
  effective_options = options.nil? ? pattern.options : options
  key = [pattern.source, effective_options, encoding, unicode]
  return CACHE[key] if CACHE.key?(key)

  parser = Regexp::Parser.new(pattern.source, options: effective_options, encoding: encoding, unicode: unicode)
  source = source_for(parser.parse, ignorecase: effective_options.anybits?(::Regexp::IGNORECASE))
  regexp_options = effective_options & ~::Regexp::IGNORECASE
  CACHE[key] = ::Regexp.new(source, regexp_options).freeze
end

.complement(ranges) ⇒ Object



90
91
92
93
94
95
96
97
98
99
# File 'lib/flexr/unicode/reference_regexp.rb', line 90

def complement(ranges)
  result = []
  cursor = 0
  ranges.sort_by(&:first).each do |lo, hi|
    result << [cursor, lo - 1] if cursor < lo
    cursor = [cursor, hi + 1].max
  end
  result << [cursor, 0x10ffff] if cursor <= 0x10ffff
  result
end

.match(pattern, subject, encoding:, options: 0, unicode: false) ⇒ Object



11
12
13
14
15
16
17
# File 'lib/flexr/unicode/reference_regexp.rb', line 11

def match(pattern, subject, encoding:, options: 0, unicode: false)
  regexp = compiled(pattern, encoding: encoding, options: options, unicode: unicode)
  subject = subject.dup.force_encoding(regexp.encoding)
  regexp.match(subject, 0)
rescue RegexpError, ArgumentError, EncodingError
  nil
end

.scalar_ranges(ranges) ⇒ Object



75
76
77
78
79
80
81
82
# File 'lib/flexr/unicode/reference_regexp.rb', line 75

def scalar_ranges(ranges)
  ranges.flat_map do |lo, hi|
    result = []
    result << [lo, [hi, 0xd7ff].min] if lo <= 0xd7ff
    result << [[lo, 0xe000].max, hi] if hi >= 0xe000
    result
  end
end

.source_for(node, ignorecase: false) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/flexr/unicode/reference_regexp.rb', line 30

def source_for(node, ignorecase: false)
  case node
  when Regexp::AST::Empty, Regexp::AST::Anchor then ""
  when Regexp::AST::Fail then "(?!)"
  when Regexp::AST::ByteRange then byte_class([[node.lo, node.hi]])
  when Regexp::AST::CodepointRange then codepoint_class([[node.lo, node.hi]])
  when Regexp::AST::CharClass
    ranges = node.ranges.flat_map do |range|
      if range.first == Regexp::AST::Property
        property_ranges = Unicode::Property.ranges(range[2])
        property_ranges = casefold_ranges(property_ranges) if ignorecase
        range[1] ? complement(property_ranges) : property_ranges
      else
        [range]
      end
    end
    ranges = complement(ranges) if node.negated
    codepoint_class(ranges)
  when Regexp::AST::Seq then node.children.map { |child| source_for(child, ignorecase: ignorecase) }.join
  when Regexp::AST::Alt
    "(?:#{node.children.map { |child| source_for(child, ignorecase: ignorecase) }.join('|')})"
  when Regexp::AST::Star then "(?:#{source_for(node.child, ignorecase: ignorecase)})*"
  when Regexp::AST::Repeat
    maximum = node.maximum.nil? ? "" : node.maximum
    "(?:#{source_for(node.child, ignorecase: ignorecase)}){#{node.minimum},#{maximum}}"
  else
    raise CompileError, "unsupported reference AST node: #{node.class}"
  end
end