Class: Kitchen::ElementBase

Inherits:
Object show all
Extended by:
Forwardable
Includes:
Mixins::BlockErrorIf
Defined in:
lib/kitchen/element_base.rb

Overview

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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Mixins::BlockErrorIf

#block_error_if

Constructor Details

#initialize(node:, document:, enumerator_class:, short_type: nil) ⇒ ElementBase

Creates a new instance

Parameters:

Raises:

  • (ArgumentError)


120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/kitchen/element_base.rb', line 120

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

Instance Attribute Details

#ancestorsArray<Ancestor> (readonly)

Returns the element’s ancestors

Returns:



331
332
333
# File 'lib/kitchen/element_base.rb', line 331

def ancestors
  @ancestors
end

#document(: clipboard) ⇒ Clipboard (readonly)

Access the clipboard for this element’s document

Returns:



17
18
19
# File 'lib/kitchen/element_base.rb', line 17

def document
  @document
end

#enumerator_classClass (readonly)

The enumerator class for this element

Returns:

  • (Class)


25
26
27
# File 'lib/kitchen/element_base.rb', line 25

def enumerator_class
  @enumerator_class
end

#search_query_that_found_meSearchQuery

The search query that located this element in the DOM

Returns:



29
30
31
# File 'lib/kitchen/element_base.rb', line 29

def search_query_that_found_me
  @search_query_that_found_me
end

#short_typeSymbol, String (readonly)

The element’s type, e.g. :page

Returns:



21
22
23
# File 'lib/kitchen/element_base.rb', line 21

def short_type
  @short_type
end

Class Method Details

.descendant(type) ⇒ Class

Returns ElementBase descendent type or nil if none found

Parameters:

  • type (Symbol)

    the descendant type, e.g. ‘:page`

Returns:

  • (Class)

    the child class for the given type



152
153
154
155
156
157
158
159
160
161
# File 'lib/kitchen/element_base.rb', line 152

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) ⇒ Class

Returns ElementBase descendent type or Error if none found

Parameters:

  • type (Symbol)

    the descendant type, e.g. ‘:page`

Returns:

  • (Class)

    the child class for the given type

Raises:

  • if the type is unknown



169
170
171
# File 'lib/kitchen/element_base.rb', line 169

def self.descendant!(type)
  descendant(type) || raise("Unknown ElementBase descendant type '#{type}'")
end

.is_the_element_class_for?(node, config:) ⇒ Boolean

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

Parameters:

Returns:

  • (Boolean)


189
190
191
# File 'lib/kitchen/element_base.rb', line 189

def self.is_the_element_class_for?(node, config:)
  Selector.named(short_type).matches?(node, config: config)
end

Instance Method Details

#[]String

Get an element attribute

Returns:

See Also:



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#[]=Object

Set an element attribute



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#add_ancestor(ancestor) ⇒ Object

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

Parameters:

Raises:

  • (StandardError)

    if there is already an ancestor with the given ancestor’s type



359
360
361
362
363
364
365
366
367
368
# File 'lib/kitchen/element_base.rb', line 359

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" \
          "#{say_source_or_nil}"
  end

  ancestor.increment_descendant_count(short_type)
  @ancestors[ancestor.type] = ancestor
end

#add_ancestors(*args) ⇒ Object

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

Parameters:

Raises:

  • (StandardError)

    if there is already an ancestor with the one of the given ancestors’ types



339
340
341
342
343
344
345
346
347
348
349
350
351
352
# File 'lib/kitchen/element_base.rb', line 339

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

#add_classObject

Add a class to the element



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#add_platform_media(format) ⇒ Object



951
952
953
954
955
956
957
958
959
960
961
# File 'lib/kitchen/element_base.rb', line 951

def add_platform_media(format)
  valid_formats = %w[screen print screenreader]
  raise "Media format invalid; valid formats are #{valid_formats}" unless valid_formats.include?(format)

  self[:'data-media'] = \
    if self[:'data-media']
      self[:'data-media'].split.to_set.add(format).to_a.join(' ')
    else
      format
    end
end

#ancestor(type) ⇒ Ancestor

Returns this element’s ancestor of the given type

Parameters:

  • type (String, Symbol)

    e.g. :page, :term

Returns:

Raises:

  • (StandardError)

    if there is no ancestor of the given type



313
314
315
316
# File 'lib/kitchen/element_base.rb', line 313

def ancestor(type)
  @ancestors[type.to_sym]&.element || raise("No ancestor of type '#{type}'" \
                                      "#{say_source_or_nil}")
end

#ancestor_elementsArray<ElementBase>

Return the elements in all of the ancestors

Returns:



374
375
376
# File 'lib/kitchen/element_base.rb', line 374

def ancestor_elements
  @ancestors.values.map(&:element)
end

#append(child: nil, sibling: nil) ⇒ Object

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

Parameters:

  • child (String) (defaults to: nil)

    the child to append

  • sibling (String) (defaults to: nil)

    the sibling to append

Raises:

  • (RecipeError)

    if specify other than just a child or a sibling



660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
# File 'lib/kitchen/element_base.rb', line 660

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_enumeratorElementEnumeratorBase

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

Returns:



914
915
916
# File 'lib/kitchen/element_base.rb', line 914

def as_enumerator
  enumerator_class.new(search_query: search_query_that_found_me) { |block| block.yield(self) }
end

#childrenNokogiri::XML::NodeSet

Get the element’s children

Returns:

  • (Nokogiri::XML::NodeSet)

See Also:



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#classesArray<String>

Gets the element’s classes

Returns:

See Also:



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#cloneObject

Returns a clone of this object



801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
# File 'lib/kitchen/element_base.rb', line 801

def clone
  super.tap do |element|
    # Define all namespaces that are used inside the node being cloned.
    #
    # When document is created, all namespaces definitions from the book
    # are collected and added to the html tag.
    # In the moment when clipboard is pasted and converted to string
    # we are loosing information about namespaces defined on html ancestor.
    #
    # It's important to add namespace definition directly to the node that has a namespace.
    # Adding it just to the node being cloned is breaking elements that have
    # namespaces without prefix (e.g <math xmlns="http://www.w3.org/1998/Math/MathML"/>)
    raw.traverse do |node|
      node.attribute_nodes.each do |attr|
        next unless attr.namespace

        unless node.has_namespace_defined_on_ancestor?(attribute: attr)
          node.add_namespace(attr.namespace.prefix, attr.namespace.href)
        end
      end

      next unless node.namespace

      unless node.has_namespace_defined_on_ancestor?
        node.add_namespace(node.namespace.prefix, node.namespace.href)
      end
    end

    # 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

#configConfig

Get the config for this element’s document

Returns:



96
# File 'lib/kitchen/element_base.rb', line 96

def_delegators :document, :config

#contains?(*selector_or_xpath_args) ⇒ Boolean

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

Parameters:

  • selector_or_xpath_args (Array<String>)

    CSS selectors or XPath arguments

Returns:

  • (Boolean)


731
732
733
# File 'lib/kitchen/element_base.rb', line 731

def contains?(*selector_or_xpath_args)
  !node.at(*selector_or_xpath_args).nil?
end

#contains_blockish?Boolean

Returns true if the current element contains blockish descendants

Returns:

  • (Boolean)


458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
# File 'lib/kitchen/element_base.rb', line 458

def contains_blockish?
  # Block-level elements: https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
  block_level = %w[
    address
    article
    aside
    blockquote
    details
    dialog
    dd
    div
    dl
    dt
    fieldset
    figcaption
    figure
    footer
    form
    h1
    h2
    h3
    h4
    h5
    h6
    header
    hgroup
    hr
    li
    main
    nav
    ol
    p
    pre
    section
    table
    ul
  ]
  search(block_level.join(',')).to_a.length.positive?
end

#content(*selector_or_xpath_args) ⇒ String

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

Parameters:

  • selector_or_xpath_args (Array<String>)

    CSS selectors or XPath arguments

Returns:



722
723
724
# File 'lib/kitchen/element_base.rb', line 722

def content(*selector_or_xpath_args)
  node.search(*selector_or_xpath_args).children.to_s
end

#copy(to: nil) ⇒ Element

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

Parameters:

  • to (Symbol, String, Clipboard, nil) (defaults to: 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.

Returns:



571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/kitchen/element_base.rb', line 571

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) ⇒ Object

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

Parameters:

  • ancestor_type (String, Symbol)


382
383
384
385
386
# File 'lib/kitchen/element_base.rb', line 382

def count_in(ancestor_type)
  @ancestors[ancestor_type]&.get_descendant_count(short_type) ||
    raise("No ancestor of type '#{ancestor_type}'#{say_source_or_nil}"
    )
end

#custom_target_label_for_modules(custom_title_content: nil, custom_number_content: nil) ⇒ Pantry

Creates labels for links to modules (used only in accounting where LO link labels are also present and utilizes only the number)

Parameters:

  • custom_title_content (String) (defaults to: nil)

    title text copied from content (module title)

  • custom_number_content (String) (defaults to: nil)

    numbering copied from content (module number)

Returns:



895
896
897
898
899
900
901
# File 'lib/kitchen/element_base.rb', line 895

def custom_target_label_for_modules(custom_title_content: nil,
                                    custom_number_content: nil)
  element_label = <<~HTML.chomp
    <span class="label-counter">#{custom_number_content}</span><span class="title-label-text">#{custom_title_content}</span>
  HTML
  pantry(name: :link_text).store element_label, label: id if id
end

#cut(to: nil) ⇒ Element

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

Parameters:

  • to (Symbol, String, Clipboard, nil) (defaults to: 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.

Returns:



551
552
553
554
555
556
557
558
559
560
561
562
# File 'lib/kitchen/element_base.rb', line 551

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

#data_smString

Returns the element’s data-sm

Returns:



246
247
248
# File 'lib/kitchen/element_base.rb', line 246

def data_sm
  self[:'data-sm']
end

#data_sm_formattedString

function to translate phil’s notation (modules/m53650/index.cnxml:6:3) (6 Line, 3 col) to M123:L456:C789

Returns:



254
255
256
257
258
259
260
# File 'lib/kitchen/element_base.rb', line 254

def data_sm_formatted
  format_match = /\/m(\d+)\/[^:]+:(\d+):(\d+)/

  module_line_column = data_sm.match(format_match).captures

  "(self) M#{module_line_column[0]}:L#{module_line_column[1]}:C#{module_line_column[2]}"
end

#data_sourceString

Returns the element’s data source map in M123:L456:C789 style and whether this is self or the nearest parent’s sm

Returns:



266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/kitchen/element_base.rb', line 266

def data_source
  return nil if !parent&.name || parent&.name == 'html'

  if data_sm.nil? && parent&.name != 'html'
    parent_source = parent.data_source

    return nil if parent_source.nil?

    "(nearest parent) #{parent_source.split(' ')[1]}" unless parent_source.match(/\(nearest parent\)/)

  else
    data_sm_formatted
  end
end

#data_typeString

Returns the element’s data-type

Returns:



222
223
224
# File 'lib/kitchen/element_base.rb', line 222

def data_type
  self[:'data-type']
end

#element_childrenTypeCastingElementEnumerator

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



536
537
538
539
540
541
542
# File 'lib/kitchen/element_base.rb', line 536

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) {|the| ... } ⇒ Element? Also known as: at

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

Parameters:

  • selector_or_xpath_args (Array<String>)

    CSS selectors or XPath arguments

  • reload (Boolean) (defaults to: false)

    ignores cache if true

Yield Parameters:

  • the (Element)

    matched XML element

Returns:

  • (Element, nil)

    the matched XML element or nil if no match found



506
507
508
509
510
# File 'lib/kitchen/element_base.rb', line 506

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

#first!(*selector_or_xpath_args, reload: false) {|the| ... } ⇒ Element

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

Parameters:

  • selector_or_xpath_args (Array<String>)

    CSS selectors or XPath arguments

  • reload (Boolean) (defaults to: false)

    ignores cache if true

Yield Parameters:

  • the (Element)

    matched XML element

Returns:

  • (Element)

    the matched XML element

Raises:



521
522
523
524
525
# File 'lib/kitchen/element_base.rb', line 521

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) ⇒ Boolean

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

Parameters:

  • type (String, Symbol)

    e.g. :page, :term

Returns:

  • (Boolean)


323
324
325
# File 'lib/kitchen/element_base.rb', line 323

def has_ancestor?(type)
  @ancestors[type.to_sym].present?
end

#has_class?(klass) ⇒ Boolean

Returns true if this element has the given class

Parameters:

  • klass (String)

    the class to test for

Returns:

  • (Boolean)


198
199
200
# File 'lib/kitchen/element_base.rb', line 198

def has_class?(klass)
  (self[:class] || '').include?(klass)
end

#hrefString

Returns the element’s href

Returns:



230
231
232
# File 'lib/kitchen/element_base.rb', line 230

def href
  self[:href]
end

#href=(value) ⇒ Object

Sets the element’s href

Parameters:

  • value (String)

    the new value for the href



238
239
240
# File 'lib/kitchen/element_base.rb', line 238

def href=(value)
  self[:href] = value
end

#idString

Returns the element’s ID

Returns:



206
207
208
# File 'lib/kitchen/element_base.rb', line 206

def id
  self[:id]
end

#id=(value) ⇒ Object

Sets the element’s ID

Parameters:

  • value (String)

    the new value for the ID



214
215
216
# File 'lib/kitchen/element_base.rb', line 214

def id=(value)
  self[:id] = value
end

#inner_html=Object

Set the inner HTML for this element

Returns:

  • Object

See Also:



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#inspectString

Returns a string version of this element

Returns:



771
772
773
# File 'lib/kitchen/element_base.rb', line 771

def inspect
  to_s
end

#is?(type) ⇒ Boolean

Returns true if this element is the given type

Parameters:

  • type (Symbol)

    the descendant type, e.g. ‘:page`

Returns:

  • (Boolean)

Raises:

  • if the type is unknown



179
180
181
# File 'lib/kitchen/element_base.rb', line 179

def is?(type)
  ElementBase.descendant!(type).is_the_element_class_for?(raw, config: config)
end

#key?(attribute) ⇒ Object

Returns true if attribute is set



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#mark_as_current_location!Object

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



755
756
757
# File 'lib/kitchen/element_base.rb', line 755

def mark_as_current_location!
  document.location = self
end

#nameString

Get the element name (the tag)

Returns:

See Also:



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#name=Object

Set the element name (the tag)



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#pagesObject

Returns a pages enumerator



905
906
907
908
# File 'lib/kitchen/element_base.rb', line 905

def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples,
:metadatas, :non_introduction_pages, :units, :titles, :exercises, :references,
:composite_pages, :composite_chapters, :solutions, :injected_questions,
:search_with, :sections, :injected_exercises, :images

#pantryPantry

Access the pantry for this element’s document

Returns:



109
# File 'lib/kitchen/element_base.rb', line 109

def_delegators :document, :pantry, :clipboard

#parentObject



609
610
611
# File 'lib/kitchen/element_base.rb', line 609

def parent
  Element.new(node: raw.parent, document: document, short_type: "parent(#{short_type})")
end

#pasteObject

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



587
588
589
590
591
592
593
594
595
596
597
598
599
600
# File 'lib/kitchen/element_base.rb', line 587

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.delete('id') unless id_tracker.first_id?(node[:id])
    end
  end
  temp_copy.to_s
end

#pathString

Get the path for this element

Returns:

See Also:



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#preceded_by_textObject

Returns true if the immediately preceding sibling is text

Returns:

  • Boolean



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#prepend(child: nil, sibling: nil) ⇒ Object

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

Parameters:

  • child (String) (defaults to: nil)

    the child to prepend

  • sibling (String) (defaults to: nil)

    the sibling to prepend

Raises:

  • (RecipeError)

    if specify other than just a child or a sibling



637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
# File 'lib/kitchen/element_base.rb', line 637

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

#previousObject

returns previous element sibling (only returns elements or nil) nil if there’s no previous sibling



617
618
619
620
621
622
623
624
625
626
# File 'lib/kitchen/element_base.rb', line 617

def previous
  prev = raw.previous_element
  return prev if prev.nil?

  Element.new(
    node: prev,
    document: document,
    short_type: "previous(#{short_type})"
  )
end

#rawNokogiri::XML::Node

Returns the underlying Nokogiri object

Returns:



763
764
765
# File 'lib/kitchen/element_base.rb', line 763

def raw
  node
end

#raw_search(*selector_or_xpath_args, reload: false) ⇒ Object



446
447
448
449
450
451
452
# File 'lib/kitchen/element_base.rb', line 446

def raw_search(*selector_or_xpath_args, reload: false)
  key = selector_or_xpath_args
  @search_cache[key] = nil if reload || !config.enable_search_cache
  # cache nil search results with a fake -1 value
  @search_cache[key] ||= raw.search(*selector_or_xpath_args) || -1
  @search_cache[key] == -1 ? nil : @search_cache[key]
end

#remember_that_a_sub_element_was_counted(search_query, type) ⇒ Object

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

Parameters:

  • search_query (SearchQuery)

    the search query matching the counted element

  • type (String)

    the type of the sub element that was counted



393
394
395
396
# File 'lib/kitchen/element_base.rb', line 393

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

#remove_attributeObject

Removes an attribute from the element



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#remove_classObject

Remove a class from the element



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#replace_children(with:) ⇒ Object

Replaces this element’s children

Parameters:

  • with (String)

    the children to substitute for the current children



680
681
682
683
# File 'lib/kitchen/element_base.rb', line 680

def replace_children(with:)
  node.children = with
  self
end


918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
# File 'lib/kitchen/element_base.rb', line 918

def rex_link
  self[:'data-is-for-rex-linking'] = 'true'

  element_with_ancestors = document.book.chapters.search_with(
    Kitchen::PageElementEnumerator, Kitchen::CompositePageElementEnumerator
  ).search('[data-is-for-rex-linking="true"]').first

  remove_attribute('data-is-for-rex-linking')

  unless element_with_ancestors
    raise("Cannot create rex link to element #{self} - needs ancestors of both types chapter & page/composite_page" \
          "#{say_source_or_nil}"
    )
  end

  book_slug = document.slug
  chapter_count = element_with_ancestors.ancestor(:chapter).count_in(:book)
  page_string = ''
  page_title = ''
  page = element_with_ancestors.ancestor(:page) if element_with_ancestors.has_ancestor?(:page)
  if page&.is_introduction?
    page_title = page.first('[data-type="document-title"]').text.kebab_case
  elsif page
    page_string = "#{page.count_in(:chapter) - 1}-"
    page_title = page.title_text.kebab_case
  else
    page = element_with_ancestors.ancestor(:composite_page)
    page_title = page.title.text.kebab_case
  end

  "https://openstax.org/books/#{book_slug}/pages/#{chapter_count}-#{page_string}#{page_title}"
end

#say_source_or_nilString

Gives Error-Ready Data Source Map message or nil if there’s no data_source

Returns:



285
286
287
# File 'lib/kitchen/element_base.rb', line 285

def say_source_or_nil
  "#{data_source ? "\nCNXML SOURCE: " : nil}#{data_source}"
end

#search(*selector_or_xpath_args, only: nil, except: nil, reload: false) ⇒ ElementEnumerator

Returns an ElementEnumerator that iterates over the provided selector or xpath queries

Parameters:

  • selector_or_xpath_args (Array<String>)

    Selector or XPath queries

  • only (Symbol, Callable) (defaults to: nil)

    the name of a method to call on an element or a lambda or proc that accepts an element; elements will only be included in the search results if the method or callable returns true

  • except (Symbol, Callable) (defaults to: nil)

    the name of a method to call on an element or a lambda or proc that accepts an element; elements will not be included in the search results if the method or callable returns false

Returns:



432
433
434
435
436
437
438
439
440
441
442
443
444
# File 'lib/kitchen/element_base.rb', line 432

def search(*selector_or_xpath_args, only: nil, except: nil, reload: false)
  block_error_if(block_given?)

  ElementEnumerator.factory.build_within(
    self,
    search_query: SearchQuery.new(
      css_or_xpath: selector_or_xpath_args,
      only: only,
      except: except
    ),
    reload: reload
  )
end

#search_historySearchHistory

Returns the search history that found this element

Returns:



414
415
416
417
418
419
# File 'lib/kitchen/element_base.rb', line 414

def search_history
  SearchHistory.new(
    ancestor_elements.last&.search_history || SearchHistory.empty,
    search_query_that_found_me
  )
end

#selectorsSelectors::Base

Get the selectors for this element’s document

Returns:



101
# File 'lib/kitchen/element_base.rb', line 101

def_delegators :config, :selectors

#set(property, value) ⇒ Object

A way to set values and chain them

Examples:

element.set(:name,"div").set("id","foo")

Parameters:

  • property (String, Symbol)

    the name of the property to set

  • value (String)

    the value to set



297
298
299
300
301
302
303
304
305
# File 'lib/kitchen/element_base.rb', line 297

def set(property, value)
  case property.to_sym
  when :name
    self.name = value
  else
    self[property.to_sym] = value
  end
  self
end

#sub_header_nameString

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.

Returns:

  • (String)

    the sub header tag name



743
744
745
746
747
748
749
750
751
# File 'lib/kitchen/element_base.rb', line 743

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, label_class: nil) ⇒ Pantry

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

Parameters:

  • label_text (String) (defaults to: nil)

    label of the element defined in yml file. (e.g. “Figure”, “Table”, “Equation”)

  • custom_content (String) (defaults to: nil)

    might be numbering of the element or text copied from content (e.g. note title)

  • cases (Boolean) (defaults to: false)

    true if labels should use grammatical cases (used in Polish books)

Returns:



863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
# File 'lib/kitchen/element_base.rb', line 863

def target_label(label_text: nil, custom_content: nil, cases: false, label_class: nil)
  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

  return unless label_class

  pantry(name: :link_type).store label_class, label: id if id
end

#textString

Get the element text

Returns:

See Also:



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#to_htmlString

Get the element as HTML

Returns:

See Also:



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#to_sString

Returns a string version of this element

Returns:



779
780
781
# File 'lib/kitchen/element_base.rb', line 779

def to_s
  remove_default_namespaces_if_clone(node.to_s)
end

#to_xhtmlString

Returns a string version of this element as XHTML

Returns:



795
796
797
# File 'lib/kitchen/element_base.rb', line 795

def to_xhtml
  remove_default_namespaces_if_clone(node.to_xhtml)
end

#to_xmlString

Returns a string version of this element as XML

Returns:



787
788
789
# File 'lib/kitchen/element_base.rb', line 787

def to_xml
  remove_default_namespaces_if_clone(node.to_xml)
end

#trashObject

Delete the element



604
605
606
607
# File 'lib/kitchen/element_base.rb', line 604

def trash
  node.remove
  self
end

#uncount(search_query) ⇒ Object

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

Parameters:

  • search_query (SearchQuery)

    the prior search query whose counts need to be undone



402
403
404
405
406
407
408
# File 'lib/kitchen/element_base.rb', line 402

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

#wrapNokogiri::XML::Node

Add HTML around this element



88
89
90
91
# File 'lib/kitchen/element_base.rb', line 88

def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
:text, :wrap, :children, :to_html, :remove_attribute,
:key?, :classes, :path, :inner_html=, :add_previous_sibling,
:preceded_by_text?

#wrap_children(name = 'div', attributes = {}) {|the| ... } ⇒ Element

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

Parameters:

  • name (String) (defaults to: 'div')

    the wrapper’s tag name, defaults to ‘div’.

  • attributes (Hash) (defaults to: {})

    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.

Yield Parameters:

  • the (Element)

    wrapper Element

Returns:



696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
# File 'lib/kitchen/element_base.rb', line 696

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