module CSL class Node extend Forwardable extend Extensions::Nesting 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 hide_default_attributes? !@show_default_attributes end def hide_default_attributes! @show_default_attributes = false end def show_default_attributes! @show_default_attributes = true 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 def parse(data) parse!(data) rescue nil end def parse!(data) node = CSL.parse!(data, self) raise ParseError, "root node not #{self.name}: #{node.inspect}" unless node.class == self || Node.equal?(self) node end private def has_language attr_accessor :language define_method :has_language? do !language.nil? end public :language, :language=, :has_language? alias_method :original_attribute_assignments, :attribute_assignments define_method :attribute_assignments do if has_language? original_attribute_assignments.unshift('xml:lang="%s"' % language) else original_attribute_assignments end end private :original_attribute_assignments, :attribute_assignments end def attr_defaults(attributes) default_attributes.merge! 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 CSL.silence_warnings do def initialize(attrs = {}) super(*attrs.symbolize_keys.values_at(*keys)) end end # @return [] a list of symbols representing the names/keys # of the attribute variables. def keys __class__.keys end def symbolize_keys self end def values super.compact end def to_hash Hash[keys.zip(values_at(*keys)).reject { |_, 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 alias merge! merge # @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 identifying 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) arguments.flatten.inject([]) do |values, key| if key.is_a?(Symbol) values.push fetch(key) else values.concat super(key) end end 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 def initialize_copy(other) @parent, @ancestors, @descendants, @siblings, @root, @depth = nil initialize(other.attributes) end def deep_copy copy = dup each_child do |child| copy.add_child child.deep_copy end copy end def merge!(options) attributes.merge!(options) self end def reverse_merge!(options) options.each_pair do |key, value| attributes[key] = value unless attribute? key end self end # @return [Boolean] whether or not the node has default attributes def has_default_attributes? !default_attributes.empty? end alias has_defaults? has_default_attributes? # Iterates through the Node's attributes def each(&block) if block_given? attributes.each_pair(&block) self else to_enum end end alias each_pair each # @param name [#to_sym] the name of the attribute # @return [Boolean] whether or not key is set to the default value def default_attribute?(name) defaults = self.class.default_attributes name, value = name.to_sym, attributes.fetch(name) return false unless !value.nil? || defaults.key?(name) defaults[name] == value end # @return [Hash] the attributes currently set to their default values def default_attributes attributes.to_hash.select do |name, _| default_attribute?(name) end end # @return [Hash] the attributes currently not set to their default values def custom_attributes attributes.to_hash.reject do |name, _| default_attribute?(name) end end # Returns true if the node contains an attribute with the passed-in name; # false otherwise. def attribute?(name) attributes.fetch(name, false) end # @param [[String]] names list of attribute names # @return [Boolean] true if the node contains attributes for all # passed-in names; false otherwise. def attributes?(*names) names.flatten(1).all? do |name| attribute?(name) end end # Returns true if the node contains any attributes (ignores nil values); # false otherwise. def has_attributes? !attributes.empty? end def has_language? false 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 # 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? # # @example # node.match?(name, conditions) # node.match?(conditions) # node.match?(other_node) # # @param name [String,Regexp,Node] must match the nodename; alternatively # you can pass a node # @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? # # @example # node.exact_match?(name, conditions) # node.exact_match?(conditions) # node.exact_match?(other_node) # # @param name [String,Regexp,Node] must match the nodename; alternatively # you can pass a node # @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? # @option filter [Array] a list of attribute names # @return [Hash] the node's attributes matching the filter def attributes_for(*filter) filter.flatten! Hash[map { |name, value| !value.nil? && filter.include?(name) ? [name, value.to_s] : nil }.compact] end # The node's formatting options. If the node's parent responds # to `inheritable_formatting_options`, these will be included # in the result. This makes it easy for nodes to push formatting # options to their child nodes. # # @return [Hash] the node's formatting options def formatting_options options = attributes_for Schema.attr(:formatting) if !root? && parent.respond_to?(:inheritable_formatting_options) parent.inheritable_formatting_options.merge(options) else options end end # Whether or not page ranges should be formatted when # rendering this node. # # Page ranges must be formatted if the node is part of # a {Style} with a page-range-format value. # # @return [Boolean] whether or not page ranges should # be formatted def format_page_ranges? root.respond_to?(:has_page_range_format?) && root.has_page_range_format? end def page_range_format return unless format_page_ranges? root.page_range_format end def strip_periods? attribute?(:'strip-periods') && !!(attributes[:'strip-periods'].to_s =~ /^true$/i) end def quotes? attribute?(:'quotes') && !!(attributes[:'quotes'].to_s =~ /^true$/i) end def <=>(other) return nil unless other.is_a?(Node) comparables <=> other.comparables 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 protected def match_conditions end def comparables c = [] c << nodename c << attributes c << (textnode? ? text : '') c << (has_children? ? children : []) c end private def attribute_assignments attrs = self.class.hide_default_attributes? ? custom_attributes : attributes.to_hash attrs.map { |name, value| value.nil? ? nil : [name, CSL.encode_xml_attr(value.to_s)].join('=') }.compact end def match_conditions_for(name, conditions) case name when Node conditions, name = name.attributes.to_hash, name.nodename 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 def to_s CSL.encode_xml_text text.to_s.strip end # 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.respond_to?(:each_pair) 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 initialize_copy(other) super @text = other.text end def textnode? true end remove_method :empty? def empty? text.nil? || text.empty? end def tags ["<#{attribute_assignments.unshift(nodename).join(' ')}>#{to_s}"] end def inspect "#<#{[self.class.name, text.inspect, *attribute_assignments].join(' ')}>" end end end