# encoding: utf-8
require_relative "../constants"
require_relative '../utils/logging'
require_relative 'monkeypatches'
# @author Alexei Matyushkin
module Qipowl::Bowlers
# Base class for all the parsers.
#
# Technically it may be instantiated, but that’s meaningless.
# Main operation method for it and all the descendants is
# {#parse}. It sequentially executes following
# private methods:
#
# - {#defreeze}
# - {#roast}
# - {#serveup}
#
# Normally the developer does not need to interfere the {#roast}
# method which proceeds the input string. To prepare the input
# for +evaluation+ one overwrites {#defreeze}, for some afterwork
# the {#serveup} method is here.
#
# Descendants are supposed to overwrite {#method_missing} for some
# custom processing and introduce DSL methods, which will be executed
# by `eval` inside the {#roast} method.
#
# Instance variables `:in` and `:out` are here to gain an access to
# the last interpreted input string and the result of evaluation
# respectively.
#
class Bowler
include TypoLogging
attr_reader :in, :out
# Internal constant for joining/splitting the strings during processing.
# Override on your own risk. I can′t imagine why you would need to do so.
SEPARATOR = $, || ' '
def execute str
@out = (serveup roast defreeze @in = str)
end
# Everything is a DSL, remember?
#
# @return true
def respond_to? method, incl_priv = false
true
end
# Everything is a DSL, remember? Even constants.
# @todo This fails to do with DSLing words, beginning with capitals :-(
#
# @return the constant name as is
def self.const_missing name
raise "There was CONST [#{name}] met. Stupid programming error."
name
end
# If somebody needs to interfere the standard processing,
# she supposed to introduce `special_handler` method. The descendants
# will be processed before standard operation (which in fact simply
# collects words within an array one by one.)
def method_missing method, *args, &block
method, *args = special_handler(method, *args, &block) \
if self.private_methods.include?(:special_handler)
[method, args].flatten
end
# Adds new +entity+ in the section specified.
# E. g., call to
#
# add_spice :linewide, :°, :deg, :degrees
#
# in HTML implementation adds a support for specifying something like:
#
# ° 15
# ° 30
# ° 45
#
# which is to be converted to the following:
#
#
# 15
# 30
# 45
#
#
# @param [Symbol] section the section (it must be one of {Mapping.SPICES}) to add new key to
# @param [Symbol] key the name for the key
# @param [Symbol] value the value
# @param [Symbol] enclosure_value optional value to be added for the key into enclosures section
def add_entity section, key, value, enclosure_value = null
if (tags = self.class.const_get("#{section.upcase}_TAGS"))
key = key.bowl.to_sym
tags[key] = value.to_sym
self.class.const_get("ENCLOSURES_TAGS")[key] = enclosure_value.to_sym if enclosure_value
self.class.class_eval %Q{
alias_method :#{key}, :∀_#{section}
} # unless self.class.instance_methods(true).include?(key.bowl)
else
logger.warn "Trying to add key “#{key}” in an invalid section “#{section}”. Ignoring…"
end
end
# Removes key from both {Mapping.SPICES} and {Mapping.SALT}. See {#add_spice}
#
# @param [Symbol] key the key to be removed
def remove_entity key
Qipowl::ENTITIES.each { |section|
next unless (curr_sect = self.class.const_get("#{section.upcase}_TAGS") rescue nil)
next unless send :"∃_#{section}_tag", key.to_sym
curr_sect.delete key
self.class.const_get("ENCLOSURES_TAGS").delete key
self.class.class_eval %Q{
remove_method :#{key.bowl}
}
}
end
protected
Qipowl::ENTITIES.each { |section|
define_method "∀_#{section}".to_sym, ->(*args) {
raise "Default method for #{section} (“#{self.class.name.gsub(/_\d+\Z/, '')}#∀_#{section}”) MUST be defined"
} unless Bowler.instance_methods(true).include?("∀_#{section}".to_sym)
}
def defreeze str
str
end
def roast str
(split str).map { |dish|
@yielded = []
rest = begin
eval(dish.bowl.carriage)
rescue Exception => e
msg = e.message.dup
logger.error '='*78
logger.error "Could not roast dish [#{msg.force_encoding(Encoding::UTF_8)}].\nWill return as is… Dish follows:\n\n"
logger.error '-'*78
logger.error dish
logger.error '='*78
[*dish]
end
harvest(nil, orphan([*rest].join(SEPARATOR))) # FIXME Check if this is correct in all the cases
@yielded.pop(@yielded.size).reverse.join(SEPARATOR)
}.join($/).uncarriage.un␚ify.unspacefy.unbowl
end
def serveup str
str.gsub(/⌦./, '').gsub(/.⌫/, '')
end
protected
# The handler of the last “orphaned” text block in the input string.
#
# E.g.:
#
# Here goes a quite significant list:
#
# • line item 1
# • nested li1
# • nested li 2
# • line item 2
#
# While all line items are operated by `•` method, the top sentence
# is orphaned (has no prepnding DSL method to be called on.)
# Since we still need to handle it somehow, the {#orphan} method is here.
#
# @param [String] str the string to be operated “bu default rule”
# @return [String] the processed input (in derivatives, here it returns the untouched input string itself)
def orphan str
str
end
# The handler for harvesting partial result.
#
# Processing sometimes calls this method, designating the meaningful
# part of input text is done and should be yielded. E.g. when the
# block of code is processed:
#
# Λ ruby
# @mapping[:inplace].each { |tag, htmltag|
# do_smth tag, htmltag
# }
# Λ
#
# After we have this part of input processed, it should be considered
# “done.” So block processors call {#harvest} to store processed parts.
#
# @param [Symbol] callee of this method. Qipowl hardly relies on method namings and sometimes we may need to know if the call was made by, say, lineitem DSL (`•`), not datalist (`▷`).
# @param [String] str string to yield
#
# @return nil
def harvest callee, str
@yielded << str unless str.vacant?
nil
end
private
# Prepares blocks in the input for the execution
def block str
return str unless self.class.const_defined?(:BLOCK_TAGS)
result = str.dup
self.class::BLOCK_TAGS.each { |tag, value|
result.gsub!(/(#{tag})\s*(\S*\s?|$)(.*?)(#{tag}|\Z)/m) {
%Q{
#{$1} #{$2.strip.bowl} #{$3.bowl.carriage}
}
}
}
result
end
# Prepares customs in the input for the execution
def custom str
return str unless self.class.const_defined?(:CUSTOM_TAGS)
result = str.dup
self.class::CUSTOM_TAGS.each { |tag, value|
result.gsub!(/#{tag}/m, value)
}
result
end
# Prepares grips in the input for the execution
# FIX