openstax_kitchen 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/.devcontainer/devcontainer.json +37 -17
  3. data/.github/config.yml +14 -0
  4. data/.github/workflows/tests.yml +5 -15
  5. data/.gitignore +1 -1
  6. data/.inch.yml +6 -0
  7. data/.rubocop.yml +65 -0
  8. data/CHANGELOG.md +16 -0
  9. data/Gemfile +5 -3
  10. data/Gemfile.lock +54 -5
  11. data/README.md +58 -11
  12. data/Rakefile +5 -3
  13. data/bin/console +4 -3
  14. data/docker/Dockerfile +36 -0
  15. data/docker/Dockerfile.ci +10 -0
  16. data/docker/bash +5 -1
  17. data/docker/build +10 -0
  18. data/docker/ci +16 -0
  19. data/docker/run +9 -0
  20. data/docker/tag_and_push_latest +17 -0
  21. data/lefthook.yml +6 -0
  22. data/lib/kitchen/ancestor.rb +38 -1
  23. data/lib/kitchen/book_document.rb +20 -2
  24. data/lib/kitchen/book_element.rb +24 -3
  25. data/lib/kitchen/book_element_enumerator.rb +4 -0
  26. data/lib/kitchen/book_recipe.rb +15 -1
  27. data/lib/kitchen/chapter_element.rb +43 -3
  28. data/lib/kitchen/chapter_element_enumerator.rb +9 -1
  29. data/lib/kitchen/clipboard.rb +35 -4
  30. data/lib/kitchen/composite_chapter_element.rb +20 -1
  31. data/lib/kitchen/composite_page_element.rb +25 -2
  32. data/lib/kitchen/composite_page_element_enumerator.rb +8 -0
  33. data/lib/kitchen/config.rb +14 -7
  34. data/lib/kitchen/counter.rb +9 -2
  35. data/lib/kitchen/debug/print_recipe_error.rb +53 -35
  36. data/lib/kitchen/directions/.rubocop.yml +22 -0
  37. data/lib/kitchen/directions/bake_appendix.rb +4 -4
  38. data/lib/kitchen/directions/bake_chapter_glossary.rb +10 -7
  39. data/lib/kitchen/directions/bake_chapter_introductions.rb +6 -6
  40. data/lib/kitchen/directions/bake_chapter_key_equations.rb +9 -6
  41. data/lib/kitchen/directions/bake_chapter_summary.rb +16 -13
  42. data/lib/kitchen/directions/bake_chapter_title/main.rb +11 -0
  43. data/lib/kitchen/directions/bake_chapter_title/v1.rb +24 -0
  44. data/lib/kitchen/directions/bake_composite_pages.rb +2 -2
  45. data/lib/kitchen/directions/bake_example.rb +6 -4
  46. data/lib/kitchen/directions/bake_exercises/main.rb +11 -0
  47. data/lib/kitchen/directions/bake_exercises/v1.rb +166 -0
  48. data/lib/kitchen/directions/bake_figure.rb +8 -5
  49. data/lib/kitchen/directions/bake_footnotes/main.rb +2 -2
  50. data/lib/kitchen/directions/bake_footnotes/v1.rb +4 -4
  51. data/lib/kitchen/directions/bake_index/main.rb +2 -2
  52. data/lib/kitchen/directions/bake_index/v1.rb +22 -15
  53. data/lib/kitchen/directions/bake_link_placeholders.rb +24 -0
  54. data/lib/kitchen/directions/bake_math_in_paragraph.rb +5 -3
  55. data/lib/kitchen/directions/bake_notes.rb +8 -8
  56. data/lib/kitchen/directions/bake_numbered_table/main.rb +2 -2
  57. data/lib/kitchen/directions/bake_numbered_table/v1.rb +21 -16
  58. data/lib/kitchen/directions/bake_page_abstracts.rb +14 -0
  59. data/lib/kitchen/directions/bake_preface/main.rb +11 -0
  60. data/lib/kitchen/directions/bake_preface/v1.rb +18 -0
  61. data/lib/kitchen/directions/bake_stepwise.rb +7 -7
  62. data/lib/kitchen/directions/bake_suggested_reading.rb +26 -0
  63. data/lib/kitchen/directions/bake_toc.rb +41 -22
  64. data/lib/kitchen/directions/bake_unit_title/main.rb +11 -0
  65. data/lib/kitchen/directions/bake_unit_title/v1.rb +23 -0
  66. data/lib/kitchen/directions/bake_unnumbered_tables.rb +7 -5
  67. data/lib/kitchen/directions/move_title_text_into_span.rb +2 -2
  68. data/lib/kitchen/document.rb +72 -13
  69. data/lib/kitchen/element.rb +11 -0
  70. data/lib/kitchen/element_base.rb +276 -56
  71. data/lib/kitchen/element_enumerator.rb +8 -0
  72. data/lib/kitchen/element_enumerator_base.rb +210 -28
  73. data/lib/kitchen/element_enumerator_factory.rb +59 -52
  74. data/lib/kitchen/element_factory.rb +27 -12
  75. data/lib/kitchen/errors.rb +5 -0
  76. data/lib/kitchen/example_element.rb +19 -1
  77. data/lib/kitchen/example_element_enumerator.rb +9 -1
  78. data/lib/kitchen/figure_element.rb +36 -2
  79. data/lib/kitchen/figure_element_enumerator.rb +9 -1
  80. data/lib/kitchen/metadata_element.rb +28 -0
  81. data/lib/kitchen/metadata_element_enumerator.rb +21 -0
  82. data/lib/kitchen/mixins/block_error_if.rb +24 -4
  83. data/lib/kitchen/note_element.rb +37 -7
  84. data/lib/kitchen/note_element_enumerator.rb +9 -1
  85. data/lib/kitchen/oven.rb +66 -15
  86. data/lib/kitchen/page_element.rb +62 -13
  87. data/lib/kitchen/page_element_enumerator.rb +9 -1
  88. data/lib/kitchen/pantry.rb +28 -1
  89. data/lib/kitchen/patches/nokogiri.rb +19 -2
  90. data/lib/kitchen/patches/renderable.rb +9 -3
  91. data/lib/kitchen/patches/string.rb +8 -0
  92. data/lib/kitchen/recipe.rb +38 -34
  93. data/lib/kitchen/search_history.rb +43 -4
  94. data/lib/kitchen/search_query.rb +84 -0
  95. data/lib/kitchen/selectors/base.rb +26 -0
  96. data/lib/kitchen/selectors/standard_1.rb +8 -0
  97. data/lib/kitchen/table_element.rb +54 -3
  98. data/lib/kitchen/table_element_enumerator.rb +9 -1
  99. data/lib/kitchen/term_element.rb +15 -1
  100. data/lib/kitchen/term_element_enumerator.rb +9 -1
  101. data/lib/kitchen/transliterations.rb +7 -5
  102. data/lib/kitchen/type_casting_element_enumerator.rb +17 -1
  103. data/lib/kitchen/unit_element.rb +39 -0
  104. data/lib/kitchen/unit_element_enumerator.rb +20 -0
  105. data/lib/kitchen/utils.rb +10 -13
  106. data/lib/kitchen/version.rb +5 -1
  107. data/lib/locales/en.yml +6 -0
  108. data/lib/openstax_kitchen.rb +43 -42
  109. data/openstax_kitchen.gemspec +26 -20
  110. data/tutorials/00/solution1.rb +9 -0
  111. data/tutorials/00/solution2.rb +8 -0
  112. data/tutorials/01/solution1.rb +18 -0
  113. data/tutorials/01/solution2.rb +26 -0
  114. data/tutorials/02/solution1.rb +31 -0
  115. data/tutorials/03/{solution_1.rb → solution1.rb} +6 -4
  116. data/tutorials/03/solution2.rb +18 -0
  117. data/tutorials/04/{solution_1.rb → solution1.rb} +4 -2
  118. data/tutorials/04/{solution_2.rb → solution2.rb} +6 -4
  119. data/tutorials/05/solution1.rb +11 -0
  120. data/tutorials/check_it +16 -15
  121. data/tutorials/setup_my_recipes +7 -6
  122. metadata +101 -22
  123. data/Dockerfile +0 -19
  124. data/docker-compose.yml +0 -12
  125. data/docker/entrypoint +0 -9
  126. data/lib/kitchen/directions/bake_exercises.rb +0 -164
  127. data/tutorials/00/solution_1.rb +0 -7
  128. data/tutorials/00/solution_2.rb +0 -6
  129. data/tutorials/01/solution_1.rb +0 -16
  130. data/tutorials/01/solution_2.rb +0 -24
  131. data/tutorials/02/solution_1.rb +0 -29
  132. data/tutorials/03/solution_2.rb +0 -15
  133. data/tutorials/05/solution_1.rb +0 -9
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ module Directions
5
+ module BakeUnitTitle
6
+ def self.v1(book:)
7
+ V1.new.bake(book: book)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::BakeUnitTitle
4
+ class V1
5
+ def bake(book:)
6
+ book.units.each do |unit|
7
+ compose_unit_title(unit: unit)
8
+ end
9
+ end
10
+
11
+ def compose_unit_title(unit:)
12
+ heading = unit.title
13
+ heading.replace_children(with:
14
+ <<~HTML
15
+ <span class="os-part-text">#{I18n.t(:unit)} </span>
16
+ <span class="os-number">#{unit.count_in(:book)}</span>
17
+ <span class="os-divider"> </span>
18
+ <span data-type="" itemprop="" class="os-text">#{heading.text}</span>
19
+ HTML
20
+ )
21
+ end
22
+ end
23
+ end
@@ -1,14 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
2
4
  module Directions
3
5
  module BakeUnnumberedTables
4
-
5
6
  def self.v1(book:)
6
- book.tables("$.unnumbered").each do |table|
7
- table.wrap(%Q(<div class="os-table">))
8
- table.remove_attribute("summary")
7
+ book.tables('$.unnumbered').each do |table|
8
+ table.wrap(%(<div class="os-table">))
9
+ table.remove_attribute('summary')
10
+ table.parent.add_class('os-unstyled-container') if table.unstyled?
11
+ table.parent.add_class('os-column-header-container') if table.column_header?
9
12
  end
10
13
  end
11
-
12
14
  end
13
15
  end
14
16
  end
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
2
4
  module Directions
3
5
  module MoveTitleTextIntoSpan
4
-
5
6
  def self.v1(title:)
6
7
  title.replace_children(with:
7
8
  <<~HTML
@@ -9,7 +10,6 @@ module Kitchen
9
10
  HTML
10
11
  )
11
12
  end
12
-
13
13
  end
14
14
  end
15
15
  end
@@ -1,25 +1,56 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
 
3
5
  module Kitchen
6
+ # Wrapper around a Nokogiri `Document`, adding search with Kitchen enumerators,
7
+ # clipboards, pantries, etc.
8
+ #
4
9
  class Document
5
10
  extend Forwardable
6
11
 
12
+ # @return [Element] the current element yielded from search results
7
13
  attr_accessor :location
14
+ # @return [Config] the configuration used in this document
8
15
  attr_reader :config
9
16
 
17
+ # @!method selectors
18
+ # The document's selectors
19
+ # @see Config#selectors
20
+ # @return [Selectors::Base]
10
21
  def_delegators :config, :selectors
22
+
23
+ # @!method to_xhtml
24
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_xhtml-instance_method Nokogiri::XML::Node#to_xhtml
25
+ # @return [String] the document as an XHTML string
26
+ # @!method to_s
27
+ # Turn this node in to a string. If the document is HTML, this method returns html.
28
+ # If the document is XML, this method returns XML.
29
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_s-instance_method Nokogiri::XML::Node#to_s
30
+ # @return [String]
31
+ # @!method to_xml
32
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_xml-instance_method Nokogiri::XML::Node#to_xml
33
+ # @return [String] the document as an XML string
34
+ # @!method to_html
35
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_html-instance_method Nokogiri::XML::Node#to_html
36
+ # @return [String] the document as an HTML string
11
37
  def_delegators :@nokogiri_document, :to_xhtml, :to_s, :to_xml, :to_html
12
38
 
39
+ # Return a new instance of Document
40
+ #
41
+ # @param nokogiri_document [Nokogiri::XML::Document]
42
+ # @param config [Config]
13
43
  def initialize(nokogiri_document:, config: nil)
14
44
  @nokogiri_document = nokogiri_document
15
45
  @location = nil
16
- @config = config || Config.new_default
46
+ @config = config || Config.new
17
47
  @next_paste_count_for_id = {}
18
- @id_copy_suffix = "_copy_"
48
+ @id_copy_suffix = '_copy_'
19
49
  end
20
50
 
21
51
  # Returns an enumerator that iterates over all children of this document
22
- # that match the provided selector or XPath arguments.
52
+ # that match the provided selector or XPath arguments. Updates `location`
53
+ # during iteration.
23
54
  #
24
55
  # @param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments
25
56
  # @return [ElementEnumerator]
@@ -29,9 +60,11 @@ module Kitchen
29
60
 
30
61
  ElementEnumerator.new do |block|
31
62
  nokogiri_document.search(*selector_or_xpath_args).each do |inner_node|
32
- element = Kitchen::Element.new(node: inner_node,
33
- document: self,
34
- short_type: Utils.search_path_to_type(selector_or_xpath_args))
63
+ element = Kitchen::Element.new(
64
+ node: inner_node,
65
+ document: self,
66
+ short_type: Utils.search_path_to_type(selector_or_xpath_args)
67
+ )
35
68
  self.location = element
36
69
  block.yield(element)
37
70
  end
@@ -92,28 +125,52 @@ module Kitchen
92
125
  )
93
126
  end
94
127
 
128
+ # Create a new Element from a string
129
+ #
130
+ # @param string [String] the element as a string
131
+ #
132
+ # @example
133
+ # create_element_from_string("<div class='foo'>bar</div>") #=> <div class="foo">bar</div>
134
+ #
135
+ # @return [Element]
136
+ #
95
137
  def create_element_from_string(string)
96
- Kitchen::Element.new(
97
- node: @nokogiri_document.create_element("div"),
98
- document: self,
99
- short_type: "created_element_#{SecureRandom.hex(4)}"
100
- ).element_children.first
138
+ children = Nokogiri::XML("<foo>#{string}</foo>").search('foo').first.element_children
139
+ raise('new_element must only make one top-level element') if children.many?
140
+
141
+ node = children.first
142
+
143
+ create_element(node.name, node.attributes).tap do |element|
144
+ element.inner_html = node.children
145
+ end
101
146
  end
102
147
 
148
+ # Keeps track that an element with the given ID has been copied. When such
149
+ # elements are pasted, this information is used to give those elements unique
150
+ # IDs that don't duplicate the original element.
151
+ #
152
+ # @param id [String] the ID
153
+ #
103
154
  def record_id_copied(id)
104
155
  return if id.blank?
156
+
105
157
  @next_paste_count_for_id[id] ||= 1
106
158
  end
107
159
 
160
+ # Returns a unique ID given the ID of an element that was copied and is about
161
+ # to be pasted
162
+ #
163
+ # @param original_id [String]
164
+ #
108
165
  def modified_id_to_paste(original_id)
109
166
  return nil if original_id.nil?
110
- return "" if original_id.blank?
167
+ return '' if original_id.blank?
111
168
 
112
169
  count = next_count_for_pasted_id(original_id)
113
170
 
114
171
  # A count of 0 means the element was cut and this is the first paste, do not
115
172
  # modify the ID; otherwise, use the uniquified ID.
116
- if count == 0
173
+ if count.zero?
117
174
  original_id
118
175
  else
119
176
  "#{original_id}#{@id_copy_suffix}#{count}"
@@ -123,6 +180,7 @@ module Kitchen
123
180
  # Returns the underlying Nokogiri Document object
124
181
  #
125
182
  # @return [Nokogiri::XML::Document]
183
+ #
126
184
  def raw
127
185
  @nokogiri_document
128
186
  end
@@ -131,6 +189,7 @@ module Kitchen
131
189
 
132
190
  def next_count_for_pasted_id(id)
133
191
  return if id.blank?
192
+
134
193
  (@next_paste_count_for_id[id] ||= 0).tap do
135
194
  @next_paste_count_for_id[id] += 1
136
195
  end
@@ -1,6 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kitchen
4
+ # An one-off element that isn't one of the main elements we have dedicated classes
5
+ # for (like ChapterElement). Provides a way to set an arbitrary +short_type+
6
+ #
2
7
  class Element < ElementBase
3
8
 
9
+ # Creates a new +Element+
10
+ #
11
+ # @param node [Nokogiri::XML::Node] the node this element wraps
12
+ # @param document [Document] this element's document
13
+ # @param short_type [Symbol, String] the type for this element
14
+ #
4
15
  def initialize(node:, document:, short_type: nil)
5
16
  super(node: node,
6
17
  document: document,
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require 'securerandom'
3
5
 
4
6
  module Kitchen
5
-
6
7
  # Abstract base class for all elements. If you are looking for a simple concrete
7
8
  # element class, use `Element`.
8
9
  #
@@ -10,35 +11,101 @@ module Kitchen
10
11
  extend Forwardable
11
12
  include Mixins::BlockErrorIf
12
13
 
14
+ # The element's document
15
+ # @return [Document]
13
16
  attr_reader :document
17
+
18
+ # The element's type, e.g. +:page+
19
+ # @return [Symbol, String]
14
20
  attr_reader :short_type
21
+
22
+ # The enumerator class for this element
23
+ # @return [Class]
15
24
  attr_reader :enumerator_class
16
- attr_accessor :css_or_xpath_that_found_me
25
+
26
+ # The search query that located this element in the DOM
27
+ # @return [SearchQuery]
28
+ attr_accessor :search_query_that_found_me
17
29
 
18
30
  # @!method name
19
31
  # Get the element name (the tag)
32
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#name-instance_method Nokogiri::XML::Node#name
33
+ # @return [String]
20
34
  # @!method name=
21
35
  # Set the element name (the tag)
36
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#name=-instance_method Nokogiri::XML::Node#name=
22
37
  # @!method []
23
38
  # Get an element attribute
39
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#[]-instance_method Nokogiri::XML::Node#[]
40
+ # @return [String]
24
41
  # @!method []=
25
42
  # Set an element attribute
43
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#[]=-instance_method Nokogiri::XML::Node#[]=
26
44
  # @!method add_class
27
45
  # Add a class to the element
46
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#add_class-instance_method Nokogiri::XML::Node#add_class
28
47
  # @!method remove_class
29
48
  # Remove a class from the element
49
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#remove_class-instance_method Nokogiri::XML::Node#remove_class
50
+ # @!method text
51
+ # Get the element text
52
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#text-instance_method Nokogiri::XML::Node#text
53
+ # @return [String]
54
+ # @!method wrap
55
+ # Add HTML around this element
56
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#wrap-instance_method Nokogiri::XML::Node#wrap
57
+ # @return [Nokogiri::XML::Node]
58
+ # @!method children
59
+ # Get the element's children
60
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#children-instance_method Nokogiri::XML::Node#children
61
+ # @return [Nokogiri::XML::NodeSet]
62
+ # @!method to_html
63
+ # Get the element as HTML
64
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_html-instance_method Nokogiri::XML::Node#to_html
65
+ # @return [String]
66
+ # @!method remove_attribute
67
+ # Removes an attribute from the element
68
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#remove_attribute-instance_method Nokogiri::XML::Node#remove_attribute
69
+ # @!method classes
70
+ # Gets the element's classes
71
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#classes-instance_method Nokogiri::XML::Node#classes
72
+ # @return [Array<String>]
73
+ # @!method path
74
+ # Get the path for this element
75
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#path-instance_method Nokogiri::XML::Node#path
76
+ # @return [String]
77
+ # @!method inner_html=
78
+ # Set the inner HTML for this element
79
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#inner_html=-instance_method Nokogiri::XML::Node#inner_html=
80
+ # @return Object
30
81
  def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
31
- :text, :wrap, :children, :to_html, :remove_attribute,
32
- :classes, :path
82
+ :text, :wrap, :children, :to_html, :remove_attribute,
83
+ :classes, :path, :inner_html=
33
84
 
85
+ # @!method config
86
+ # Get the config for this element's document
87
+ # @return [Config]
34
88
  def_delegators :document, :config
89
+
90
+ # @!method selectors
91
+ # Get the selectors for this element's document
92
+ # @return [Selectors::Base]
35
93
  def_delegators :config, :selectors
36
94
 
95
+ # Creates a new instance
96
+ #
97
+ # @param node [Nokogiri::XML::Node] the wrapped element
98
+ # @param document [Document] the element's document
99
+ # @param enumerator_class [ElementEnumeratorBase] the enumerator that matches this element type
100
+ # @param short_type [Symbol, String] the type of this element
101
+ #
37
102
  def initialize(node:, document:, enumerator_class:, short_type: nil)
38
- raise(ArgumentError, "node cannot be nil") if node.nil?
103
+ raise(ArgumentError, 'node cannot be nil') if node.nil?
104
+
39
105
  @node = node
40
106
 
41
- raise(ArgumentError, "enumerator_class cannot be nil") if enumerator_class.nil?
107
+ raise(ArgumentError, 'enumerator_class cannot be nil') if enumerator_class.nil?
108
+
42
109
  @enumerator_class = enumerator_class
43
110
 
44
111
  @short_type = short_type || "unknown_type_#{SecureRandom.hex(4)}"
@@ -48,33 +115,57 @@ module Kitchen
48
115
  when Kitchen::Document
49
116
  document
50
117
  else
51
- raise(ArgumentError, "`document` is not a known document type")
118
+ raise(ArgumentError, '`document` is not a known document type')
52
119
  end
53
120
 
54
121
  @ancestors = HashWithIndifferentAccess.new
55
- @counts_in = HashWithIndifferentAccess.new
56
- @css_or_xpath_that_has_been_counted = {}
122
+ @search_query_matches_that_have_been_counted = {}
57
123
  @is_a_clone = false
58
124
  end
59
125
 
60
- def self.is_the_element_class_for?(node)
126
+ # Returns true if this class represents the element for the given node
127
+ #
128
+ # @param node [Nokogiri::XML::Node] the underlying node
129
+ # @return [Boolean]
130
+ #
131
+ def self.is_the_element_class_for?(_node)
61
132
  # override this in subclasses
62
133
  false
63
134
  end
64
135
 
136
+ # Returns true if this element has the given class
137
+ #
138
+ # @param klass [String] the class to test for
139
+ # @return [Boolean]
140
+ #
65
141
  def has_class?(klass)
66
- (self[:class] || "").include?(klass)
142
+ (self[:class] || '').include?(klass)
67
143
  end
68
144
 
145
+ # Returns the element's ID
146
+ #
147
+ # @return [String]
148
+ #
69
149
  def id
70
150
  self[:id]
71
151
  end
72
152
 
153
+ # Sets the element's ID
154
+ #
155
+ # @param value [String] the new value for the ID
156
+ #
73
157
  def id=(value)
74
158
  self[:id] = value
75
159
  end
76
160
 
77
161
  # A way to set values and chain them
162
+ #
163
+ # @param property [String, Symbol] the name of the property to set
164
+ # @param value [String] the value to set
165
+ #
166
+ # @example
167
+ # element.set(:name,"div").set("id","foo")
168
+ #
78
169
  def set(property, value)
79
170
  case property.to_sym
80
171
  when :name
@@ -85,18 +176,37 @@ module Kitchen
85
176
  self
86
177
  end
87
178
 
179
+ # Returns this element's ancestor of the given type
180
+ #
181
+ # @param type [String, Symbol] e.g. +:page+, +:term+
182
+ # @return [Ancestor]
183
+ # @raise [StandardError] if there is no ancestor of the given type
184
+ #
88
185
  def ancestor(type)
89
186
  @ancestors[type.to_sym]&.element || raise("No ancestor of type '#{type}'")
90
187
  end
91
188
 
189
+ # Returns true iff this element has an ancestor of the given type
190
+ #
191
+ # @param type [String, Symbol] e.g. +:page+, +:term+
192
+ # @return [Boolean]
193
+ #
92
194
  def has_ancestor?(type)
93
195
  @ancestors[type.to_sym].present?
94
196
  end
95
197
 
96
- def ancestors
97
- @ancestors
98
- end
198
+ # Returns the element's ancestors
199
+ #
200
+ # @return [Array<Ancestor>]
201
+ #
202
+ attr_reader :ancestors
99
203
 
204
+ # Adds ancestors to this element, for each incrementing descendant counts for this type
205
+ #
206
+ # @param args [Array<Hash, Ancestor, Element, Document>] the ancestors
207
+ # @raise [StandardError] if there is already an ancestor with the one of
208
+ # the given ancestors' types
209
+ #
100
210
  def add_ancestors(*args)
101
211
  args.each do |arg|
102
212
  case arg
@@ -112,50 +222,93 @@ module Kitchen
112
222
  end
113
223
  end
114
224
 
225
+ # Adds one ancestor, incrementing its descendant counts for this element type
226
+ #
227
+ # @param ancestor [Ancestor]
228
+ # @raise [StandardError] if there is already an ancestor with the given ancestor's type
229
+ #
115
230
  def add_ancestor(ancestor)
116
231
  if @ancestors[ancestor.type].present?
117
232
  raise "Trying to add an ancestor of type '#{ancestor.type}' but one of that " \
118
233
  "type is already present"
119
234
  end
120
235
 
236
+ ancestor.increment_descendant_count(short_type)
121
237
  @ancestors[ancestor.type] = ancestor
122
238
  end
123
239
 
240
+ # Return the elements in all of the ancestors
241
+ #
242
+ # @return [Array<ElementBase>]
243
+ #
124
244
  def ancestor_elements
125
245
  @ancestors.values.map(&:element)
126
246
  end
127
247
 
128
- def count_as_descendant
129
- @ancestors.each_pair do |type, ancestor|
130
- @counts_in[type] = ancestor.increment_descendant_count(short_type)
131
- end
132
- end
133
-
248
+ # Returns the count of this element's type in the given ancestor type
249
+ #
250
+ # @param ancestor_type [String, Symbol]
251
+ #
134
252
  def count_in(ancestor_type)
135
- @counts_in[ancestor_type] || raise("No ancestor of type '#{ancestor_type}'")
253
+ @ancestors[ancestor_type]&.get_descendant_count(short_type) ||
254
+ raise("No ancestor of type '#{ancestor_type}'")
136
255
  end
137
256
 
138
- def remember_that_sub_elements_are_already_counted(css_or_xpath:, count:)
139
- @css_or_xpath_that_has_been_counted[css_or_xpath] = count
140
- end
141
-
142
- def have_sub_elements_already_been_counted?(css_or_xpath)
143
- number_of_sub_elements_already_counted(css_or_xpath) != 0
257
+ # Track that a sub element found by the given query has been counted
258
+ #
259
+ # @param search_query [SearchQuery] the search query matching the counted element
260
+ # @param type [String] the type of the sub element that was counted
261
+ #
262
+ def remember_that_a_sub_element_was_counted(search_query, type)
263
+ @search_query_matches_that_have_been_counted[search_query.to_s] ||= Hash.new(0)
264
+ @search_query_matches_that_have_been_counted[search_query.to_s][type] += 1
144
265
  end
145
266
 
146
- def number_of_sub_elements_already_counted(css_or_xpath)
147
- @css_or_xpath_that_has_been_counted[css_or_xpath] || 0
267
+ # Undo the counts from a prior search query (so that they can be counted again)
268
+ #
269
+ # @param search_query [SearchQuery] the prior search query whose counts need to be undone
270
+ #
271
+ def uncount(search_query)
272
+ @search_query_matches_that_have_been_counted.delete(search_query.to_s)&.each do |type, count|
273
+ ancestors.each_value do |ancestor|
274
+ ancestor.decrement_descendant_count(type, by: count)
275
+ end
276
+ end
148
277
  end
149
278
 
279
+ # Returns the search history that found this element
280
+ #
281
+ # @return [SearchHistory]
282
+ #
150
283
  def search_history
151
- history = ancestor_elements.map(&:css_or_xpath_that_found_me) + [css_or_xpath_that_found_me]
152
- history.compact.join(" ")
284
+ SearchHistory.new(
285
+ ancestor_elements.last&.search_history || SearchHistory.empty,
286
+ search_query_that_found_me
287
+ )
153
288
  end
154
289
 
155
- def search(*selector_or_xpath_args)
290
+ # Returns an ElementEnumerator that iterates over the provided selector or xpath queries
291
+ #
292
+ # @param selector_or_xpath_args [Array<String>] Selector or XPath queries
293
+ # @param only [Symbol, Callable] the name of a method to call on an element or a
294
+ # lambda or proc that accepts an element; elements will only be included in the
295
+ # search results if the method or callable returns true
296
+ # @param except [Symbol, Callable] the name of a method to call on an element or a
297
+ # lambda or proc that accepts an element; elements will not be included in the
298
+ # search results if the method or callable returns false
299
+ # @return [ElementEnumerator]
300
+ #
301
+ def search(*selector_or_xpath_args, only: nil, except: nil)
156
302
  block_error_if(block_given?)
157
303
 
158
- ElementEnumerator.factory.build_within(self, css_or_xpath: selector_or_xpath_args)
304
+ ElementEnumerator.factory.build_within(
305
+ self,
306
+ search_query: SearchQuery.new(
307
+ css_or_xpath: selector_or_xpath_args,
308
+ only: only,
309
+ except: except
310
+ )
311
+ )
159
312
  end
160
313
 
161
314
  # Yields and returns the first child element that matches the provided
@@ -185,16 +338,32 @@ module Kitchen
185
338
  end
186
339
  end
187
340
 
341
+ # @!method at
342
+ # @see first
188
343
  alias_method :at, :first
189
344
 
190
- def element_children()
345
+ # Returns an enumerator over the direct child elements of this element, with the
346
+ # specific type (e.g. +TermElement+) if such type is available.
347
+ #
348
+ # @return [TypeCastingElementEnumerator]
349
+ #
350
+ def element_children
191
351
  block_error_if(block_given?)
192
- TypeCastingElementEnumerator.factory.build_within(self, css_or_xpath: "./*")
352
+ TypeCastingElementEnumerator.factory.build_within(
353
+ self,
354
+ search_query: SearchQuery.new(css_or_xpath: './*')
355
+ )
193
356
  end
194
357
 
358
+ # Searches for elements handled by a list of enumerator classes. All element that
359
+ # matches one of those enumerator classes are iterated over.
360
+ #
361
+ # @param enumerator_classes [Array<ElementEnumeratorBase>]
362
+ # @return [TypeCastingElementEnumerator]
363
+ #
195
364
  def search_with(*enumerator_classes)
196
365
  block_error_if(block_given?)
197
- raise "must supply at least one enumerator class" if enumerator_classes.empty?
366
+ raise 'must supply at least one enumerator class' if enumerator_classes.empty?
198
367
 
199
368
  factory = enumerator_classes[0].factory
200
369
  enumerator_classes[1..-1].each do |enumerator_class|
@@ -232,6 +401,7 @@ module Kitchen
232
401
  the_copy = clone
233
402
  the_copy.raw.traverse do |node|
234
403
  next if node.text? || node.document?
404
+
235
405
  document.record_id_copied(node[:id])
236
406
  end
237
407
  clipboard(to).add(the_copy) if to.present?
@@ -247,6 +417,7 @@ module Kitchen
247
417
  temp_copy = clone
248
418
  temp_copy.raw.traverse do |node|
249
419
  next if node.text? || node.document?
420
+
250
421
  node[:id] = document.modified_id_to_paste(node[:id]) unless node[:id].blank?
251
422
  end
252
423
  temp_copy.to_s
@@ -263,20 +434,19 @@ module Kitchen
263
434
  Element.new(node: raw.parent, document: document, short_type: "parent(#{short_type})")
264
435
  end
265
436
 
266
- # TODO make it clear if all of these methods take Element, Node, or String
437
+ # TODO: make it clear if all of these methods take Element, Node, or String
267
438
 
268
439
  # If child argument given, prepends it before the element's current children.
269
440
  # If sibling is given, prepends it as a sibling to this element.
270
441
  #
271
442
  # @param child [String] the child to prepend
272
443
  # @param sibling [String] the sibling to prepend
444
+ # @raise [RecipeError] if specify other than just a child or a sibling
273
445
  #
274
446
  def prepend(child: nil, sibling: nil)
275
- if child && sibling
276
- raise RecipeError, "Only one of `child` or `sibling` can be specified"
277
- elsif !child && !sibling
278
- raise RecipeError, "One of `child` or `sibling` must be specified"
279
- elsif child
447
+ require_one_of_child_or_sibling(child, sibling)
448
+
449
+ if child
280
450
  if node.children.empty?
281
451
  node.children = child.to_s
282
452
  else
@@ -285,6 +455,7 @@ module Kitchen
285
455
  else
286
456
  node.add_previous_sibling(sibling)
287
457
  end
458
+
288
459
  self
289
460
  end
290
461
 
@@ -293,13 +464,12 @@ module Kitchen
293
464
  #
294
465
  # @param child [String] the child to append
295
466
  # @param sibling [String] the sibling to append
467
+ # @raise [RecipeError] if specify other than just a child or a sibling
296
468
  #
297
469
  def append(child: nil, sibling: nil)
298
- if child && sibling
299
- raise RecipeError, "Only one of `child` or `sibling` can be specified"
300
- elsif !child && !sibling
301
- raise RecipeError, "One of `child` or `sibling` must be specified"
302
- elsif child
470
+ require_one_of_child_or_sibling(child, sibling)
471
+
472
+ if child
303
473
  if node.children.empty?
304
474
  node.children = child.to_s
305
475
  else
@@ -308,6 +478,7 @@ module Kitchen
308
478
  else
309
479
  node.next = sibling
310
480
  end
481
+
311
482
  self
312
483
  end
313
484
 
@@ -320,7 +491,7 @@ module Kitchen
320
491
  self
321
492
  end
322
493
 
323
- # TODO methods like replace_children that take string, either forbid or handle Element/Node args
494
+ # TODO: methods like replace_children that take string, either forbid or handle Element/Node args
324
495
 
325
496
  # Get the content of children matching the provided selector. Mostly
326
497
  # useful when there is one child with text you want to extract.
@@ -350,11 +521,19 @@ module Kitchen
350
521
  # @return [String] the sub header tag name
351
522
  #
352
523
  def sub_header_name
353
- first_header = node.search("h1, h2, h3, h4, h5, h6").first
524
+ first_header = node.search('h1, h2, h3, h4, h5, h6').first
525
+
526
+ if first_header.nil?
527
+ 'h1'
528
+ else
529
+ first_header.name.gsub(/\d/) { |num| (num.to_i + 1).to_s }
530
+ end
531
+ end
354
532
 
355
- first_header.nil? ?
356
- "h1" :
357
- first_header.name.gsub(/\d/) {|num| (num.to_i + 1).to_s}
533
+ # Mark the location so that if there's an error we can show the developer where.
534
+ #
535
+ def mark_as_current_location!
536
+ document.location = self
358
537
  end
359
538
 
360
539
  # Returns the underlying Nokogiri object
@@ -365,22 +544,40 @@ module Kitchen
365
544
  node
366
545
  end
367
546
 
547
+ # Returns a string version of this element
548
+ #
549
+ # @return [String]
550
+ #
368
551
  def inspect
369
552
  to_s
370
553
  end
371
554
 
555
+ # Returns a string version of this element
556
+ #
557
+ # @return [String]
558
+ #
372
559
  def to_s
373
560
  remove_default_namespaces_if_clone(node.to_s)
374
561
  end
375
562
 
563
+ # Returns a string version of this element as XML
564
+ #
565
+ # @return [String]
566
+ #
376
567
  def to_xml
377
568
  remove_default_namespaces_if_clone(node.to_xml)
378
569
  end
379
570
 
571
+ # Returns a string version of this element as XHTML
572
+ #
573
+ # @return [String]
574
+ #
380
575
  def to_xhtml
381
576
  remove_default_namespaces_if_clone(node.to_xhtml)
382
577
  end
383
578
 
579
+ # Returns a clone of this object
580
+ #
384
581
  def clone
385
582
  super.tap do |element|
386
583
  # When we call dup, the dup gets a bunch of default namespace stuff that
@@ -409,17 +606,32 @@ module Kitchen
409
606
 
410
607
  # @!method pages
411
608
  # Returns a pages enumerator
412
- def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples
609
+ def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples,
610
+ :metadatas, :units
413
611
 
612
+ # Returns this element as an enumerator (over only one element, itself)
613
+ #
614
+ # @return [ElementEnumeratorBase] (actually returns the appropriate enumerator class for this element)
615
+ #
414
616
  def as_enumerator
415
- enumerator_class.new(css_or_xpath: css_or_xpath_that_found_me) {|block| block.yield(self)}
617
+ enumerator_class.new(search_query: search_query_that_found_me) { |block| block.yield(self) }
416
618
  end
417
619
 
418
620
  protected
419
621
 
622
+ # The wrapped Nokogiri node
623
+ # @return [Nokogiri::XML::Node] the node
420
624
  attr_accessor :node
625
+
626
+ # If this element is a clone
627
+ # @return [Boolean]
421
628
  attr_accessor :is_a_clone
422
629
 
630
+ # Return a clipboard
631
+ #
632
+ # @param name_or_object [String, Clipboard] the name of the clipboard or the clipboard itself
633
+ # @return [Clipboard]
634
+ #
423
635
  def clipboard(name_or_object)
424
636
  case name_or_object
425
637
  when Symbol
@@ -427,18 +639,26 @@ module Kitchen
427
639
  when Clipboard
428
640
  name_or_object
429
641
  else
430
- raise ArgumentError, "The provided argument (#{name_or_object}) is not "
642
+ raise ArgumentError, "The provided argument (#{name_or_object}) is not " \
431
643
  "a clipboard name or a clipboard"
432
644
  end
433
645
  end
434
646
 
647
+ # Clean up some default namespace junk for cloned elements
648
+ #
649
+ # @param string [String] the string to clean
435
650
  def remove_default_namespaces_if_clone(string)
436
651
  if is_a_clone
437
- string.gsub("xmlns:default=\"http://www.w3.org/1999/xhtml\"","").gsub("default:","")
652
+ string.gsub('xmlns:default="http://www.w3.org/1999/xhtml"', '').gsub('default:', '')
438
653
  else
439
654
  string
440
655
  end
441
656
  end
442
657
 
658
+ def require_one_of_child_or_sibling(child, sibling)
659
+ raise RecipeError, 'Only one of `child` or `sibling` can be specified' if child && sibling
660
+ raise RecipeError, 'One of `child` or `sibling` must be specified' if !child && !sibling
661
+ end
662
+
443
663
  end
444
664
  end