module CSL class Node extend Forwardable include Enumerable include Comparable include Treelike include PrettyPrinter class << self def inherited(subclass) types << subclass subclass.nesting.each do |klass| klass.types << subclass if klass < Node end end def types @types ||= Set.new end def default_attributes @default_attributes ||= {} end def constantize(name) pattern = /#{name.to_s.tr('-', '')}$/i klass = types.detect { |t| t.matches?(pattern) } case when !klass.nil? klass when nesting[-2].respond_to?(:constantize) nesting[-2].constantize(name) else nil end end # @return [Boolean] whether or not the node's name matches the # passed-in name pattern def match?(name_pattern) name_pattern === name end alias matches? match? # Returns a new node with the passed in name and attributes. def create(name, attributes = {}, &block) klass = constantize(name) node = (klass || Node).new(attributes, &block) node.nodename = name node end def create_attributes(attributes) if const?(:Attributes) const_get(:Attributes).new(default_attributes.merge(attributes)) else default_attributes.merge(attributes) end end private def attr_defaults(attributes) @default_attributes = attributes end # Creates a new Struct for the passed-in attributes. Node instances # will create an instance of this struct to manage their respective # attributes. # # The new Struct will be available as Attributes in the current node's # class scope. def attr_struct(*attributes) const_set(:Attributes, Struct.new(*attributes) { # 1.8 Compatibility @keys = attributes.map(&:to_sym).freeze class << self attr_reader :keys end def initialize(attrs = {}) super(*attrs.symbolize_keys.values_at(*keys)) end # @return [] a list of symbols representing the names/keys # of the attribute variables. def keys __class__.keys end def values super.compact end # def to_a # keys.zip(values_at(*keys)).reject { |k,v| v.nil? } # end # @return [Boolean] true if all the attribute values are nil; # false otherwise. def empty? values.compact.empty? end def fetch(key, default = nil) value = keys.include?(key.to_sym) && send(:'[]', key) if block_given? value || yield(key) else value || default end end # Merges the current with the passed-in attributes. # # @param other [#each_pair] the other attributes # @return [self] def merge(other) raise ArgumentError, "failed to merge #{other.class} into Attributes" unless other.respond_to?(:each_pair) other.each_pair do |part, value| part = part.to_sym send(:'[]=', part, value) if !value.nil? && keys.include?(part) end self end # @overload values_at(selector, ... ) # Returns an array containing the attributes in self according # to the given selector(s). The selectors may be either integer # indices, ranges (functionality inherited from Struct) or # symbols idenifying valid keys (similar to Hash#values_at). # # @example # attributes.values_at(:family, :nick) #=> ['Matsumoto', 'Matz'] # # @see Struct#values_at # @return [Array] the list of values def values_at(*arguments) super(*arguments.flatten.map { |k| k.is_a?(Symbol) ? keys.index(k) : k }) end }) end end attr_reader :attributes def_delegators :attributes, :[], :[]=, :values, :values_at, :length, :size def initialize(attributes = {}) @attributes = self.class.create_attributes(attributes) @children = self.class.create_children yield self if block_given? end # Iterates through the Node's attributes def each if block_given? attributes.each_pair(&Proc.new) self else to_enum end end alias each_pair each # Returns true if the node contains an attribute with the passed-in name; # false otherwise. def attribute?(name) attributes.fetch(name, false) end # Returns true if the node contains any attributes (ignores nil values); # false otherwise. def has_attributes? !attributes.empty? end def textnode? false end alias has_text? textnode? def save_to(path, options = {}) File.open(path, 'w:UTF-8') do |f| f << (options[:compact] ? to_xml : pretty_print) end self end # Tests whether or not the Name matches the passed-in node name and # attribute conditions; if a Hash is passed as a single argument, # it is taken as the conditions parameter (the name parameter is # automatically matches in this case). # # Whether or not the arguments match the node is determined as # follows: # # 1. The name must match {#nodename} # 2. All attribute name/value pairs passed as conditions must match # the corresponding attributes of the node # # Note that only attributes present in the passed-in conditions # influence the match – if you want to match only nodes that contain # no other attributes than specified by the conditions, {#exact_match?} # should be used instead. # # @see #exact_match? # # @param name [String,Regexp] must match the nodename # @param conditions [Hash] the conditions # # @return [Boolean] whether or not the query matches the node def match?(name = nodename, conditions = {}) name, conditions = match_conditions_for(name, conditions) return false unless name === nodename return true if conditions.empty? conditions.values.zip( attributes.values_at(*conditions.keys)).all? do |condition, value| condition === value end end alias matches? match? # Tests whether or not the Name matches the passed-in node name and # attribute conditions exactly; if a Hash is passed as a single argument, # it is taken as the conditions parameter (the name parameter is # automatically matches in this case). # # Whether or not the arguments match the node is determined as # follows: # # 1. The name must match {#nodename} # 2. All attribute name/value pairs of the node must match the # corresponding pairs in the passed-in Hash # # Note that all node attributes are used by this method – if you want # to match only a subset of attributes {#match?} should be used instead. # # @see #match? # # @param name [String,Regexp] must match the nodename # @param conditions [Hash] the conditions # # @return [Boolean] whether or not the query matches the node exactly def exact_match?(name = nodename, conditions = {}) name, conditions = match_conditions_for(name, conditions) return false unless name === nodename return true if conditions.empty? conditions.values_at(*attributes.keys).zip( attributes.values_at(*attributes.keys)).all? do |condition, value| condition === value end end alias matches_exactly? exact_match? def <=>(other) [nodename, attributes, children] <=> [other.nodename, other.attributes, other.children] rescue nil end # Returns the node' XML tags (including attribute assignments) as an # array of strings. def tags if has_children? tags = [] tags << "<#{[nodename, *attribute_assignments].join(' ')}>" tags << children.map { |node| node.respond_to?(:tags) ? node.tags : [node.to_s] }.flatten(1) tags << "" tags else ["<#{[nodename, *attribute_assignments].join(' ')}/>"] end end def inspect "#<#{[self.class.name, *attribute_assignments].join(' ')} children=[#{children.count}]>" end alias to_s pretty_print private def attribute_assignments each_pair.map { |name, value| value.nil? ? nil: [name, value.to_s.inspect].join('=') }.compact end def match_conditions_for(name, conditions) case name when Hash conditions, name = name, nodename when Symbol name = name.to_s end [name, conditions.symbolize_keys] end end class TextNode < Node has_no_children class << self undef_method :attr_children # @override def create(name, attributes = {}, &block) klass = constantize(name) node = (klass || TextNode).new(attributes, &block) node.nodename = name node end end attr_accessor :text alias to_s text # TextNodes quack like a string. # def_delegators :to_s, *String.instance_methods(false).reject do |m| # m.to_s =~ /^\W|!$|(?:^(?:hash|eql?|to_s|length|size|inspect)$)/ # end # # String.instance_methods(false).select { |m| m.to_s =~ /!$/ }.each do |m| # define_method(m) do # content.send(m) if content.respond_to?(m) # end # end def initialize(argument = '') case when argument.is_a?(Hash) super when argument.respond_to?(:to_s) super({}) @text = argument.to_s yield self if block_given? else raise ArgumentError, "failed to create text node from #{argument.inspect}" end end def textnode? true end def tags ["<#{attribute_assignments.unshift(nodename).join(' ')}>#{text}"] end def inspect "#<#{[self.class.name, text.inspect, *attribute_assignments].join(' ')}>" end end end