module JsRegex::SecondPass

After conversion of a full Regexp::Expression tree, this checks for Node instances that need further processing.

Public Class Methods

call(tree) click to toggle source
# File lib/js_regex/second_pass.rb, line 8
def call(tree)
  substitute_root_level_keep_mark(tree)
  alternate_conditional_permutations(tree)
  tree
end

Private Class Methods

adapt_backref_to_permutation(backref_node, caps_per_branch, i) click to toggle source
# File lib/js_regex/second_pass.rb, line 121
def adapt_backref_to_permutation(backref_node, caps_per_branch, i)
  new_num = backref_node.reference + caps_per_branch * i
  backref_node.update(children: ["\\#{new_num}"])
end
adapt_conditional_to_permutation(conditional_node, truthy) click to toggle source
# File lib/js_regex/second_pass.rb, line 111
def adapt_conditional_to_permutation(conditional_node, truthy)
  branches = conditional_node.children[1...-1]
  if branches.count == 1
    truthy || null_quantify(branches.first)
  else
    null_quantify(truthy ? branches.last : branches.first)
  end
  conditional_node.update(type: :plain)
end
adapt_referenced_group_to_permutation(group_node, truthy) click to toggle source
# File lib/js_regex/second_pass.rb, line 107
def adapt_referenced_group_to_permutation(group_node, truthy)
  truthy ? min_quantify(group_node) : null_quantify(group_node)
end
alternate_conditional_permutations(tree) click to toggle source
# File lib/js_regex/second_pass.rb, line 29
def alternate_conditional_permutations(tree)
  permutations = conditional_tree_permutations(tree)
  return if permutations.empty?

  alternatives = permutations.map.with_index do |variant, i|
    Node.new((i.zero? ? '(?:' : '|(?:'), variant, ')')
  end
  tree.update(children: alternatives)
end
build_permutation(node, conds, truthy_conds, caps_per_branch, i) click to toggle source
# File lib/js_regex/second_pass.rb, line 80
def build_permutation(node, conds, truthy_conds, caps_per_branch, i)
  truthy = truthy_conds.include?(node.reference)

  case node.type
  when :backref
    # We cannot use named groups or backrefs in the conditional expansion,
    # their repetition would cause a "Duplicate capture group name" error in JS.
    node.update(children: [
      node.children.first.sub(/k<.*>/, node.reference.to_s)
    ])
    # backref numbers need to be incremented for subsequent "branches"
    adapt_backref_to_permutation(node, caps_per_branch, i)
  when :captured_group
    # Remove name, c.f. :backref handling.
    node.update(children: [
      node.children.first.sub(/\?<.*>/, ''),
      *node.children[1..-1]
    ])
    # if the group is referenced by any condition, modulate its quantity
    if conds.include?(node.reference)
      adapt_referenced_group_to_permutation(node, truthy)
    end
  when :conditional
    adapt_conditional_to_permutation(node, truthy)
  end
end
captured_group_count(tree) click to toggle source
# File lib/js_regex/second_pass.rb, line 68
def captured_group_count(tree)
  count = 0
  crawl(tree) { |node| count += 1 if node.type.equal?(:captured_group) }
  count
end
condition_permutations(conditions) click to toggle source
# File lib/js_regex/second_pass.rb, line 74
def condition_permutations(conditions)
  (0..(conditions.length)).inject([]) do |arr, n|
    arr + conditions.combination(n).to_a
  end
end
conditional_tree_permutations(tree) click to toggle source
# File lib/js_regex/second_pass.rb, line 39
def conditional_tree_permutations(tree)
  conds = conditions(tree)
  return [] if conds.empty?

  caps_per_branch = captured_group_count(tree)

  condition_permutations(conds).map.with_index do |truthy_conds, i|
    tree_permutation = tree.clone
    # find referenced groups and conditionals and make one-sided
    crawl(tree_permutation) do |node|
      build_permutation(node, conds, truthy_conds, caps_per_branch, i)
    end
  end
end
conditions(tree) click to toggle source
# File lib/js_regex/second_pass.rb, line 60
def conditions(tree)
  conditions = []
  crawl(tree) do |node|
    conditions << node.reference if node.type.equal?(:conditional)
  end
  conditions
end
crawl(node) { |node| ... } click to toggle source
# File lib/js_regex/second_pass.rb, line 54
def crawl(node, &block)
  return if node.instance_of?(String)
  yield(node)
  node.children.each { |child| crawl(child, &block) }
end
guarantees_at_least_one_match?(quantifier) click to toggle source
# File lib/js_regex/second_pass.rb, line 138
def guarantees_at_least_one_match?(quantifier)
  quantifier.nil? || quantifier.min > 0
end
min_quantify(node) click to toggle source
# File lib/js_regex/second_pass.rb, line 126
def min_quantify(node)
  return if guarantees_at_least_one_match?(qtf = node.quantifier)

  if qtf.max.equal?(1) # any zero_or_one quantifier (?, ??, ?+)
    node.update(quantifier: nil)
  else
    min_quantifier = qtf.dup
    min_quantifier.text = "{1,#{qtf.max}}#{'?' if qtf.reluctant?}"
    node.update(quantifier: min_quantifier)
  end
end
null_quantify(node) click to toggle source
# File lib/js_regex/second_pass.rb, line 142
def null_quantify(node)
  null_quantifier = Regexp::Expression::Quantifier.construct(text: '{0}')
  node.update(quantifier: null_quantifier)
end
substitute_root_level_keep_mark(tree) click to toggle source
# File lib/js_regex/second_pass.rb, line 16
def substitute_root_level_keep_mark(tree)
  keep_mark_index = nil
  tree.children.each.with_index do |child, i|
    break keep_mark_index = i if child.type == :keep_mark
  end
  return unless keep_mark_index

  pre = tree.children[0...keep_mark_index]
  post = tree.children[(keep_mark_index + 1)..-1]
  lookbehind = Node.new('(?<=', *pre, ')')
  tree.update(children: [lookbehind, *post])
end