class Kitchen::ElementBase

Abstract base class for all elements. If you are looking for a simple concrete element class, use `Element`.

Attributes

ancestors[R]

Returns the element's ancestors

@return [Array<Ancestor>]

document[R]

The element's document @return [Document]

enumerator_class[R]

The enumerator class for this element @return [Class]

is_a_clone[RW]

If this element is a clone @return [Boolean]

node[RW]

The wrapped Nokogiri node @return [Nokogiri::XML::Node] the node

search_query_that_found_me[RW]

The search query that located this element in the DOM @return [SearchQuery]

short_type[R]

The element's type, e.g. :page @return [Symbol, String]

Public Class Methods

descendant(type) click to toggle source

Returns ElementBase descendent type or nil if none found

@param type [Symbol] the descendant type, e.g. `:page` @return [Class] the child class for the given type

# File lib/kitchen/element_base.rb, line 148
def self.descendant(type)
  @types_to_descendants ||=
    descendants.each_with_object({}) do |descendant, hash|
      next unless descendant.try(:short_type)

      hash[descendant.short_type] = descendant
    end

  @types_to_descendants[type]
end
descendant!(type) click to toggle source

Returns ElementBase descendent type or Error if none found

@param type [Symbol] the descendant type, e.g. `:page` @raise if the type is unknown @return [Class] the child class for the given type

# File lib/kitchen/element_base.rb, line 165
def self.descendant!(type)
  descendant(type) || raise("Unknown ElementBase descendant type '#{type}'")
end
is_the_element_class_for?(node, config:) click to toggle source

Returns true if this class represents the element for the given node

@param node [Nokogiri::XML::Node] the underlying node @param config [Kitchen::Config] @return [Boolean]

# File lib/kitchen/element_base.rb, line 185
def self.is_the_element_class_for?(node, config:)
  Selector.named(short_type).matches?(node, config: config)
end
new(node:, document:, enumerator_class:, short_type: nil) click to toggle source

Creates a new instance

@param node [Nokogiri::XML::Node] the wrapped element @param document [Document] the element's document @param enumerator_class [ElementEnumeratorBase] the enumerator that matches this element type @param short_type [Symbol, String] the type of this element

# File lib/kitchen/element_base.rb, line 116
def initialize(node:, document:, enumerator_class:, short_type: nil)
  raise(ArgumentError, 'node cannot be nil') if node.nil?

  @node = node

  raise(ArgumentError, 'enumerator_class cannot be nil') if enumerator_class.nil?

  @enumerator_class = enumerator_class

  @short_type = short_type ||
                self.class.try(:short_type) ||
                "unknown_type_#{SecureRandom.hex(4)}"

  @document =
    case document
    when Kitchen::Document
      document
    else
      raise(ArgumentError, '`document` is not a known document type')
    end

  @ancestors = HashWithIndifferentAccess.new
  @search_query_matches_that_have_been_counted = {}
  @is_a_clone = false
  @search_cache = {}
end

Public Instance Methods

add_ancestor(ancestor) click to toggle source

Adds one ancestor, incrementing its descendant counts for this element type

@param ancestor [Ancestor] @raise [StandardError] if there is already an ancestor with the given ancestor's type

# File lib/kitchen/element_base.rb, line 283
def add_ancestor(ancestor)
  if @ancestors[ancestor.type].present?
    raise "Trying to add an ancestor of type '#{ancestor.type}' but one of that " \
          "type is already present"
  end

  ancestor.increment_descendant_count(short_type)
  @ancestors[ancestor.type] = ancestor
end
add_ancestors(*args) click to toggle source

Adds ancestors to this element, for each incrementing descendant counts for this type

@param args [Array<Hash, Ancestor, Element, Document>] the ancestors @raise [StandardError] if there is already an ancestor with the one of

the given ancestors' types
# File lib/kitchen/element_base.rb, line 263
def add_ancestors(*args)
  args.each do |arg|
    case arg
    when Hash
      add_ancestors(*arg.values)
    when Ancestor
      add_ancestor(arg)
    when Element, Document
      add_ancestor(Ancestor.new(arg))
    else
      raise "Unsupported ancestor type `#{arg.class}`"
    end
  end
end
ancestor(type) click to toggle source

Returns this element's ancestor of the given type

@param type [String, Symbol] e.g. :page, :term @return [Ancestor] @raise [StandardError] if there is no ancestor of the given type

# File lib/kitchen/element_base.rb, line 238
def ancestor(type)
  @ancestors[type.to_sym]&.element || raise("No ancestor of type '#{type}'")
end
ancestor_elements() click to toggle source

Return the elements in all of the ancestors

@return [Array<ElementBase>]

# File lib/kitchen/element_base.rb, line 297
def ancestor_elements
  @ancestors.values.map(&:element)
end
append(child: nil, sibling: nil) click to toggle source

If child argument given, appends it after the element's current children. If sibling is given, appends it as a sibling to this element.

@param child [String] the child to append @param sibling [String] the sibling to append @raise [RecipeError] if specify other than just a child or a sibling

# File lib/kitchen/element_base.rb, line 561
def append(child: nil, sibling: nil)
  require_one_of_child_or_sibling(child, sibling)

  if child
    if node.children.empty?
      node.children = child.to_s
    else
      node.add_child(child)
    end
  else
    node.next = sibling
  end

  self
end
as_enumerator() click to toggle source

Returns this element as an enumerator (over only one element, itself)

@return [ElementEnumeratorBase] (actually returns the appropriate enumerator class for this element)

# File lib/kitchen/element_base.rb, line 771
def as_enumerator
  enumerator_class.new(search_query: search_query_that_found_me) { |block| block.yield(self) }
end
at(*selector_or_xpath_args, reload: false)

@!method at

@see first
Alias for: first
clone() click to toggle source

Returns a clone of this object

Calls superclass method
# File lib/kitchen/element_base.rb, line 702
def clone
  super.tap do |element|
    # When we call dup, the dup gets a bunch of default namespace stuff that
    # the original doesn't have.  Why? Unclear, but hard to get rid of nicely.
    # So here we mark that the element is a clone and then all of the `to_s`-like
    # methods gsub out the default namespace gunk.  Clones are mostly used for
    # clipboards and are accessed using `paste` methods, so modifying the `to_s`
    # behavior works for us.  If we end up using `clone` in a way that doesn't
    # eventually get converted to string, we may have to investigate other
    # options.
    #
    # An alternative is to remove the `xmlns` attribute in the `html` tag before
    # the input file is parse into a Nokogiri document and then to add it back
    # in when the baked file is written out.
    #
    # Nokogiri::XML::Document.remove_namespaces! is not an option because that blows
    # away our MathML namespace.
    #
    # I may not fully understand why the extra default namespace stuff is happening
    # FWIW :-)
    #
    element.node = node.dup
    element.is_a_clone = true
  end
end
contains?(*selector_or_xpath_args) click to toggle source

Returns true if this element has a child matching the provided selector

@param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments @return [Boolean]

# File lib/kitchen/element_base.rb, line 632
def contains?(*selector_or_xpath_args)
  !node.at(*selector_or_xpath_args).nil?
end
content(*selector_or_xpath_args) click to toggle source

Get the content of children matching the provided selector. Mostly useful when there is one child with text you want to extract.

@param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments @return [String]

# File lib/kitchen/element_base.rb, line 623
def content(*selector_or_xpath_args)
  node.search(*selector_or_xpath_args).children.to_s
end
copied_id() click to toggle source

Copy the element's id

# File lib/kitchen/element_base.rb, line 498
def copied_id
  id_tracker.record_id_copied(id)
  id_tracker.modified_id_to_paste(id)
end
copy(to: nil) click to toggle source

Makes a copy of the element and places it on the specified clipboard.

@param to [Symbol, String, Clipboard, nil] the name of the clipboard (or a Clipboard

object) to cut to.  String values are converted to symbols.  If not provided, the
copy is not placed on a clipboard.

@return [Element] the copied element

# File lib/kitchen/element_base.rb, line 466
def copy(to: nil)
  # See `clone` method for a note about namespaces
  block_error_if(block_given?)

  the_copy = clone
  the_copy.raw.traverse do |node|
    next if node.text? || node.document?

    id_tracker.record_id_copied(node[:id])
  end
  get_clipboard(to).add(the_copy) if to.present?
  the_copy
end
count_in(ancestor_type) click to toggle source

Returns the count of this element's type in the given ancestor type

@param ancestor_type [String, Symbol]

# File lib/kitchen/element_base.rb, line 305
def count_in(ancestor_type)
  @ancestors[ancestor_type]&.get_descendant_count(short_type) ||
    raise("No ancestor of type '#{ancestor_type}'")
end
cut(to: nil) click to toggle source

Removes the element from its parent and places it on the specified clipboard

@param to [Symbol, String, Clipboard, nil] the name of the clipboard (or a Clipboard

object) to cut to. String values are converted to symbols. If not provided, the
element is not placed on a clipboard.

@return [Element] the cut element

# File lib/kitchen/element_base.rb, line 446
def cut(to: nil)
  block_error_if(block_given?)

  raw.traverse do |node|
    next if node.text? || node.document?

    id_tracker.record_id_cut(node[:id])
  end
  node.remove
  get_clipboard(to).add(self) if to.present?
  self
end
element_children() click to toggle source

Returns an enumerator over the direct child elements of this element, with the specific type (e.g. TermElement) if such type is available.

@return [TypeCastingElementEnumerator]

# File lib/kitchen/element_base.rb, line 414
def element_children
  block_error_if(block_given?)
  TypeCastingElementEnumerator.factory.build_within(
    self,
    search_query: SearchQuery.new(css_or_xpath: './*')
  )
end
first(*selector_or_xpath_args, reload: false) { |element| ... } click to toggle source

Yields and returns the first child element that matches the provided selector or XPath arguments.

@param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments @param reload [Boolean] ignores cache if true @yieldparam [Element] the matched XML element @return [Element, nil] the matched XML element or nil if no match found

# File lib/kitchen/element_base.rb, line 384
def first(*selector_or_xpath_args, reload: false)
  search(*selector_or_xpath_args, reload: reload).first.tap do |element|
    yield(element) if block_given?
  end
end
Also aliased as: at
first!(*selector_or_xpath_args, reload: false) { |element| ... } click to toggle source

Yields and returns the first child element that matches the provided selector or XPath arguments.

@param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments @param reload [Boolean] ignores cache if true @yieldparam [Element] the matched XML element @raise [ElementNotFoundError] if no matching element is found @return [Element] the matched XML element

# File lib/kitchen/element_base.rb, line 399
def first!(*selector_or_xpath_args, reload: false)
  search(*selector_or_xpath_args, reload: reload).first!.tap do |element|
    yield(element) if block_given?
  end
end
has_ancestor?(type) click to toggle source

Returns true iff this element has an ancestor of the given type

@param type [String, Symbol] e.g. :page, :term @return [Boolean]

# File lib/kitchen/element_base.rb, line 247
def has_ancestor?(type)
  @ancestors[type.to_sym].present?
end
has_class?(klass) click to toggle source

Returns true if this element has the given class

@param klass [String] the class to test for @return [Boolean]

# File lib/kitchen/element_base.rb, line 194
def has_class?(klass)
  (self[:class] || '').include?(klass)
end
id() click to toggle source

Returns the element's ID

@return [String]

# File lib/kitchen/element_base.rb, line 202
def id
  self[:id]
end
id=(value) click to toggle source

Sets the element's ID

@param value [String] the new value for the ID

# File lib/kitchen/element_base.rb, line 210
def id=(value)
  self[:id] = value
end
inspect() click to toggle source

Returns a string version of this element

@return [String]

# File lib/kitchen/element_base.rb, line 672
def inspect
  to_s
end
is?(type) click to toggle source

Returns true if this element is the given type

@param type [Symbol] the descendant type, e.g. `:page` @raise if the type is unknown @return [Boolean]

# File lib/kitchen/element_base.rb, line 175
def is?(type)
  ElementBase.descendant!(type).is_the_element_class_for?(raw, config: config)
end
mark_as_current_location!() click to toggle source

Mark the location so that if there's an error we can show the developer where.

# File lib/kitchen/element_base.rb, line 656
def mark_as_current_location!
  document.location = self
end
parent() click to toggle source
# File lib/kitchen/element_base.rb, line 510
def parent
  Element.new(node: raw.parent, document: document, short_type: "parent(#{short_type})")
end
paste() click to toggle source

When an element is cut or copied, use this method to get the element's content; keeps IDs unique

# File lib/kitchen/element_base.rb, line 482
def paste
  # See `clone` method for a note about namespaces
  block_error_if(block_given?)
  temp_copy = clone
  temp_copy.raw.traverse do |node|
    next if node.text? || node.document?

    if node[:id].present?
      id_tracker.record_id_pasted(node[:id])
      node[:id] = id_tracker.modified_id_to_paste(node[:id])
    end
  end
  temp_copy.to_s
end
prepend(child: nil, sibling: nil) click to toggle source

If child argument given, prepends it before the element's current children. If sibling is given, prepends it as a sibling to this element.

@param child [String] the child to prepend @param sibling [String] the sibling to prepend @raise [RecipeError] if specify other than just a child or a sibling

# File lib/kitchen/element_base.rb, line 538
def prepend(child: nil, sibling: nil)
  require_one_of_child_or_sibling(child, sibling)

  if child
    if node.children.empty?
      node.children = child.to_s
    else
      node.children.first.add_previous_sibling(child)
    end
  else
    node.add_previous_sibling(sibling)
  end

  self
end
previous() click to toggle source

returns previous element skips double indentations that the nokigiri sometimes picks up nil if there's no previous sibling

# File lib/kitchen/element_base.rb, line 518
def previous
  prev = raw.previous
  return prev if prev.nil?

  Element.new(
    node: prev,
    document: document,
    short_type: "previous(#{short_type})"
  )
end
raw() click to toggle source

Returns the underlying Nokogiri object

@return [Nokogiri::XML::Node]

# File lib/kitchen/element_base.rb, line 664
def raw
  node
end
remember_that_a_sub_element_was_counted(search_query, type) click to toggle source

Track that a sub element found by the given query has been counted

@param search_query [SearchQuery] the search query matching the counted element @param type [String] the type of the sub element that was counted

# File lib/kitchen/element_base.rb, line 315
def remember_that_a_sub_element_was_counted(search_query, type)
  @search_query_matches_that_have_been_counted[search_query.to_s] ||= Hash.new(0)
  @search_query_matches_that_have_been_counted[search_query.to_s][type] += 1
end
replace_children(with:) click to toggle source

Replaces this element's children

@param with [String] the children to substitute for the current children

# File lib/kitchen/element_base.rb, line 581
def replace_children(with:)
  node.children = with
  self
end
search_history() click to toggle source

Returns the search history that found this element

@return [SearchHistory]

# File lib/kitchen/element_base.rb, line 336
def search_history
  SearchHistory.new(
    ancestor_elements.last&.search_history || SearchHistory.empty,
    search_query_that_found_me
  )
end
search_with(*enumerator_classes) click to toggle source

Searches for elements handled by a list of enumerator classes. All element that matches one of those enumerator classes are iterated over.

@param enumerator_classes [Array<ElementEnumeratorBase>] @return [TypeCastingElementEnumerator]

# File lib/kitchen/element_base.rb, line 428
def search_with(*enumerator_classes)
  block_error_if(block_given?)
  raise 'must supply at least one enumerator class' if enumerator_classes.empty?

  factory = enumerator_classes[0].factory
  enumerator_classes[1..-1].each do |enumerator_class|
    factory = factory.or_with(enumerator_class.factory)
  end
  factory.build_within(self)
end
set(property, value) click to toggle source

A way to set values and chain them

@param property [String, Symbol] the name of the property to set @param value [String] the value to set

@example

element.set(:name,"div").set("id","foo")
# File lib/kitchen/element_base.rb, line 222
def set(property, value)
  case property.to_sym
  when :name
    self.name = value
  else
    self[property.to_sym] = value
  end
  self
end
sub_header_name() click to toggle source

Returns the header tag name that is one level under the first header tag in this element, e.g. if this element is a “div” whose first header is “h1”, this will return “h2”

TODO this method may not be needed.

@return [String] the sub header tag name

# File lib/kitchen/element_base.rb, line 644
def sub_header_name
  first_header = node.search('h1, h2, h3, h4, h5, h6').first

  if first_header.nil?
    'h1'
  else
    first_header.name.gsub(/\d/) { |num| (num.to_i + 1).to_s }
  end
end
target_label(label_text: nil, custom_content: nil, cases: false) click to toggle source

Creates labels for links to inside elements like Figures, Tables, Equations, Exercises, Notes.

@param label_text [String] label of the element defined in yml file.

(e.g. "Figure", "Table", "Equation")

@param custom_content [String] might be numbering of the element or text

copied from content (e.g. note title)

@param cases [Boolean] true if labels should use grammatical cases

(used in Polish books)

@return [Pantry]

# File lib/kitchen/element_base.rb, line 739
def target_label(label_text: nil, custom_content: nil, cases: false)
  if cases
    cases = %w[nominative genitive dative accusative instrumental locative vocative]
    element_labels = {}

    cases.each do |label_case|
      element_labels[label_case] = "#{I18n.t("#{label_text}.#{label_case}")} #{custom_content}"

      element_label_case = element_labels[label_case]

      pantry(name: "#{label_case}_link_text").store element_label_case, label: id if id
    end
  else
    element_label = if label_text
                      "#{I18n.t(label_text.to_s)} #{custom_content}"
                    else
                      custom_content
                    end
    pantry(name: :link_text).store element_label, label: id if id
  end
end
to_s() click to toggle source

Returns a string version of this element

@return [String]

# File lib/kitchen/element_base.rb, line 680
def to_s
  remove_default_namespaces_if_clone(node.to_s)
end
to_xhtml() click to toggle source

Returns a string version of this element as XHTML

@return [String]

# File lib/kitchen/element_base.rb, line 696
def to_xhtml
  remove_default_namespaces_if_clone(node.to_xhtml)
end
to_xml() click to toggle source

Returns a string version of this element as XML

@return [String]

# File lib/kitchen/element_base.rb, line 688
def to_xml
  remove_default_namespaces_if_clone(node.to_xml)
end
trash() click to toggle source

Delete the element

# File lib/kitchen/element_base.rb, line 505
def trash
  node.remove
  self
end
uncount(search_query) click to toggle source

Undo the counts from a prior search query (so that they can be counted again)

@param search_query [SearchQuery] the prior search query whose counts need to be undone

# File lib/kitchen/element_base.rb, line 324
def uncount(search_query)
  @search_query_matches_that_have_been_counted.delete(search_query.to_s)&.each do |type, count|
    ancestors.each_value do |ancestor|
      ancestor.decrement_descendant_count(type, by: count)
    end
  end
end
wrap_children(name='div', attributes={}) { |element(node: new_node, document: document, short_type: nil)| ... } click to toggle source

Wraps the element's children in a new element. Yields the new wrapper element to a block, if provided.

@param name [String] the wrapper's tag name, defaults to 'div'. @param attributes [Hash] the wrapper's attributes. XML attributes often use hyphens

(e.g. 'data-type') which are hard to put into symbols.  Therefore underscores in
keys passed to this method will be converted to hyphens.  If you really want an
underscore you can use a double underscore.

@yieldparam [Element] the wrapper Element @return [Element] self

# File lib/kitchen/element_base.rb, line 597
def wrap_children(name='div', attributes={})
  if name.is_a?(Hash)
    attributes = name
    name = 'div'
  end

  node.children = node.document.create_element(name) do |new_node|
    # For some reason passing attributes to create_element doesn't work, so doing here
    attributes.each do |k, v|
      new_node[k.to_s.gsub(/([^_])_([^_])/, '\1-\2').gsub('__', '_')] = v
    end
    new_node.children = children
    yield Element.new(node: new_node, document: document, short_type: nil) if block_given?
  end

  self
end

Protected Instance Methods

get_clipboard(name_or_object) click to toggle source

Return a clipboard

@param name_or_object [String, Clipboard] the name of the clipboard or the clipboard itself @return [Clipboard]

# File lib/kitchen/element_base.rb, line 790
def get_clipboard(name_or_object)
  case name_or_object
  when Symbol
    clipboard(name: name_or_object)
  when Clipboard
    name_or_object
  else
    raise ArgumentError, "The provided argument (#{name_or_object}) is not " \
                         "a clipboard name or a clipboard"
  end
end
remove_default_namespaces_if_clone(string) click to toggle source

Clean up some default namespace junk for cloned elements

@param string [String] the string to clean

# File lib/kitchen/element_base.rb, line 805
def remove_default_namespaces_if_clone(string)
  if is_a_clone
    string.gsub('xmlns:default="http://www.w3.org/1999/xhtml"', '')
          .gsub('xmlns="http://www.w3.org/1999/xhtml"', '')
          .gsub('default:', '')
  else
    string
  end
end
require_one_of_child_or_sibling(child, sibling) click to toggle source
# File lib/kitchen/element_base.rb, line 815
def require_one_of_child_or_sibling(child, sibling)
  raise RecipeError, 'Only one of `child` or `sibling` can be specified' if child && sibling
  raise RecipeError, 'One of `child` or `sibling` must be specified' if !child && !sibling
end