From 19b50342ee46e43ce598f6e829585f62ebc8252c Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sat, 15 Feb 2020 12:22:25 -0500 Subject: [PATCH 1/6] rufo formatting --- lib/nokogiri/xml/node.rb | 212 +++++++++++++++++---------------- lib/nokogiri/xml/searchable.rb | 30 ++--- 2 files changed, 123 insertions(+), 119 deletions(-) diff --git a/lib/nokogiri/xml/node.rb b/lib/nokogiri/xml/node.rb index f2a35a6ce6..255ef90ede 100644 --- a/lib/nokogiri/xml/node.rb +++ b/lib/nokogiri/xml/node.rb @@ -1,7 +1,7 @@ # encoding: UTF-8 # frozen_string_literal: true -require 'stringio' -require 'nokogiri/xml/node/save_options' +require "stringio" +require "nokogiri/xml/node/save_options" module Nokogiri module XML @@ -57,49 +57,49 @@ class Node include Enumerable # Element node type, see Nokogiri::XML::Node#element? - ELEMENT_NODE = 1 + ELEMENT_NODE = 1 # Attribute node type - ATTRIBUTE_NODE = 2 + ATTRIBUTE_NODE = 2 # Text node type, see Nokogiri::XML::Node#text? - TEXT_NODE = 3 + TEXT_NODE = 3 # CDATA node type, see Nokogiri::XML::Node#cdata? CDATA_SECTION_NODE = 4 # Entity reference node type - ENTITY_REF_NODE = 5 + ENTITY_REF_NODE = 5 # Entity node type - ENTITY_NODE = 6 + ENTITY_NODE = 6 # PI node type - PI_NODE = 7 + PI_NODE = 7 # Comment node type, see Nokogiri::XML::Node#comment? - COMMENT_NODE = 8 + COMMENT_NODE = 8 # Document node type, see Nokogiri::XML::Node#xml? - DOCUMENT_NODE = 9 + DOCUMENT_NODE = 9 # Document type node type DOCUMENT_TYPE_NODE = 10 # Document fragment node type DOCUMENT_FRAG_NODE = 11 # Notation node type - NOTATION_NODE = 12 + NOTATION_NODE = 12 # HTML document node type, see Nokogiri::XML::Node#html? HTML_DOCUMENT_NODE = 13 # DTD node type - DTD_NODE = 14 + DTD_NODE = 14 # Element declaration type - ELEMENT_DECL = 15 + ELEMENT_DECL = 15 # Attribute declaration type - ATTRIBUTE_DECL = 16 + ATTRIBUTE_DECL = 16 # Entity declaration type - ENTITY_DECL = 17 + ENTITY_DECL = 17 # Namespace declaration type - NAMESPACE_DECL = 18 + NAMESPACE_DECL = 18 # XInclude start type - XINCLUDE_START = 19 + XINCLUDE_START = 19 # XInclude end type - XINCLUDE_END = 20 + XINCLUDE_END = 20 # DOCB document node type DOCB_DOCUMENT_NODE = 21 - def initialize name, document # :nodoc: + def initialize(name, document) # :nodoc: # ... Ya. This is empty on purpose. end @@ -111,20 +111,20 @@ def decorate! ### # Search this node's immediate children using CSS selector +selector+ - def > selector + def >(selector) ns = document.root.namespaces xpath CSS.xpath_for(selector, :prefix => "./", :ns => ns).first end ### # Get the attribute value for the attribute +name+ - def [] name + def [](name) get(name.to_s) end ### # Set the attribute value for the attribute +name+ to +value+ - def []= name, value + def []=(name, value) set name.to_s, value.to_s end @@ -135,7 +135,7 @@ def []= name, value # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +<<+. - def add_child node_or_tags + def add_child(node_or_tags) node_or_tags = coerce(node_or_tags) if node_or_tags.is_a?(XML::NodeSet) node_or_tags.each { |n| add_child_node_and_reparent_attrs n } @@ -152,7 +152,7 @@ def add_child node_or_tags # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +add_child+. - def prepend_child node_or_tags + def prepend_child(node_or_tags) if first = children.first # Mimic the error add_child would raise. raise RuntimeError, "Document already has a root node" if document? && !(node_or_tags.comment? || node_or_tags.processing_instruction?) @@ -162,7 +162,6 @@ def prepend_child node_or_tags end end - ### # Add html around this node # @@ -181,7 +180,7 @@ def wrap(html) # Returns self, to support chaining of calls (e.g., root << child1 << child2) # # Also see related method +add_child+. - def << node_or_tags + def <<(node_or_tags) add_child node_or_tags self end @@ -193,7 +192,7 @@ def << node_or_tags # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +before+. - def add_previous_sibling node_or_tags + def add_previous_sibling(node_or_tags) raise ArgumentError.new("A document may not have multiple root nodes.") if (parent && parent.document?) && !(node_or_tags.comment? || node_or_tags.processing_instruction?) add_sibling :previous, node_or_tags @@ -206,7 +205,7 @@ def add_previous_sibling node_or_tags # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +after+. - def add_next_sibling node_or_tags + def add_next_sibling(node_or_tags) raise ArgumentError.new("A document may not have multiple root nodes.") if (parent && parent.document?) && !(node_or_tags.comment? || node_or_tags.processing_instruction?) add_sibling :next, node_or_tags @@ -219,7 +218,7 @@ def add_next_sibling node_or_tags # Returns self, to support chaining of calls. # # Also see related method +add_previous_sibling+. - def before node_or_tags + def before(node_or_tags) add_previous_sibling node_or_tags self end @@ -231,7 +230,7 @@ def before node_or_tags # Returns self, to support chaining of calls. # # Also see related method +add_next_sibling+. - def after node_or_tags + def after(node_or_tags) add_next_sibling node_or_tags self end @@ -243,7 +242,7 @@ def after node_or_tags # Returns self. # # Also see related method +children=+ - def inner_html= node_or_tags + def inner_html=(node_or_tags) self.children = node_or_tags self end @@ -255,7 +254,7 @@ def inner_html= node_or_tags # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +inner_html=+ - def children= node_or_tags + def children=(node_or_tags) node_or_tags = coerce(node_or_tags) children.unlink if node_or_tags.is_a?(XML::NodeSet) @@ -273,13 +272,13 @@ def children= node_or_tags # Returns the reparented node (if +node_or_tags+ is a Node), or NodeSet (if +node_or_tags+ is a DocumentFragment, NodeSet, or string). # # Also see related method +swap+. - def replace node_or_tags + def replace(node_or_tags) # We cannot replace a text node directly, otherwise libxml will return # an internal error at parser.c:13031, I don't know exactly why # libxml is trying to find a parent node that is an element or document # so I can't tell if this is bug in libxml or not. issue #775. if text? - replacee = Nokogiri::XML::Node.new 'dummy', document + replacee = Nokogiri::XML::Node.new "dummy", document add_previous_sibling_node replacee unlink return replacee.replace node_or_tags @@ -303,33 +302,33 @@ def replace node_or_tags # Returns self, to support chaining of calls. # # Also see related method +replace+. - def swap node_or_tags + def swap(node_or_tags) replace node_or_tags self end - alias :next :next_sibling - alias :previous :previous_sibling + alias :next :next_sibling + alias :previous :previous_sibling # :stopdoc: # HACK: This is to work around an RDoc bug - alias :next= :add_next_sibling + alias :next= :add_next_sibling # :startdoc: - alias :previous= :add_previous_sibling - alias :remove :unlink - alias :get_attribute :[] - alias :attr :[] - alias :set_attribute :[]= - alias :text :content - alias :inner_text :content + alias :previous= :add_previous_sibling + alias :remove :unlink + alias :get_attribute :[] + alias :attr :[] + alias :set_attribute :[]= + alias :text :content + alias :inner_text :content alias :has_attribute? :key? - alias :name :node_name - alias :name= :node_name= - alias :type :node_type - alias :to_str :text - alias :clone :dup - alias :elements :element_children + alias :name :node_name + alias :name= :node_name= + alias :type :node_type + alias :to_str :text + alias :clone :dup + alias :elements :element_children #### # Returns a hash containing the node's attributes. The key is @@ -373,7 +372,7 @@ def each # Get the list of class names of this Node, without # deduplication or sorting. def classes - self['class'].to_s.scan(/\S+/) + self["class"].to_s.scan(/\S+/) end ### @@ -384,9 +383,9 @@ def classes # # More than one class may be added at a time, separated by a # space. - def add_class name + def add_class(name) names = classes - self['class'] = (names + (name.scan(/\S+/) - names)).join(' ') + self["class"] = (names + (name.scan(/\S+/) - names)).join(" ") self end @@ -398,8 +397,8 @@ def add_class name # # More than one class may be appended at a time, separated by a # space. - def append_class name - self['class'] = (classes + name.scan(/\S+/)).join(' ') + def append_class(name) + self["class"] = (classes + name.scan(/\S+/)).join(" ") self end @@ -413,13 +412,13 @@ def append_class name # # If no class name is left after removal, or when +name+ is nil, # the "class" attribute is removed from this Node. - def remove_class name = nil + def remove_class(name = nil) if name names = classes - name.scan(/\S+/) if names.empty? - delete 'class' + delete "class" else - self['class'] = names.join(' ') + self["class"] = names.join(" ") end else delete "class" @@ -429,23 +428,24 @@ def remove_class name = nil ### # Remove the attribute named +name+ - def remove_attribute name + def remove_attribute(name) attr = attributes[name].remove if key? name clear_xpath_context if Nokogiri.jruby? attr end + alias :delete :remove_attribute ### # Returns true if this Node matches +selector+ - def matches? selector + def matches?(selector) ancestors.last.search(selector).include?(self) end ### # Create a DocumentFragment containing +tags+ that is relative to _this_ # context node. - def fragment tags + def fragment(tags) type = document.html? ? Nokogiri::HTML : Nokogiri::XML type::DocumentFragment.new(document, tags, self) end @@ -454,7 +454,7 @@ def fragment tags # Parse +string_or_io+ as a document fragment within the context of # *this* node. Returns a XML::NodeSet containing the nodes parsed from # +string_or_io+. - def parse string_or_io, options = nil + def parse(string_or_io, options = nil) ## # When the current node is unparented and not an element node, use the # document as the parsing context instead. Otherwise, the in-context @@ -490,13 +490,13 @@ def parse string_or_io, options = nil #### # Set the Node's content to a Text node containing +string+. The string gets XML escaped, not interpreted as markup. - def content= string + def content=(string) self.native_content = encode_special_chars(string.to_s) end ### # Set the parent Node for this Node - def parent= parent_node + def parent=(parent_node) parent_node.add_child(self) parent_node end @@ -582,6 +582,7 @@ def read_only? def element? type == ELEMENT_NODE end + alias :elem? :element? ### @@ -592,7 +593,7 @@ def to_s end # Get the inner_html for this node's Node#children - def inner_html *args + def inner_html(*args) children.map { |x| x.to_html(*args) }.join end @@ -600,13 +601,13 @@ def inner_html *args def css_path path.split(/\//).map { |part| part.length == 0 ? nil : part.gsub(/\[(\d+)\]/, ':nth-of-type(\1)') - }.compact.join(' > ') + }.compact.join(" > ") end ### # Get a list of ancestor Node for this Node. If +selector+ is given, # the ancestors must match +selector+ - def ancestors selector = nil + def ancestors(selector = nil) return NodeSet.new(document) unless respond_to?(:parent) return NodeSet.new(document) unless parent @@ -633,9 +634,10 @@ def ancestors selector = nil # present in parsed XML. A default namespace set with this method will # now show up in #attributes, but when this node is serialized to XML an # "xmlns" attribute will appear. See also #namespace and #namespace= - def default_namespace= url + def default_namespace=(url) add_namespace_definition(nil, url) end + alias :add_namespace :add_namespace_definition ### @@ -644,14 +646,14 @@ def default_namespace= url # a Namespace added this way will NOT be serialized as an xmlns attribute # for this node. You probably want #default_namespace= instead, or perhaps # #add_namespace_definition with a nil prefix argument. - def namespace= ns + def namespace=(ns) return set_namespace(ns) unless ns unless Nokogiri::XML::Namespace === ns raise TypeError, "#{ns.class} can't be coerced into Nokogiri::XML::Namespace" end if ns.document != document - raise ArgumentError, 'namespace must be declared on the same document' + raise ArgumentError, "namespace must be declared on the same document" end set_namespace ns @@ -659,20 +661,20 @@ def namespace= ns #### # Yields self and all children to +block+ recursively. - def traverse &block - children.each{|j| j.traverse(&block) } + def traverse(&block) + children.each { |j| j.traverse(&block) } block.call(self) end ### # Accept a visitor. This method calls "visit" on +visitor+ with self. - def accept visitor + def accept(visitor) visitor.visit(self) end ### # Test to see if this Node is equal to +other+ - def == other + def ==(other) return false unless other return false unless other.respond_to?(:pointer_id) pointer_id == other.pointer_id @@ -692,17 +694,17 @@ def == other # config.format.as_xml # end # - def serialize *args, &block + def serialize(*args, &block) options = args.first.is_a?(Hash) ? args.shift : { - :encoding => args[0], - :save_with => args[1] + :encoding => args[0], + :save_with => args[1], } encoding = options[:encoding] || document.encoding options[:encoding] = encoding outstring = String.new - outstring.force_encoding(Encoding.find(encoding || 'utf-8')) + outstring.force_encoding(Encoding.find(encoding || "utf-8")) io = StringIO.new(outstring) write_to io, options, &block io.string @@ -715,7 +717,7 @@ def serialize *args, &block # # See Node#write_to for a list of +options+. For formatted output, # use Node#to_xhtml instead. - def to_html options = {} + def to_html(options = {}) to_format SaveOptions::DEFAULT_HTML, options end @@ -725,7 +727,7 @@ def to_html options = {} # doc.to_xml(:indent => 5, :encoding => 'UTF-8') # # See Node#write_to for a list of +options+ - def to_xml options = {} + def to_xml(options = {}) options[:save_with] ||= SaveOptions::DEFAULT_XML serialize(options) end @@ -736,7 +738,7 @@ def to_xml options = {} # doc.to_xhtml(:indent => 5, :encoding => 'UTF-8') # # See Node#write_to for a list of +options+ - def to_xhtml options = {} + def to_xhtml(options = {}) to_format SaveOptions::DEFAULT_XHTML, options end @@ -757,22 +759,22 @@ def to_xhtml options = {} # # node.write_to(io, :indent_text => '-', :indent => 2) # - def write_to io, *options - options = options.first.is_a?(Hash) ? options.shift : {} - encoding = options[:encoding] || options[0] + def write_to(io, *options) + options = options.first.is_a?(Hash) ? options.shift : {} + encoding = options[:encoding] || options[0] if Nokogiri.jruby? - save_options = options[:save_with] || options[1] - indent_times = options[:indent] || 0 + save_options = options[:save_with] || options[1] + indent_times = options[:indent] || 0 else - save_options = options[:save_with] || options[1] || SaveOptions::FORMAT - indent_times = options[:indent] || 2 + save_options = options[:save_with] || options[1] || SaveOptions::FORMAT + indent_times = options[:indent] || 2 end - indent_text = options[:indent_text] || ' ' + indent_text = options[:indent_text] || " " # Any string times 0 returns an empty string. Therefore, use the same # string instead of generating a new empty string for every node with # zero indentation. - indentation = indent_times.zero? ? '' : (indent_text * indent_times) + indentation = indent_times.zero? ? "" : (indent_text * indent_times) config = SaveOptions.new(save_options.to_i) yield config if block_given? @@ -784,7 +786,7 @@ def write_to io, *options # Write Node as HTML to +io+ with +options+ # # See Node#write_to for a list of +options+ - def write_html_to io, options = {} + def write_html_to(io, options = {}) write_format_to SaveOptions::DEFAULT_HTML, io, options end @@ -792,7 +794,7 @@ def write_html_to io, options = {} # Write Node as XHTML to +io+ with +options+ # # See Node#write_to for a list of +options+ - def write_xhtml_to io, options = {} + def write_xhtml_to(io, options = {}) write_format_to SaveOptions::DEFAULT_XHTML, io, options end @@ -802,7 +804,7 @@ def write_xhtml_to io, options = {} # doc.write_xml_to io, :encoding => 'UTF-8' # # See Node#write_to for a list of options - def write_xml_to io, options = {} + def write_xml_to(io, options = {}) options[:save_with] ||= SaveOptions::DEFAULT_XML write_to io, options end @@ -810,7 +812,7 @@ def write_xml_to io, options = {} ### # Compare two Node objects with respect to their Document. Nodes from # different documents cannot be compared. - def <=> other + def <=>(other) return nil unless other.is_a?(Nokogiri::XML::Node) return nil unless document == other.document compare other @@ -820,7 +822,7 @@ def <=> other # Do xinclude substitution on the subtree below node. If given a block, a # Nokogiri::XML::ParseOptions object initialized from +options+, will be # passed to it, allowing more convenient modification of the parser options. - def do_xinclude options = XML::ParseOptions::DEFAULT_XML + def do_xinclude(options = XML::ParseOptions::DEFAULT_XML) options = Nokogiri::XML::ParseOptions.new(options) if Integer === options # give options to user @@ -830,7 +832,7 @@ def do_xinclude options = XML::ParseOptions::DEFAULT_XML process_xincludes(options.to_i) end - def canonicalize(mode=XML::XML_C14N_1_0,inclusive_namespaces=nil,with_comments=false) + def canonicalize(mode = XML::XML_C14N_1_0, inclusive_namespaces = nil, with_comments = false) c14n_root = self document.canonicalize(mode, inclusive_namespaces, with_comments) do |node, parent| tn = node.is_a?(XML::Node) ? node : parent @@ -840,14 +842,14 @@ def canonicalize(mode=XML::XML_C14N_1_0,inclusive_namespaces=nil,with_comments=f private - def add_sibling next_or_previous, node_or_tags + def add_sibling(next_or_previous, node_or_tags) impl = (next_or_previous == :next) ? :add_next_sibling_node : :add_previous_sibling_node - iter = (next_or_previous == :next) ? :reverse_each : :each + iter = (next_or_previous == :next) ? :reverse_each : :each node_or_tags = coerce node_or_tags if node_or_tags.is_a?(XML::NodeSet) if text? - pivot = Nokogiri::XML::Node.new 'dummy', document + pivot = Nokogiri::XML::Node.new "dummy", document send impl, pivot else pivot = self @@ -863,14 +865,14 @@ def add_sibling next_or_previous, node_or_tags USING_LIBXML_WITH_BROKEN_SERIALIZATION = Nokogiri.uses_libxml?("~> 2.6.0").freeze private_constant :USING_LIBXML_WITH_BROKEN_SERIALIZATION - def to_format save_option, options + def to_format(save_option, options) return dump_html if USING_LIBXML_WITH_BROKEN_SERIALIZATION options[:save_with] = save_option unless options[:save_with] serialize(options) end - def write_format_to save_option, io, options + def write_format_to(save_option, io, options) return (io << dump_html) if USING_LIBXML_WITH_BROKEN_SERIALIZATION options[:save_with] ||= save_option @@ -881,7 +883,7 @@ def inspect_attributes [:name, :namespace, :attribute_nodes, :children] end - def coerce data # :nodoc: + def coerce(data) case data when XML::NodeSet return data @@ -902,9 +904,9 @@ def coerce data # :nodoc: end # @private - IMPLIED_XPATH_CONTEXTS = [ './/'.freeze ].freeze # :nodoc: + IMPLIED_XPATH_CONTEXTS = [".//".freeze].freeze - def add_child_node_and_reparent_attrs node # :nodoc: + def add_child_node_and_reparent_attrs(node) add_child_node node node.attribute_nodes.find_all { |a| a.name =~ /:/ }.each do |attr_node| attr_node.remove diff --git a/lib/nokogiri/xml/searchable.rb b/lib/nokogiri/xml/searchable.rb index 49a14ad857..cc6d686c4a 100644 --- a/lib/nokogiri/xml/searchable.rb +++ b/lib/nokogiri/xml/searchable.rb @@ -46,7 +46,7 @@ module Searchable # ) # # See Searchable#xpath and Searchable#css for further usage help. - def search *args + def search(*args) paths, handler, ns, binds = extract_params(args) xpaths = paths.map(&:to_s).map do |path| @@ -55,6 +55,7 @@ def search *args xpath(*(xpaths + [ns, handler, binds].compact)) end + alias :/ :search ### @@ -64,9 +65,10 @@ def search *args # result. +paths+ must be one or more XPath or CSS queries. # # See Searchable#search for more information. - def at *args + def at(*args) search(*args).first end + alias :% :at ### @@ -102,7 +104,7 @@ def at *args # found in an XML document, where tags names are case-sensitive # (e.g., "H1" is distinct from "h1"). # - def css *args + def css(*args) rules, handler, ns, _ = extract_params(args) css_internal self, rules, handler, ns @@ -115,7 +117,7 @@ def css *args # match. +rules+ must be one or more CSS selectors. # # See Searchable#css for more information. - def at_css *args + def at_css(*args) css(*args).first end @@ -149,7 +151,7 @@ def at_css *args # end # }.new) # - def xpath *args + def xpath(*args) paths, handler, ns, binds = extract_params(args) xpath_internal self, paths, handler, ns, binds @@ -162,17 +164,17 @@ def xpath *args # match. +paths+ must be one or more XPath queries. # # See Searchable#xpath for more information. - def at_xpath *args + def at_xpath(*args) xpath(*args).first end private - def css_internal node, rules, handler, ns + def css_internal(node, rules, handler, ns) xpath_internal node, css_rules_to_xpath(rules, ns), handler, ns, nil end - def xpath_internal node, paths, handler, ns, binds + def xpath_internal(node, paths, handler, ns, binds) document = node.document return NodeSet.new(document) unless document @@ -187,12 +189,12 @@ def xpath_internal node, paths, handler, ns, binds end end - def xpath_impl node, path, handler, ns, binds + def xpath_impl(node, path, handler, ns, binds) ctx = XPathContext.new(node) ctx.register_namespaces(ns) - path = path.gsub(/xmlns:/, ' :') unless Nokogiri.uses_libxml? + path = path.gsub(/xmlns:/, " :") unless Nokogiri.uses_libxml? - binds.each do |key,value| + binds.each do |key, value| ctx.register_variable key.to_s, value end if binds @@ -203,13 +205,13 @@ def css_rules_to_xpath(rules, ns) rules.map { |rule| xpath_query_from_css_rule(rule, ns) } end - def xpath_query_from_css_rule rule, ns + def xpath_query_from_css_rule(rule, ns) self.class::IMPLIED_XPATH_CONTEXTS.map do |implied_xpath_context| CSS.xpath_for(rule.to_s, :prefix => implied_xpath_context, :ns => ns) - end.join(' | ') + end.join(" | ") end - def extract_params params # :nodoc: + def extract_params(params) # :nodoc: handler = params.find do |param| ![Hash, String, Symbol].include?(param.class) end From e6ca6c4edf9bf37ef255e28df82592b64f55b7e0 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sun, 16 Feb 2020 09:31:20 -0500 Subject: [PATCH 2/6] docs: group Node methods together, and use yard @!group and do the same for Searchable related to #1996 --- lib/nokogiri/xml/node.rb | 201 +++++++++++++++++---------------- lib/nokogiri/xml/searchable.rb | 6 +- 2 files changed, 111 insertions(+), 96 deletions(-) diff --git a/lib/nokogiri/xml/node.rb b/lib/nokogiri/xml/node.rb index 255ef90ede..d512823a0e 100644 --- a/lib/nokogiri/xml/node.rb +++ b/lib/nokogiri/xml/node.rb @@ -109,6 +109,8 @@ def decorate! document.decorate(self) end + # @!group Searching via XPath or CSS Queries + ### # Search this node's immediate children using CSS selector +selector+ def >(selector) @@ -116,17 +118,9 @@ def >(selector) xpath CSS.xpath_for(selector, :prefix => "./", :ns => ns).first end - ### - # Get the attribute value for the attribute +name+ - def [](name) - get(name.to_s) - end + # @!endgroup - ### - # Set the attribute value for the attribute +name+ to +value+ - def []=(name, value) - set name.to_s, value.to_s - end + # @!group Manipulating Document Structure ### # Add +node_or_tags+ as a child of this Node. @@ -307,29 +301,94 @@ def swap(node_or_tags) self end + #### + # Set the Node's content to a Text node containing +string+. The string gets XML escaped, not interpreted as markup. + def content=(string) + self.native_content = encode_special_chars(string.to_s) + end + + ### + # Set the parent Node for this Node + def parent=(parent_node) + parent_node.add_child(self) + parent_node + end + + ### + # Adds a default namespace supplied as a string +url+ href, to self. + # The consequence is as an xmlns attribute with supplied argument were + # present in parsed XML. A default namespace set with this method will + # now show up in #attributes, but when this node is serialized to XML an + # "xmlns" attribute will appear. See also #namespace and #namespace= + def default_namespace=(url) + add_namespace_definition(nil, url) + end + + ### + # Set the default namespace on this node (as would be defined with an + # "xmlns=" attribute in XML source), as a Namespace object +ns+. Note that + # a Namespace added this way will NOT be serialized as an xmlns attribute + # for this node. You probably want #default_namespace= instead, or perhaps + # #add_namespace_definition with a nil prefix argument. + def namespace=(ns) + return set_namespace(ns) unless ns + + unless Nokogiri::XML::Namespace === ns + raise TypeError, "#{ns.class} can't be coerced into Nokogiri::XML::Namespace" + end + if ns.document != document + raise ArgumentError, "namespace must be declared on the same document" + end + + set_namespace ns + end + + ### + # Do xinclude substitution on the subtree below node. If given a block, a + # Nokogiri::XML::ParseOptions object initialized from +options+, will be + # passed to it, allowing more convenient modification of the parser options. + def do_xinclude(options = XML::ParseOptions::DEFAULT_XML) + options = Nokogiri::XML::ParseOptions.new(options) if Integer === options + + # give options to user + yield options if block_given? + + # call c extension + process_xincludes(options.to_i) + end + alias :next :next_sibling alias :previous :previous_sibling - - # :stopdoc: - # HACK: This is to work around an RDoc bug alias :next= :add_next_sibling - # :startdoc: - alias :previous= :add_previous_sibling alias :remove :unlink - alias :get_attribute :[] - alias :attr :[] - alias :set_attribute :[]= + alias :name= :node_name= + alias :add_namespace :add_namespace_definition + + # @!endgroup + alias :text :content alias :inner_text :content - alias :has_attribute? :key? alias :name :node_name - alias :name= :node_name= alias :type :node_type alias :to_str :text alias :clone :dup alias :elements :element_children + # @!group Working With Node Attributes + + ### + # Get the attribute value for the attribute +name+ + def [](name) + get(name.to_s) + end + + ### + # Set the attribute value for the attribute +name+ to +value+ + def []=(name, value) + set name.to_s, value.to_s + end + #### # Returns a hash containing the node's attributes. The key is # the attribute name without any namespace, the value is a Nokogiri::XML::Attr @@ -368,6 +427,14 @@ def each } end + ### + # Remove the attribute named +name+ + def remove_attribute(name) + attr = attributes[name].remove if key? name + clear_xpath_context if Nokogiri.jruby? + attr + end + ### # Get the list of class names of this Node, without # deduplication or sorting. @@ -426,15 +493,13 @@ def remove_class(name = nil) self end - ### - # Remove the attribute named +name+ - def remove_attribute(name) - attr = attributes[name].remove if key? name - clear_xpath_context if Nokogiri.jruby? - attr - end - alias :delete :remove_attribute + alias :get_attribute :[] + alias :attr :[] + alias :set_attribute :[]= + alias :has_attribute? :key? + + # @!endgroup ### # Returns true if this Node matches +selector+ @@ -488,19 +553,6 @@ def parse(string_or_io, options = nil) node_set end - #### - # Set the Node's content to a Text node containing +string+. The string gets XML escaped, not interpreted as markup. - def content=(string) - self.native_content = encode_special_chars(string.to_s) - end - - ### - # Set the parent Node for this Node - def parent=(parent_node) - parent_node.add_child(self) - parent_node - end - ### # Returns a Hash of +{prefix => value}+ for all namespaces on this # node and its ancestors. @@ -628,37 +680,6 @@ def ancestors(selector = nil) }) end - ### - # Adds a default namespace supplied as a string +url+ href, to self. - # The consequence is as an xmlns attribute with supplied argument were - # present in parsed XML. A default namespace set with this method will - # now show up in #attributes, but when this node is serialized to XML an - # "xmlns" attribute will appear. See also #namespace and #namespace= - def default_namespace=(url) - add_namespace_definition(nil, url) - end - - alias :add_namespace :add_namespace_definition - - ### - # Set the default namespace on this node (as would be defined with an - # "xmlns=" attribute in XML source), as a Namespace object +ns+. Note that - # a Namespace added this way will NOT be serialized as an xmlns attribute - # for this node. You probably want #default_namespace= instead, or perhaps - # #add_namespace_definition with a nil prefix argument. - def namespace=(ns) - return set_namespace(ns) unless ns - - unless Nokogiri::XML::Namespace === ns - raise TypeError, "#{ns.class} can't be coerced into Nokogiri::XML::Namespace" - end - if ns.document != document - raise ArgumentError, "namespace must be declared on the same document" - end - - set_namespace ns - end - #### # Yields self and all children to +block+ recursively. def traverse(&block) @@ -680,6 +701,17 @@ def ==(other) pointer_id == other.pointer_id end + ### + # Compare two Node objects with respect to their Document. Nodes from + # different documents cannot be compared. + def <=>(other) + return nil unless other.is_a?(Nokogiri::XML::Node) + return nil unless document == other.document + compare other + end + + # @!group Serialization and Generating Output + ### # Serialize Node using +options+. Save options can also be set using a # block. See SaveOptions. @@ -809,29 +841,6 @@ def write_xml_to(io, options = {}) write_to io, options end - ### - # Compare two Node objects with respect to their Document. Nodes from - # different documents cannot be compared. - def <=>(other) - return nil unless other.is_a?(Nokogiri::XML::Node) - return nil unless document == other.document - compare other - end - - ### - # Do xinclude substitution on the subtree below node. If given a block, a - # Nokogiri::XML::ParseOptions object initialized from +options+, will be - # passed to it, allowing more convenient modification of the parser options. - def do_xinclude(options = XML::ParseOptions::DEFAULT_XML) - options = Nokogiri::XML::ParseOptions.new(options) if Integer === options - - # give options to user - yield options if block_given? - - # call c extension - process_xincludes(options.to_i) - end - def canonicalize(mode = XML::XML_C14N_1_0, inclusive_namespaces = nil, with_comments = false) c14n_root = self document.canonicalize(mode, inclusive_namespaces, with_comments) do |node, parent| @@ -840,6 +849,8 @@ def canonicalize(mode = XML::XML_C14N_1_0, inclusive_namespaces = nil, with_comm end end + # @!endgroup + private def add_sibling(next_or_previous, node_or_tags) diff --git a/lib/nokogiri/xml/searchable.rb b/lib/nokogiri/xml/searchable.rb index cc6d686c4a..064650256c 100644 --- a/lib/nokogiri/xml/searchable.rb +++ b/lib/nokogiri/xml/searchable.rb @@ -12,7 +12,9 @@ module Searchable # Regular expression used by Searchable#search to determine if a query # string is CSS or XPath LOOKS_LIKE_XPATH = /^(\.\/|\/|\.\.|\.$)/ - + + # @!group Searching via XPath or CSS Queries + ### # call-seq: search *paths, [namespace-bindings, xpath-variable-bindings, custom-handler-class] # @@ -168,6 +170,8 @@ def at_xpath(*args) xpath(*args).first end + # @!endgroup + private def css_internal(node, rules, handler, ns) From aef55269b310c84fcb8df6409688e6a5e836dd34 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Sun, 16 Feb 2020 11:37:05 -0500 Subject: [PATCH 3/6] move Node attribute methods into a separate test file --- test/xml/node/test_attribute_methods.rb | 179 ++++++++++++++++++++++++ test/xml/test_node.rb | 169 ---------------------- 2 files changed, 179 insertions(+), 169 deletions(-) create mode 100644 test/xml/node/test_attribute_methods.rb diff --git a/test/xml/node/test_attribute_methods.rb b/test/xml/node/test_attribute_methods.rb new file mode 100644 index 0000000000..eb7a392ce9 --- /dev/null +++ b/test/xml/node/test_attribute_methods.rb @@ -0,0 +1,179 @@ +require "helper" + +class Nokogiri::XML::Node + class TestAttributeMethods < Nokogiri::TestCase + def setup + super + @xml = Nokogiri::XML(File.read(XML_FILE), XML_FILE) + end + + def test_each + attributes = [] + @xml.xpath("//address")[1].each do |key, value| + attributes << [key, value] + end + assert_equal [["domestic", "Yes"], ["street", "Yes"]], attributes + end + + def test_remove_attribute + address = @xml.xpath("/staff/employee/address").first + assert_equal "Yes", address["domestic"] + attr = address.attributes["domestic"] + + returned_attr = address.remove_attribute "domestic" + assert_nil address["domestic"] + assert_equal attr, returned_attr + end + + def test_remove_attribute_when_not_found + address = @xml.xpath("/staff/employee/address").first + attr = address.remove_attribute "not-an-attribute" + assert_nil attr + end + + def test_attribute_setter_accepts_non_string + address = @xml.xpath("/staff/employee/address").first + assert_equal "Yes", address[:domestic] + address[:domestic] = "Altered Yes" + assert_equal "Altered Yes", address[:domestic] + end + + def test_attribute_accessor_accepts_non_string + address = @xml.xpath("/staff/employee/address").first + assert_equal "Yes", address["domestic"] + assert_equal "Yes", address[:domestic] + end + + def test_empty_attribute_reading + node = Nokogiri::XML '' + + assert_equal "", node.root["empty"] + assert_equal " ", node.root["whitespace"] + end + + def test_delete + address = @xml.xpath("/staff/employee/address").first + assert_equal "Yes", address["domestic"] + address.delete "domestic" + assert_nil address["domestic"] + end + + def test_attributes + assert node = @xml.search("//address").first + assert_nil(node["asdfasdfasdf"]) + assert_equal("Yes", node["domestic"]) + + assert node = @xml.search("//address")[2] + attr = node.attributes + assert_equal 2, attr.size + assert_equal "Yes", attr["domestic"].value + assert_equal "Yes", attr["domestic"].to_s + assert_equal "No", attr["street"].value + end + + def test_values + assert_equal %w{ Yes Yes }, @xml.xpath("//address")[1].values + end + + def test_value? + refute @xml.xpath("//address")[1].value?("no_such_value") + assert @xml.xpath("//address")[1].value?("Yes") + end + + def test_keys + assert_equal %w{ domestic street }, @xml.xpath("//address")[1].keys + end + + def test_attribute_with_symbol + assert_equal "Yes", @xml.css("address").first[:domestic] + end + + def test_non_existent_attribute_should_return_nil + node = @xml.root.first_element_child + assert_nil node.attribute("type") + end + + def test_classes + xml = Nokogiri::XML(<<-eoxml) +
+

test

+

test

+
+ eoxml + div = xml.at_xpath("//div") + p1, p2 = xml.xpath("//p") + + assert_equal [], div.classes + assert_equal %w[foo bar foo], p1.classes + assert_equal [], p2.classes + end + + def test_add_class + xml = Nokogiri::XML(<<-eoxml) +
+

test

+

test

+
+ eoxml + div = xml.at_xpath("//div") + p1, p2 = xml.xpath("//p") + + assert_same div, div.add_class("main") + assert_equal "main", div["class"] + + assert_same p1, p1.add_class("baz foo") + assert_equal "foo bar foo baz", p1["class"] + + assert_same p2, p2.add_class("foo baz foo") + assert_equal "foo baz foo", p2["class"] + end + + def test_append_class + xml = Nokogiri::XML(<<-eoxml) +
+

test

+

test

+
+ eoxml + div = xml.at_xpath("//div") + p1, p2 = xml.xpath("//p") + + assert_same div, div.append_class("main") + assert_equal "main", div["class"] + + assert_same p1, p1.append_class("baz foo") + assert_equal "foo bar foo baz foo", p1["class"] + + assert_same p2, p2.append_class("foo baz foo") + assert_equal "foo baz foo", p2["class"] + end + + def test_remove_class + xml = Nokogiri::XML(<<-eoxml) +
+

test

+

test

+

test

+

test

+
+ eoxml + div = xml.at_xpath("//div") + p1, p2, p3, p4 = xml.xpath("//p") + + assert_same div, div.remove_class("main") + assert_nil div["class"] + + assert_same p1, p1.remove_class("bar baz") + assert_equal "foo foo", p1["class"] + + assert_same p2, p2.remove_class() + assert_nil p2["class"] + + assert_same p3, p3.remove_class("foo") + assert_nil p3["class"] + + assert_same p4, p4.remove_class("foo") + assert_nil p4["class"] + end + end +end diff --git a/test/xml/test_node.rb b/test/xml/test_node.rb index b9dabe43ec..efb37e753c 100644 --- a/test/xml/test_node.rb +++ b/test/xml/test_node.rb @@ -579,15 +579,6 @@ def test_write_to assert_equal @xml.to_xml, io.read end - def test_attribute_with_symbol - assert_equal 'Yes', @xml.css('address').first[:domestic] - end - - def test_non_existent_attribute_should_return_nil - node = @xml.root.first_element_child - assert_nil node.attribute('type') - end - def test_write_to_with_block called = false io = StringIO.new @@ -640,27 +631,6 @@ def test_hold_refence_to_subnode assert_equal 'b', node_b.name end - def test_values - assert_equal %w{ Yes Yes }, @xml.xpath('//address')[1].values - end - - def test_value? - refute @xml.xpath('//address')[1].value?('no_such_value') - assert @xml.xpath('//address')[1].value?('Yes') - end - - def test_keys - assert_equal %w{ domestic street }, @xml.xpath('//address')[1].keys - end - - def test_each - attributes = [] - @xml.xpath('//address')[1].each do |key, value| - attributes << [key, value] - end - assert_equal [['domestic', 'Yes'], ['street', 'Yes']], attributes - end - def test_new assert node = Nokogiri::XML::Node.new('input', @xml) assert_equal 1, node.node_type @@ -687,132 +657,6 @@ def test_read_only? assert entity_decl.read_only? end - def test_remove_attribute - address = @xml.xpath('/staff/employee/address').first - assert_equal 'Yes', address['domestic'] - attr = address.attributes['domestic'] - - returned_attr = address.remove_attribute 'domestic' - assert_nil address['domestic'] - assert_equal attr, returned_attr - end - - def test_remove_attribute_when_not_found - address = @xml.xpath('/staff/employee/address').first - attr = address.remove_attribute 'not-an-attribute' - assert_nil attr - end - - def test_attribute_setter_accepts_non_string - address = @xml.xpath("/staff/employee/address").first - assert_equal "Yes", address[:domestic] - address[:domestic] = "Altered Yes" - assert_equal "Altered Yes", address[:domestic] - end - - def test_attribute_accessor_accepts_non_string - address = @xml.xpath("/staff/employee/address").first - assert_equal "Yes", address["domestic"] - assert_equal "Yes", address[:domestic] - end - - def test_empty_attribute_reading - node = Nokogiri::XML '' - - assert_equal '', node.root['empty'] - assert_equal ' ', node.root['whitespace'] - end - - def test_delete - address = @xml.xpath('/staff/employee/address').first - assert_equal 'Yes', address['domestic'] - address.delete 'domestic' - assert_nil address['domestic'] - end - - def test_classes - xml = Nokogiri::XML(<<-eoxml) -
-

test

-

test

-
- eoxml - div = xml.at_xpath('//div') - p1, p2 = xml.xpath('//p') - - assert_equal [], div.classes - assert_equal %w[foo bar foo], p1.classes - assert_equal [], p2.classes - end - - def test_add_class - xml = Nokogiri::XML(<<-eoxml) -
-

test

-

test

-
- eoxml - div = xml.at_xpath('//div') - p1, p2 = xml.xpath('//p') - - assert_same div, div.add_class('main') - assert_equal 'main', div['class'] - - assert_same p1, p1.add_class('baz foo') - assert_equal 'foo bar foo baz', p1['class'] - - assert_same p2, p2.add_class('foo baz foo') - assert_equal 'foo baz foo', p2['class'] - end - - def test_append_class - xml = Nokogiri::XML(<<-eoxml) -
-

test

-

test

-
- eoxml - div = xml.at_xpath('//div') - p1, p2 = xml.xpath('//p') - - assert_same div, div.append_class('main') - assert_equal 'main', div['class'] - - assert_same p1, p1.append_class('baz foo') - assert_equal 'foo bar foo baz foo', p1['class'] - - assert_same p2, p2.append_class('foo baz foo') - assert_equal 'foo baz foo', p2['class'] - end - - def test_remove_class - xml = Nokogiri::XML(<<-eoxml) -
-

test

-

test

-

test

-

test

-
- eoxml - div = xml.at_xpath('//div') - p1, p2, p3, p4 = xml.xpath('//p') - - assert_same div, div.remove_class('main') - assert_nil div['class'] - - assert_same p1, p1.remove_class('bar baz') - assert_equal 'foo foo', p1['class'] - - assert_same p2, p2.remove_class() - assert_nil p2['class'] - - assert_same p3, p3.remove_class('foo') - assert_nil p3['class'] - - assert_same p4, p4.remove_class('foo') - assert_nil p4['class'] - end - def test_set_content_with_symbol node = @xml.at('//name') node.content = :foo @@ -919,19 +763,6 @@ def test_set_property_non_string assert_equal('false', node['foo']) end - def test_attributes - assert node = @xml.search('//address').first - assert_nil(node['asdfasdfasdf']) - assert_equal('Yes', node['domestic']) - - assert node = @xml.search('//address')[2] - attr = node.attributes - assert_equal 2, attr.size - assert_equal 'Yes', attr['domestic'].value - assert_equal 'Yes', attr['domestic'].to_s - assert_equal 'No', attr['street'].value - end - def test_path assert set = @xml.search('//employee') assert node = set.first From dcc104cfeca66b74883cf2b375298f46cb369df7 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 25 Feb 2020 07:55:43 -0500 Subject: [PATCH 4/6] Node: new #kwattr methods - #kwattr_values - #kwattr_add - #kwattr_append - #kwattr_remove and rewrite CSS class convenience methods using their #kwattr analog. related to https://github.com/flavorjones/loofah/issues/173 --- lib/nokogiri/xml/node.rb | 63 +++++++--- test/xml/node/test_attribute_methods.rb | 146 ++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 14 deletions(-) diff --git a/lib/nokogiri/xml/node.rb b/lib/nokogiri/xml/node.rb index d512823a0e..d1eef2a9ea 100644 --- a/lib/nokogiri/xml/node.rb +++ b/lib/nokogiri/xml/node.rb @@ -439,7 +439,7 @@ def remove_attribute(name) # Get the list of class names of this Node, without # deduplication or sorting. def classes - self["class"].to_s.scan(/\S+/) + kwattr_values("class") end ### @@ -451,9 +451,7 @@ def classes # More than one class may be added at a time, separated by a # space. def add_class(name) - names = classes - self["class"] = (names + (name.scan(/\S+/) - names)).join(" ") - self + kwattr_add("class", name) end ### @@ -465,8 +463,7 @@ def add_class(name) # More than one class may be appended at a time, separated by a # space. def append_class(name) - self["class"] = (classes + name.scan(/\S+/)).join(" ") - self + kwattr_append("class", name) end ### @@ -480,15 +477,42 @@ def append_class(name) # If no class name is left after removal, or when +name+ is nil, # the "class" attribute is removed from this Node. def remove_class(name = nil) - if name - names = classes - name.scan(/\S+/) - if names.empty? - delete "class" - else - self["class"] = names.join(" ") - end + kwattr_remove("class", name) + end + + def kwattr_values(attribute_name) + keywordify(get_attribute(attribute_name) || []) + end + + def kwattr_add(attribute_name, keywords) + keywords = keywordify(keywords) + current_kws = kwattr_values(attribute_name) + new_kws = (current_kws + (keywords - current_kws)).join(" ") + set_attribute(attribute_name, new_kws) + self + end + + def kwattr_append(attribute_name, keywords) + keywords = keywordify(keywords) + current_kws = kwattr_values(attribute_name) + new_kws = (current_kws + keywords).join(" ") + set_attribute(attribute_name, new_kws) + self + end + + def kwattr_remove(attribute_name, keywords) + if keywords.nil? + remove_attribute(attribute_name) + return self + end + + keywords = keywordify(keywords) + current_kws = kwattr_values(attribute_name) + new_kws = current_kws - keywords + if new_kws.empty? + remove_attribute(attribute_name) else - delete "class" + set_attribute(attribute_name, new_kws.join(" ")) end self end @@ -853,6 +877,17 @@ def canonicalize(mode = XML::XML_C14N_1_0, inclusive_namespaces = nil, with_comm private + def keywordify(keywords) + case keywords + when Enumerable + return keywords + when String + return keywords.scan(/\S+/) + else + raise ArgumentError.new("Keyword attributes must be passed as either a String or an Enumerable, but received #{keywords.class}") + end + end + def add_sibling(next_or_previous, node_or_tags) impl = (next_or_previous == :next) ? :add_next_sibling_node : :add_previous_sibling_node iter = (next_or_previous == :next) ? :reverse_each : :each diff --git a/test/xml/node/test_attribute_methods.rb b/test/xml/node/test_attribute_methods.rb index eb7a392ce9..b7cbd5ff20 100644 --- a/test/xml/node/test_attribute_methods.rb +++ b/test/xml/node/test_attribute_methods.rb @@ -93,6 +93,9 @@ def test_non_existent_attribute_should_return_nil assert_nil node.attribute("type") end + # + # CSS classes, specifically + # def test_classes xml = Nokogiri::XML(<<-eoxml)
@@ -175,5 +178,148 @@ def test_remove_class assert_same p4, p4.remove_class("foo") assert_nil p4["class"] end + + # + # keyword attributes, generally + # + describe "keyword attribute helpers" do + let(:node) do + Nokogiri::XML::DocumentFragment.parse(<<~EOM).at_css("div") +
hello
+ EOM + end + + describe "setup" do + it { _(node.get_attribute("noob")).must_be_nil } + end + + describe "#kwattr_values" do + it "returns an array of space-delimited values" do + _(node.kwattr_values("blargh")).must_equal(%w[foo bar baz bar foo quux foo manx]) + end + + describe "when no attribute exists" do + it "returns an empty array" do + _(node.kwattr_values("noob")).must_equal([]) + end + end + + describe "when an empty attribute exists" do + it "returns an empty array" do + node.set_attribute("noob", "") + _(node.kwattr_values("noob")).must_equal([]) + + node.set_attribute("noob", " ") + _(node.kwattr_values("noob")).must_equal([]) + end + end + end + + describe "kwattr_add" do + it "returns the node for chaining" do + _(node.kwattr_add("noob", "asdf")).must_be_same_as(node) + end + + it "creates a new attribute when necessary" do + _(node.kwattr_add("noob", "asdf").get_attribute("noob")).wont_be_nil + end + + it "adds a new bare keyword string" do + _(node.kwattr_add("blargh", "jimmy").kwattr_values("blargh")). + must_equal(%w[foo bar baz bar foo quux foo manx jimmy]) + end + + it "does not add a repeated bare keyword string" do + _(node.kwattr_add("blargh", "foo").kwattr_values("blargh")). + must_equal(%w[foo bar baz bar foo quux foo manx]) + end + + describe "given a string of keywords" do + it "adds new keywords and ignores existing keywords" do + _(node.kwattr_add("blargh", "foo jimmy\tjohnny").kwattr_values("blargh")). + must_equal(%w[foo bar baz bar foo quux foo manx jimmy johnny]) + end + end + + describe "given an array of keywords" do + it "adds new keywords and ignores existing keywords" do + _(node.kwattr_add("blargh", %w[foo jimmy]).kwattr_values("blargh")). + must_equal(%w[foo bar baz bar foo quux foo manx jimmy]) + end + end + end + + describe "kwattr_append" do + it "returns the node for chaining" do + _(node.kwattr_append("noob", "asdf")).must_be_same_as(node) + end + + it "creates a new attribute when necessary" do + _(node.kwattr_append("noob", "asdf").get_attribute("noob")).wont_be_nil + end + + it "adds a new bare keyword string" do + _(node.kwattr_append("blargh", "jimmy").kwattr_values("blargh")). + must_equal(%w[foo bar baz bar foo quux foo manx jimmy]) + end + + it "adds a repeated bare keyword string" do + _(node.kwattr_append("blargh", "foo").kwattr_values("blargh")). + must_equal(%w[foo bar baz bar foo quux foo manx foo]) + end + + describe "given a string of keywords" do + it "adds new keywords and existing keywords" do + _(node.kwattr_append("blargh", "foo jimmy\tjohnny").kwattr_values("blargh")). + must_equal(%w[foo bar baz bar foo quux foo manx foo jimmy johnny]) + end + end + + describe "given an array of keywords" do + it "adds new keywords and existing keywords" do + _(node.kwattr_append("blargh", %w[foo jimmy]).kwattr_values("blargh")). + must_equal(%w[foo bar baz bar foo quux foo manx foo jimmy]) + end + end + end + + describe "kwattr_remove" do + it "returns the node for chaining" do + _(node.kwattr_remove("noob", "asdf")).must_be_same_as(node) + end + + it "gracefully handles a non-existent attribute" do + _(node.kwattr_remove("noob", "asdf").get_attribute("noob")).must_be_nil + end + + it "removes an existing bare keyword string" do + _(node.kwattr_remove("blargh", "foo").kwattr_values("blargh")). + must_equal(%w[bar baz bar quux manx]) + end + + it "gracefully ignores a non-existent bare keyword string" do + _(node.kwattr_remove("blargh", "jimmy").kwattr_values("blargh")). + must_equal(%w[foo bar baz bar foo quux foo manx]) + end + + describe "given a string of keywords" do + it "removes existing keywords and ignores other keywords" do + _(node.kwattr_remove("blargh", "foo jimmy\tjohnny").kwattr_values("blargh")). + must_equal(%w[bar baz bar quux manx]) + end + end + + describe "given an array of keywords" do + it "adds new keywords and existing keywords" do + _(node.kwattr_remove("blargh", %w[foo jimmy]).kwattr_values("blargh")). + must_equal(%w[bar baz bar quux manx]) + end + end + + it "removes the attribute when no values are left" do + _(node.kwattr_remove("blargh", %w[foo bar baz bar foo quux foo manx]).get_attribute("blargh")).must_be_nil + end + end + end end end From 39b7384e7898a224cfa921cab7e7067dad180a98 Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 25 Feb 2020 08:45:21 -0500 Subject: [PATCH 5/6] docs: improve Node CSS class convenience methods - Node#classes - Node#add_class - Node#append_class - Node#remove_class --- lib/nokogiri/xml/node.rb | 158 +++++++++++++++++++++++++++++++-------- 1 file changed, 127 insertions(+), 31 deletions(-) diff --git a/lib/nokogiri/xml/node.rb b/lib/nokogiri/xml/node.rb index d1eef2a9ea..f61e985ef0 100644 --- a/lib/nokogiri/xml/node.rb +++ b/lib/nokogiri/xml/node.rb @@ -435,49 +435,145 @@ def remove_attribute(name) attr end - ### - # Get the list of class names of this Node, without - # deduplication or sorting. + # Get the CSS class names of a Node. + # + # This is a convenience function and is equivalent to: + # node.kwattr_values("class") + # + # @see #kwattr_values + # @see #add_class + # @see #append_class + # @see #remove_class + # + # @return [Array] + # + # The CSS classes present in the Node's +class+ attribute. If + # the attribute is empty or non-existent, the return value is + # an empty array. + # + # @example + # node # =>
+ # node.classes # => ["section", "title", "header"] + # def classes kwattr_values("class") end - ### - # Add +name+ to the "class" attribute value of this Node and - # return self. If the value is already in the current value, it - # is not added. If no "class" attribute exists yet, one is - # created with the given value. + # Ensure HTML CSS classes are present on a +Node+. Any CSS + # classes in +names+ that already exist in the +Node+'s +class+ + # attribute are _not_ added. Note that any existing duplicates + # in the +class+ attribute are not removed. Compare with + # {#append_class}. + # + # This is a convenience function and is equivalent to: + # node.kwattr_add("class", names) + # + # @see #kwattr_add + # @see #classes + # @see #append_class + # @see #remove_class + # + # @param names [String, Array] + # + # CSS class names to be added to the Node's +class+ + # attribute. May be a string containing whitespace-delimited + # names, or an Array of String names. Any class names already + # present will not be added. Any class names not present will + # be added. If no +class+ attribute exists, one is created. + # + # @return [Node] Returns +self+ for ease of chaining method calls. + # + # @example Ensure that a +Node+ has CSS class "section" + # node # =>
+ # node.add_class("section") # =>
+ # node.add_class("section") # =>
# duplicate not added + # + # @example Ensure that a +Node+ has CSS classes "section" and "header", via a String argument. + # node # =>
+ # node.add_class("section header") # =>
+ # # Note that the CSS class "section" is not added because it is already present. + # # Note also that the pre-existing duplicate CSS class "section" is not removed. + # + # @example Ensure that a +Node+ has CSS classes "section" and "header", via an Array argument. + # node # =>
+ # node.add_class(["section", "header"]) # =>
# - # More than one class may be added at a time, separated by a - # space. - def add_class(name) - kwattr_add("class", name) + def add_class(names) + kwattr_add("class", names) end - ### - # Append +name+ to the "class" attribute value of this Node and - # return self. The value is simply appended without checking if - # it is already in the current value. If no "class" attribute - # exists yet, one is created with the given value. + # Add HTML CSS classes to a +Node+, regardless of + # duplication. Compare with {#add_class}. + # + # This is a convenience function and is equivalent to: + # node.kwattr_append("class", names) + # + # @see #kwattr_append + # @see #classes + # @see #add_class + # @see #remove_class + # + # @param names [String, Array] + # + # CSS class names to be appended to the Node's +class+ + # attribute. May be a string containing whitespace-delimited + # names, or an Array of String names. All class names passed + # in will be appended to the +class+ attribute even if they + # are already present in the attribute value. If no +class+ + # attribute exists, one is created. + # + # @return [Node] Returns +self+ for ease of chaining method calls. + # + # @example Append "section" to a +Node+'s CSS +class+ attriubute + # node # =>
+ # node.append_class("section") # =>
+ # node.append_class("section") # =>
# duplicate added! + # + # @example Append "section" and "header" to a +Node+'s CSS +class+ attribute, via a String argument. + # node # =>
+ # node.append_class("section header") # =>
+ # # Note that the CSS class "section" is appended even though it is already present. # - # More than one class may be appended at a time, separated by a - # space. - def append_class(name) - kwattr_append("class", name) + # @example Append "section" and "header" to a +Node+'s CSS +class+ attribute, via an Array argument. + # node # =>
+ # node.append_class(["section", "header"]) # =>
+ # node.append_class(["section", "header"]) # =>
+ # + def append_class(names) + kwattr_append("class", names) end - ### - # Remove +name+ from the "class" attribute value of this Node - # and return self. If there are many occurrences of the name, - # they are all removed. + # Remove HTML CSS classes from a +Node+. Any CSS classes in +names+ that + # exist in the +Node+'s +class+ attribute are removed, including any + # multiple entries. + # + # If no CSS classes remain after this operation, or if +names+ is + # +nil+, the +class+ attribute is deleted from the node. + # + # This is a convenience function and is equivalent to: + # node.kwattr_remove("class", names) + # + # @see #kwattr_remove + # @see #classes + # @see #add_class + # @see #append_class + # + # @param names [String, Array] + # + # CSS class names to be removed from the Node's +class+ attribute. May + # be a string containing whitespace-delimited names, or an Array of + # String names. Any class names already present will be removed. If no + # CSS classes remain, the +class+ attribute is deleted. + # + # @return [Node] Returns +self+ for ease of chaining method calls. # - # More than one class may be removed at a time, separated by a - # space. + # @example + # node # =>
+ # node.remove_class("section") # =>
+ # node.remove_class("header") # =>
# attribute is deleted when empty # - # If no class name is left after removal, or when +name+ is nil, - # the "class" attribute is removed from this Node. - def remove_class(name = nil) - kwattr_remove("class", name) + def remove_class(names = nil) + kwattr_remove("class", names) end def kwattr_values(attribute_name) From 66649ee762303fc29968fe435711a9b002f0b2bd Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Wed, 26 Feb 2020 11:23:36 -0500 Subject: [PATCH 6/6] docs: document Node kwattr methods --- lib/nokogiri/xml/node.rb | 149 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/lib/nokogiri/xml/node.rb b/lib/nokogiri/xml/node.rb index f61e985ef0..1df0680e62 100644 --- a/lib/nokogiri/xml/node.rb +++ b/lib/nokogiri/xml/node.rb @@ -576,10 +576,81 @@ def remove_class(names = nil) kwattr_remove("class", names) end + # Retrieve values from a keyword attribute of a Node. + # + # A "keyword attribute" is a node attribute that contains a set + # of space-delimited values. Perhaps the most familiar example + # of this is the HTML +class+ attribute used to contain CSS + # classes. But other keyword attributes exist, for instance + # [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel). + # + # @see #classes + # @see #kwattr_add + # @see #kwattr_append + # @see #kwattr_remove + # + # @param attribute_name [String] The name of the keyword attribute to be inspected. + # + # @return [Array] + # + # The values present in the Node's +attribute_name+ + # attribute. If the attribute is empty or non-existent, the + # return value is an empty array. + # + # @example + # node # => link + # node.kwattr_values("rel") # => ["nofollow", "noopener", "external"] + # def kwattr_values(attribute_name) keywordify(get_attribute(attribute_name) || []) end + # Ensure that values are present in a keyword attribute. + # + # Any values in +keywords+ that already exist in the +Node+'s + # attribute values are _not_ added. Note that any existing + # duplicates in the attribute values are not removed. Compare + # with {#kwattr_append}. + # + # A "keyword attribute" is a node attribute that contains a set + # of space-delimited values. Perhaps the most familiar example + # of this is the HTML +class+ attribute used to contain CSS + # classes. But other keyword attributes exist, for instance + # [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel). + # + # @see #add_class + # @see #kwattr_values + # @see #kwattr_append + # @see #kwattr_remove + # + # @param attribute_name [String] The name of the keyword attribute to be modified. + # + # @param keywords [String, Array] + # + # Keywords to be added to the attribute named + # +attribute_name+. May be a string containing + # whitespace-delimited values, or an Array of String + # values. Any values already present will not be added. Any + # values not present will be added. If the named attribute + # does not exist, it is created. + # + # @return [Node] Returns +self+ for ease of chaining method calls. + # + # @example Ensure that a +Node+ has "nofollow" in its +rel+ attribute. + # node # => + # node.kwattr_add("rel", "nofollow") # => + # node.kwattr_add("rel", "nofollow") # => # duplicate not added + # + # @example Ensure that a +Node+ has "nofollow" and "noreferrer" in its +rel+ attribute, via a String argument. + # node # => + # node.kwattr_add("rel", "nofollow noreferrer") # => + # # Note that "nofollow" is not added because it is already present. + # # Note also that the pre-existing duplicate "nofollow" is not removed. + # + # @example Ensure that a +Node+ has "nofollow" and "noreferrer" in its +rel+ attribute, via an Array argument. + # node # => + # node.kwattr_add("rel", ["nofollow", "noreferrer"]) # => + # def kwattr_add(attribute_name, keywords) keywords = keywordify(keywords) current_kws = kwattr_values(attribute_name) @@ -588,6 +659,48 @@ def kwattr_add(attribute_name, keywords) self end + # Add keywords to a Node's keyword attribute, regardless of + # duplication. Compare with {#kwattr_add}. + # + # A "keyword attribute" is a node attribute that contains a set + # of space-delimited values. Perhaps the most familiar example + # of this is the HTML +class+ attribute used to contain CSS + # classes. But other keyword attributes exist, for instance + # [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel). + # + # @see #append_class + # @see #kwattr_values + # @see #kwattr_add + # @see #kwattr_remove + # + # @param attribute_name [String] The name of the keyword attribute to be modified. + # + # @param keywords [String, Array] + # + # Keywords to be added to the attribute named + # +attribute_name+. May be a string containing + # whitespace-delimited values, or an Array of String + # values. All values passed in will be appended to the named + # attribute even if they are already present in the + # attribute. If the named attribute does not exist, it is + # created. + # + # @return [Node] Returns +self+ for ease of chaining method calls. + # + # @example Append "nofollow" to the +rel+ attribute. + # node # => + # node.kwattr_append("rel", "nofollow") # => + # node.kwattr_append("rel", "nofollow") # => # duplicate added! + # + # @example Append "nofollow" and "noreferrer" to the +rel+ attribute, via a String argument. + # node # => + # node.kwattr_append("rel", "nofollow noreferrer") # => + # # Note that "nofollow" is appended even though it is already present. + # + # @example Append "nofollow" and "noreferrer" to the +rel+ attribute, via an Array argument. + # node # => + # node.kwattr_append("rel", ["nofollow", "noreferrer"]) # => + # def kwattr_append(attribute_name, keywords) keywords = keywordify(keywords) current_kws = kwattr_values(attribute_name) @@ -596,6 +709,42 @@ def kwattr_append(attribute_name, keywords) self end + # Remove keywords from a keyword attribute. Any matching + # keywords that exist in the named attribute are removed, + # including any multiple entries. + # + # If no keywords remain after this operation, or if +keywords+ + # is +nil+, the attribute is deleted from the node. + # + # A "keyword attribute" is a node attribute that contains a set + # of space-delimited values. Perhaps the most familiar example + # of this is the HTML +class+ attribute used to contain CSS + # classes. But other keyword attributes exist, for instance + # [`rel`](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel). + # + # @see #remove_class + # @see #kwattr_values + # @see #kwattr_add + # @see #kwattr_append + # + # @param attribute_name [String] The name of the keyword attribute to be modified. + # + # @param keywords [String, Array] + # + # Keywords to be removed from the attribute named + # +attribute_name+. May be a string containing + # whitespace-delimited values, or an Array of String + # values. Any keywords present in the named attribute will be + # removed. If no keywords remain, or if +keywords+ is nil, the + # attribute is deleted. + # + # @return [Node] Returns +self+ for ease of chaining method calls. + # + # @example + # node # => link + # node.kwattr_remove("rel", "nofollow") # => link + # node.kwattr_remove("rel", "noreferrer") # => link # attribute is deleted when empty + # def kwattr_remove(attribute_name, keywords) if keywords.nil? remove_attribute(attribute_name)