require 'wlang/encoder_set'
require 'wlang/rule_set'
module WLang
#
# Implements the _dialect_ abstraction (see {README}[link://files/README.rdoc]).
# A dialect instance is an aggregation of encoders and ruleset (through EncoderSet
# and RuleSet classes). A dialect is also a node in the dialect tree and has a
# qualified name through this tree. For example wlang/xhtml is the
# qualified name of a xhtml dialect which is a child dialect of
# wlang.
#
# Users are not intended to use this class directly. Use the Domain Specific
# Language instead (see WLang::Dialect::DSL).
#
# === For developers only
#
# In order to avoid having users to install all required gems of all dialects
# wlang implements a lazy load design pattern on the dialect tree, through the
# WLang::Dialect::DSL and WLang::Dialect::Loader classes. The former only creates
# Dialect instances as tree nodes (by chaining dialects through @parent) and
# installs mapping with file extensions. Rules and encoders are not initially
# installed (precisely: WLang::Dialect::DSL#require_ruby is simply ignored).
# When a given dialect is needed by wlang it is first built (through the build!
# method and the WLang::Dialect::Loader class).
#
# Standard dialect obtention methods (WLang#dialect as well as WLang::Dialect#dialect)
# ensure that returned dialects are built. If you obtain dialects another way,
# be sure that they are built before using them (is_built? and build! are your
# friends to achieve that goal).
#
# Moreover, child dialects may require tools of their ancestors. The following
# invariant should always be respected: if a dialect is built, all its ancestors
# are built as well. This invariant is not enforced by the build! method because
# it is trivially respected by the way WLang::Dialect#dialect is implemented.
#
class Dialect
# Underlying ruleset
attr_reader :ruleset
# Underlying encoders
attr_reader :encoders
# Dialect name
attr_reader :name
# Parent dialect
attr_reader :parent
# Sub dialects by name
attr_reader :dialects
# Post transformer
attr_accessor :post_transformer
#
# Creates a dialect instance. _builder_ block is a chunk of code of the DSL
# that will be executed twice: once at construction time to create sub dialects
# nodes and install file extensions and once at building time to install ruleset
# and encoders.
#
def initialize(name, parent, &builder)
@name, @parent = name, parent
@builder, @built = builder, builder.nil?
@dialects = nil
@encoders = nil
@ruleset = nil
DSL.new(self).instance_eval(&builder) unless builder.nil?
end
### Lazy load mechanism ######################################################
#
# Force the dialect to be built. Has no effect if it is already built. Invokes
# the DSL chunk of code through WLang::DSL::Loader otherwise.
#
def build!
unless is_built?
WLang::Dialect::Loader.new(self).instance_eval(&@builder)
@built = true
end
self
end
# Returns true if this dialect is already built, false otherwise.
def is_built?
return @built
end
### Installation #############################################################
#
# Adds a child dialect under _name_. _name_ cannot be qualified and must be a
# valid dialect name according to the wlang specification (see WLang::DIALECT_NAME_REGEXP).
# _child_ must be a Dialect instance.
#
def add_child_dialect(name, child)
raise(ArgumentError, "Invalid dialect name") unless WLang::DIALECT_NAME_REGEXP =~ name
raise(ArgumentError, "Dialect expected") unless Dialect===child
@dialects = {} if @dialects.nil?
@dialects[name] = child
end
# See EncoderSet#add_encoder
def add_encoder(name, &block)
@encoders = EncoderSet.new if @encoders.nil?
@encoders.add_encoder(name, &block)
end
# See EncoderSet#add_encoders
def add_encoders(mod, pairs)
@encoders = EncoderSet.new if @encoders.nil?
@encoders.add_encoders(mod, pairs)
end
# See RuleSet::add_rule
def add_rule(name, &block)
@ruleset = RuleSet.new if @ruleset.nil?
@ruleset.add_rule(name, &block)
end
# See RuleSet::add_rules
def add_rules(mod, pairs)
@ruleset = RuleSet.new if @ruleset.nil?
@ruleset.add_rules(mod, pairs)
end
### Query API ################################################################
# Returns qualified name of this dialect
def qualified_name
parentname = @parent.nil? ? "" : @parent.to_s
return ""==parentname ? @name : parentname + '/' + @name
end
#
# Finds a child dialect by name. _name_ can be a String denoting a qualified
# name as well as an Array of strings, resulting from a qualified name split.
# This method should always be invoked on built dialects, it always returns nil
# otherwise. When found, returned dialect is automatically built as well as all
# its ancestors. When not found, the method returns nil.
#
def dialect(name)
# implement argument conventions
if String===name
raise(ArgumentError, "Invalid dialect name #{name}") unless WLang::QUALIFIED_DIALECT_NAME_REGEXP =~ name
name = name.split('/')
elsif not(Array===name)
raise(ArgumentError,"Invalid dialect name #{name}")
end
# not built or no child at all
return nil if @dialects.nil?
# get first child name and find it
child_name = name[0]
child_dialect = @dialects[child_name]
if child_dialect.nil?
# unexisting, return nil
return nil
elsif name.length==1
# found and last of qualified name -> build it
return child_dialect.build!
else
# found but not last of qualified name -> build it and delegate
child_dialect.build!
return child_dialect.dialect(name[1..-1])
end
end
# Applies post transformation
def apply_post_transform(text)
case self.post_transformer
when String
WLang::encode(text, self.post_transformer, {})
when Proc
self.post_transformer.call(text)
else
text
end
end
#
# Finds an encoder by name.
#
def encoder(name)
# implement argument conventions
if String===name
raise(ArgumentError, "Invalid encoder name #{name}") unless WLang::QUALIFIED_ENCODER_NAME_REGEXP =~ name
name = name.split('/')
elsif not(Array===name)
raise(ArgumentError,"Invalid encoder name #{name}")
end
# last name in qualified?
if name.length==1
# delegate to encoders
return nil if @encoders.nil?
return @encoders.get_encoder(name[0])
else
# find sub dialect, and delegate
child_name = name[0]
child_dialect = dialect(child_name)
if child_dialect.nil?
return nil
else
return child_dialect.encoder(name[1..-1])
end
end
end
# Finds a encoder in dialect tree
def find_encoder(name)
raise(ArgumentError, "Invalid (relative) encoder name #{name}") unless String===name
raise(ArgumentError, "Invalid (relative) encoder name #{name}") if name.include?("/")
return nil if @encoders.nil?
if @encoders.has_encoder?(name)
@encoders.get_encoder(name)
elsif @parent
@parent.find_encoder(name)
else
nil
end
end
# See RuleSet#pattern.
def pattern(block_symbols)
return RuleSet.new.pattern(block_symbols) if @ruleset.nil?
@ruleset.pattern(block_symbols)
end
### Other utilities ##########################################################
# Factors a spacing friendly buffer for instantiation in this dialect
def factor_buffer
#IntelligentBuffer.new
""
end
# Returns a string representation
def to_s
qualified_name
end
end # class Dialect
end #module WLang