decko-commons/decko

View on GitHub
cardname/lib/cardname/contextual.rb

Summary

Maintainability
A
55 mins
Test Coverage
A
100%
class Cardname
  # contextual (or relative) names are names that vary by context
  module Contextual
    RELATIVE_REGEXP = /\b_(left|right|whole|self|user|main|\d+|L*R?)\b/

    # true if name is left or right of context
    # @return [Boolean]
    def child_of? context
      return false unless compound?

      context_key = context.to_name.key
      absolute_name(context).parent_keys.include? context_key
    end

    # @return [Boolean]
    def relative?
      starts_with_joint? || (s =~ RELATIVE_REGEXP).present?
    end

    # starts with joint, no other contextual element
    # @return [Boolean]
    def simple_relative?
      starts_with_joint? && (s =~ RELATIVE_REGEXP).nil?
    end

    # not relative
    # @return [Boolean]
    def absolute?
      !relative?
    end

    # @return [String]
    def from *from
      name_from(*from).s
    end

    # if possible, relativize name into one beginning with a "+".
    # The new name must absolutize back to the correct
    # original name in the context of "from"
    # @return [Cardname]
    def name_from *from
      return self unless (remaining = remove_context(*from))

      compressed = remaining.compact.unshift(nil).to_name  # exactly one nil at beginning
      key == compressed.absolute_name(from).key ? compressed : self
    end

    # interpret contextual name
    # @return [String]
    def absolute context
      context = (context || "").to_name
      new_parts = absolutize_contextual_parts context
      return "" if new_parts.empty?

      absolutize_extremes new_parts, context.s
      new_parts.join self.class.joint
    end

    # @return [Cardname]
    def absolute_name context
      absolute(context).to_name
    end

    # 1 = left; 2= left of left; 3 = left of left of left....
    # @return [Cardname]
    def nth_left_name n
      (n >= length ? parts[0] : parts[0..-n - 1]).to_name
    end

    private

    def parts_excluding *string
      exclude_name = string.to_name
      exclude_keys = exclude_name ? exclude_name.part_names.map(&:key) : []
      parts_minus exclude_keys
    end

    def remove_context *from
      return false unless from.compact.present?

      remaining = parts_excluding(*from)
      return false if remaining.compact.empty? || # all name parts in context
                      remaining == parts          # no name parts in context

      remaining
    end

    # @return [Array <String>]
    def parts_minus keys_to_ignore
      parts.map do |part|
        next if part.empty?
        next if part =~ /^_/ # this removes relative parts.  why?
        next if keys_to_ignore.member? part.to_name.key

        part
      end
    end

    def absolutize_contextual_parts context
      parts.map do |part|
        case part
        when /^_user$/i            then user_part part
        when /^_main$/i            then self.class.params[:main_name]
        when /^(_self|_whole|_)$/i then context.s
        when /^_left$/i            then context.trunk
        # NOTE: - inconsistent use of left v. trunk
        when /^_right$/i           then context.tag
        when /^_(\d+)$/i           then ordinal_part $LAST_MATCH_INFO[1].to_i, context
        when /^_(L*)(R?)$/i        then partmap_part $LAST_MATCH_INFO, context
        else                            part
        end.to_s.strip
      end
    end

    def user_part part
      self.class.session || part
    end

    def ordinal_part pos, context
      pos = context.length if pos > context.length
      context.parts[pos - 1]
    end

    def partmap_part match, context
      l_s = match[1].size
      r_s = !match[2].empty?
      l_part = context.nth_left_name l_s
      r_s ? l_part.tag : l_part.s
    end

    def absolutize_extremes new_parts, context
      [0, -1].each do |i|
        new_parts[i] = context if absolutize_extreme? new_parts, context, i
      end
    end

    def absolutize_extreme? new_parts, context, index
      return false if new_parts[index].present?

      # following avoids recontextualizing with relative contexts.
      # Eg, `+A+B+.absolute('+A')` should be +A+B, not +A+A+B.
      !new_parts.to_name.send "#{%i[start end][index]}s_with_parts?", context
    end
  end
end