openstax_kitchen 2.0.0 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (181) 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 +2 -2
  6. data/.inch.yml +6 -0
  7. data/.rubocop.yml +65 -0
  8. data/CHANGELOG.md +85 -1
  9. data/Gemfile +5 -3
  10. data/Gemfile.lock +65 -17
  11. data/README.md +65 -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 +15 -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 +40 -5
  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 -6
  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 +21 -6
  31. data/lib/kitchen/composite_page_element.rb +35 -7
  32. data/lib/kitchen/composite_page_element_enumerator.rb +9 -1
  33. data/lib/kitchen/config.rb +20 -6
  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/main.rb +18 -0
  39. data/lib/kitchen/directions/bake_chapter_glossary/v1.rb +30 -0
  40. data/lib/kitchen/directions/bake_chapter_introductions.rb +7 -7
  41. data/lib/kitchen/directions/bake_chapter_key_concepts/main.rb +16 -0
  42. data/lib/kitchen/directions/bake_chapter_key_concepts/v1.rb +35 -0
  43. data/lib/kitchen/directions/bake_chapter_key_equations.rb +30 -20
  44. data/lib/kitchen/directions/bake_chapter_references/main.rb +16 -0
  45. data/lib/kitchen/directions/bake_chapter_references/v1.rb +35 -0
  46. data/lib/kitchen/directions/bake_chapter_section_exercises/main.rb +11 -0
  47. data/lib/kitchen/directions/bake_chapter_section_exercises/v1.rb +28 -0
  48. data/lib/kitchen/directions/bake_chapter_summary.rb +45 -36
  49. data/lib/kitchen/directions/bake_chapter_title/main.rb +11 -0
  50. data/lib/kitchen/directions/bake_chapter_title/v1.rb +24 -0
  51. data/lib/kitchen/directions/bake_checkpoint.rb +44 -0
  52. data/lib/kitchen/directions/bake_composite_chapters.rb +14 -0
  53. data/lib/kitchen/directions/bake_composite_pages.rb +2 -2
  54. data/lib/kitchen/directions/bake_equations.rb +37 -0
  55. data/lib/kitchen/directions/bake_example.rb +39 -11
  56. data/lib/kitchen/directions/bake_figure.rb +8 -5
  57. data/lib/kitchen/directions/bake_first_elements.rb +16 -0
  58. data/lib/kitchen/directions/bake_footnotes/main.rb +2 -2
  59. data/lib/kitchen/directions/bake_footnotes/v1.rb +6 -5
  60. data/lib/kitchen/directions/bake_free_response/free_response.xhtml.erb +10 -0
  61. data/lib/kitchen/directions/bake_free_response/main.rb +11 -0
  62. data/lib/kitchen/directions/bake_free_response/v1.rb +29 -0
  63. data/lib/kitchen/directions/bake_further_research.rb +59 -0
  64. data/lib/kitchen/directions/bake_index/main.rb +2 -2
  65. data/lib/kitchen/directions/bake_index/v1.rb +46 -18
  66. data/lib/kitchen/directions/bake_link_placeholders.rb +24 -0
  67. data/lib/kitchen/directions/bake_math_in_paragraph.rb +5 -3
  68. data/lib/kitchen/directions/bake_non_introduction_pages.rb +26 -0
  69. data/lib/kitchen/directions/bake_notes/bake_autotitled_notes.rb +29 -0
  70. data/lib/kitchen/directions/bake_notes/bake_note_subtitle.rb +22 -0
  71. data/lib/kitchen/directions/bake_notes/bake_numbered_notes.rb +51 -0
  72. data/lib/kitchen/directions/bake_notes/bake_unclassified_notes.rb +30 -0
  73. data/lib/kitchen/directions/bake_numbered_exercise/main.rb +15 -0
  74. data/lib/kitchen/directions/bake_numbered_exercise/v1.rb +47 -0
  75. data/lib/kitchen/directions/bake_numbered_table/main.rb +4 -4
  76. data/lib/kitchen/directions/bake_numbered_table/v1.rb +37 -18
  77. data/lib/kitchen/directions/bake_page_abstracts.rb +30 -0
  78. data/lib/kitchen/directions/bake_preface/main.rb +11 -0
  79. data/lib/kitchen/directions/bake_preface/v1.rb +18 -0
  80. data/lib/kitchen/directions/bake_references/main.rb +16 -0
  81. data/lib/kitchen/directions/bake_references/v1.rb +48 -0
  82. data/lib/kitchen/directions/bake_stepwise.rb +8 -12
  83. data/lib/kitchen/directions/bake_suggested_reading.rb +31 -0
  84. data/lib/kitchen/directions/bake_theorem/main.rb +11 -0
  85. data/lib/kitchen/directions/bake_theorem/v1.rb +28 -0
  86. data/lib/kitchen/directions/bake_toc.rb +49 -22
  87. data/lib/kitchen/directions/bake_unit_title/main.rb +11 -0
  88. data/lib/kitchen/directions/bake_unit_title/v1.rb +23 -0
  89. data/lib/kitchen/directions/bake_unnumbered_tables.rb +7 -5
  90. data/lib/kitchen/directions/book_answer_key_container/eob_solutions_container.xhtml.erb +9 -0
  91. data/lib/kitchen/directions/book_answer_key_container/main.rb +11 -0
  92. data/lib/kitchen/directions/book_answer_key_container/v1.rb +13 -0
  93. data/lib/kitchen/directions/chapter_review_container/chapter_review.xhtml.erb +9 -0
  94. data/lib/kitchen/directions/chapter_review_container/main.rb +11 -0
  95. data/lib/kitchen/directions/chapter_review_container/v1.rb +13 -0
  96. data/lib/kitchen/directions/eoc_section_title_link_snippet.rb +20 -0
  97. data/lib/kitchen/directions/move_exercises_to_eoc/main.rb +27 -0
  98. data/lib/kitchen/directions/move_exercises_to_eoc/v1.rb +36 -0
  99. data/lib/kitchen/directions/move_exercises_to_eoc/v2.rb +49 -0
  100. data/lib/kitchen/directions/move_solutions_to_answer_key/main.rb +14 -0
  101. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/american_government.rb +19 -0
  102. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/calculus.rb +41 -0
  103. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/uphysics.rb +63 -0
  104. data/lib/kitchen/directions/move_solutions_to_answer_key/v1.rb +34 -0
  105. data/lib/kitchen/directions/move_title_text_into_span.rb +2 -2
  106. data/lib/kitchen/document.rb +83 -13
  107. data/lib/kitchen/element.rb +20 -3
  108. data/lib/kitchen/element_base.rb +373 -63
  109. data/lib/kitchen/element_enumerator.rb +8 -0
  110. data/lib/kitchen/element_enumerator_base.rb +297 -28
  111. data/lib/kitchen/element_enumerator_factory.rb +64 -53
  112. data/lib/kitchen/element_factory.rb +27 -12
  113. data/lib/kitchen/errors.rb +5 -0
  114. data/lib/kitchen/example_element.rb +21 -6
  115. data/lib/kitchen/example_element_enumerator.rb +9 -1
  116. data/lib/kitchen/exercise_element.rb +42 -0
  117. data/lib/kitchen/exercise_element_enumerator.rb +21 -0
  118. data/lib/kitchen/figure_element.rb +36 -5
  119. data/lib/kitchen/figure_element_enumerator.rb +9 -1
  120. data/lib/kitchen/metadata_element.rb +34 -0
  121. data/lib/kitchen/metadata_element_enumerator.rb +21 -0
  122. data/lib/kitchen/mixins/block_error_if.rb +24 -4
  123. data/lib/kitchen/note_element.rb +48 -20
  124. data/lib/kitchen/note_element_enumerator.rb +9 -1
  125. data/lib/kitchen/oven.rb +66 -15
  126. data/lib/kitchen/page_element.rb +84 -14
  127. data/lib/kitchen/page_element_enumerator.rb +9 -1
  128. data/lib/kitchen/pantry.rb +28 -1
  129. data/lib/kitchen/patches/nokogiri.rb +59 -2
  130. data/lib/kitchen/patches/renderable.rb +9 -3
  131. data/lib/kitchen/patches/string.rb +8 -0
  132. data/lib/kitchen/recipe.rb +69 -32
  133. data/lib/kitchen/reference_element.rb +27 -0
  134. data/lib/kitchen/references_element_enumerator.rb +20 -0
  135. data/lib/kitchen/search_history.rb +43 -4
  136. data/lib/kitchen/search_query.rb +106 -0
  137. data/lib/kitchen/selector.rb +24 -0
  138. data/lib/kitchen/selectors/base.rb +65 -0
  139. data/lib/kitchen/selectors/standard_1.rb +21 -0
  140. data/lib/kitchen/table_element.rb +55 -7
  141. data/lib/kitchen/table_element_enumerator.rb +9 -1
  142. data/lib/kitchen/templates/eob_section_title_template.xhtml.erb +10 -0
  143. data/lib/kitchen/templates/eoc_section_title_template.xhtml.erb +10 -0
  144. data/lib/kitchen/term_element.rb +15 -4
  145. data/lib/kitchen/term_element_enumerator.rb +9 -1
  146. data/lib/kitchen/transliterations.rb +7 -5
  147. data/lib/kitchen/type_casting_element_enumerator.rb +17 -1
  148. data/lib/kitchen/unit_element.rb +45 -0
  149. data/lib/kitchen/unit_element_enumerator.rb +20 -0
  150. data/lib/kitchen/utils.rb +10 -13
  151. data/lib/kitchen/version.rb +5 -1
  152. data/lib/locales/en.yml +18 -7
  153. data/lib/locales/pl.yml +24 -0
  154. data/lib/openstax_kitchen.rb +44 -42
  155. data/openstax_kitchen.gemspec +26 -20
  156. data/tutorials/00/solution1.rb +9 -0
  157. data/tutorials/00/solution2.rb +8 -0
  158. data/tutorials/01/solution1.rb +18 -0
  159. data/tutorials/01/solution2.rb +26 -0
  160. data/tutorials/02/solution1.rb +31 -0
  161. data/tutorials/03/{solution_1.rb → solution1.rb} +6 -4
  162. data/tutorials/03/solution2.rb +18 -0
  163. data/tutorials/04/{solution_1.rb → solution1.rb} +4 -2
  164. data/tutorials/04/{solution_2.rb → solution2.rb} +6 -4
  165. data/tutorials/05/solution1.rb +11 -0
  166. data/tutorials/check_it +16 -15
  167. data/tutorials/setup_my_recipes +7 -6
  168. metadata +149 -24
  169. data/Dockerfile +0 -19
  170. data/docker-compose.yml +0 -12
  171. data/docker/entrypoint +0 -9
  172. data/lib/kitchen/directions/bake_chapter_glossary.rb +0 -34
  173. data/lib/kitchen/directions/bake_exercises.rb +0 -164
  174. data/lib/kitchen/directions/bake_notes.rb +0 -58
  175. data/tutorials/00/solution_1.rb +0 -7
  176. data/tutorials/00/solution_2.rb +0 -6
  177. data/tutorials/01/solution_1.rb +0 -16
  178. data/tutorials/01/solution_2.rb +0 -24
  179. data/tutorials/02/solution_1.rb +0 -29
  180. data/tutorials/03/solution_2.rb +0 -15
  181. data/tutorials/05/solution_1.rb +0 -9
@@ -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,
@@ -8,8 +19,14 @@ module Kitchen
8
19
  short_type: short_type)
9
20
  end
10
21
 
11
- # # @!method pages
12
- # # Returns a pages enumerator
13
- # def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples
22
+ # Returns true if this class represents the element for the given node; always false
23
+ # for this generic class
24
+ #
25
+ # @param node [Nokogiri::XML::Node] the underlying node
26
+ # @return [Boolean]
27
+ #
28
+ def self.is_the_element_class_for?(_node, **)
29
+ false
30
+ end
14
31
  end
15
32
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'forwardable'
2
4
  require 'securerandom'
3
5
 
6
+ # rubocop:disable Metrics/ClassLength
4
7
  module Kitchen
5
-
6
8
  # Abstract base class for all elements. If you are looking for a simple concrete
7
9
  # element class, use `Element`.
8
10
  #
@@ -10,71 +12,207 @@ module Kitchen
10
12
  extend Forwardable
11
13
  include Mixins::BlockErrorIf
12
14
 
15
+ # The element's document
16
+ # @return [Document]
13
17
  attr_reader :document
18
+
19
+ # The element's type, e.g. +:page+
20
+ # @return [Symbol, String]
14
21
  attr_reader :short_type
22
+
23
+ # The enumerator class for this element
24
+ # @return [Class]
15
25
  attr_reader :enumerator_class
16
- attr_accessor :css_or_xpath_that_found_me
26
+
27
+ # The search query that located this element in the DOM
28
+ # @return [SearchQuery]
29
+ attr_accessor :search_query_that_found_me
17
30
 
18
31
  # @!method name
19
32
  # Get the element name (the tag)
33
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#name-instance_method Nokogiri::XML::Node#name
34
+ # @return [String]
20
35
  # @!method name=
21
36
  # Set the element name (the tag)
37
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#name=-instance_method Nokogiri::XML::Node#name=
22
38
  # @!method []
23
39
  # Get an element attribute
40
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#[]-instance_method Nokogiri::XML::Node#[]
41
+ # @return [String]
24
42
  # @!method []=
25
43
  # Set an element attribute
44
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#[]=-instance_method Nokogiri::XML::Node#[]=
26
45
  # @!method add_class
27
46
  # Add a class to the element
47
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#add_class-instance_method Nokogiri::XML::Node#add_class
28
48
  # @!method remove_class
29
49
  # Remove a class from the element
50
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#remove_class-instance_method Nokogiri::XML::Node#remove_class
51
+ # @!method text
52
+ # Get the element text
53
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#text-instance_method Nokogiri::XML::Node#text
54
+ # @return [String]
55
+ # @!method wrap
56
+ # Add HTML around this element
57
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#wrap-instance_method Nokogiri::XML::Node#wrap
58
+ # @return [Nokogiri::XML::Node]
59
+ # @!method children
60
+ # Get the element's children
61
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#children-instance_method Nokogiri::XML::Node#children
62
+ # @return [Nokogiri::XML::NodeSet]
63
+ # @!method to_html
64
+ # Get the element as HTML
65
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_html-instance_method Nokogiri::XML::Node#to_html
66
+ # @return [String]
67
+ # @!method remove_attribute
68
+ # Removes an attribute from the element
69
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#remove_attribute-instance_method Nokogiri::XML::Node#remove_attribute
70
+ # @!method classes
71
+ # Gets the element's classes
72
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#classes-instance_method Nokogiri::XML::Node#classes
73
+ # @return [Array<String>]
74
+ # @!method path
75
+ # Get the path for this element
76
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#path-instance_method Nokogiri::XML::Node#path
77
+ # @return [String]
78
+ # @!method inner_html=
79
+ # Set the inner HTML for this element
80
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#inner_html=-instance_method Nokogiri::XML::Node#inner_html=
81
+ # @return Object
30
82
  def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
31
- :text, :wrap, :children, :to_html, :remove_attribute,
32
- :classes, :path
83
+ :text, :wrap, :children, :to_html, :remove_attribute,
84
+ :classes, :path, :inner_html=
33
85
 
86
+ # @!method config
87
+ # Get the config for this element's document
88
+ # @return [Config]
34
89
  def_delegators :document, :config
90
+
91
+ # @!method selectors
92
+ # Get the selectors for this element's document
93
+ # @return [Selectors::Base]
35
94
  def_delegators :config, :selectors
36
95
 
96
+ # @!method pantry
97
+ # Access the pantry for this element's document
98
+ # @return [Pantry]
99
+ # @!method :clipboard
100
+ # Access the clipboard for this element's document
101
+ # @return [Clipboard]
102
+ def_delegators :document, :pantry, :clipboard
103
+
104
+ # Creates a new instance
105
+ #
106
+ # @param node [Nokogiri::XML::Node] the wrapped element
107
+ # @param document [Document] the element's document
108
+ # @param enumerator_class [ElementEnumeratorBase] the enumerator that matches this element type
109
+ # @param short_type [Symbol, String] the type of this element
110
+ #
37
111
  def initialize(node:, document:, enumerator_class:, short_type: nil)
38
- raise(ArgumentError, "node cannot be nil") if node.nil?
112
+ raise(ArgumentError, 'node cannot be nil') if node.nil?
113
+
39
114
  @node = node
40
115
 
41
- raise(ArgumentError, "enumerator_class cannot be nil") if enumerator_class.nil?
116
+ raise(ArgumentError, 'enumerator_class cannot be nil') if enumerator_class.nil?
117
+
42
118
  @enumerator_class = enumerator_class
43
119
 
44
- @short_type = short_type || "unknown_type_#{SecureRandom.hex(4)}"
120
+ @short_type = short_type ||
121
+ self.class.try(:short_type) ||
122
+ "unknown_type_#{SecureRandom.hex(4)}"
45
123
 
46
124
  @document =
47
125
  case document
48
126
  when Kitchen::Document
49
127
  document
50
128
  else
51
- raise(ArgumentError, "`document` is not a known document type")
129
+ raise(ArgumentError, '`document` is not a known document type')
52
130
  end
53
131
 
54
132
  @ancestors = HashWithIndifferentAccess.new
55
- @counts_in = HashWithIndifferentAccess.new
56
- @css_or_xpath_that_has_been_counted = {}
133
+ @search_query_matches_that_have_been_counted = {}
57
134
  @is_a_clone = false
58
135
  end
59
136
 
60
- def self.is_the_element_class_for?(node)
61
- # override this in subclasses
62
- false
137
+ # Returns ElementBase descendent type or nil if none found
138
+ #
139
+ # @param type [Symbol] the descendant type, e.g. `:page`
140
+ # @return [Class] the child class for the given type
141
+ #
142
+ def self.descendant(type)
143
+ @types_to_descendants ||=
144
+ descendants.each_with_object({}) do |descendant, hash|
145
+ next unless descendant.try(:short_type)
146
+
147
+ hash[descendant.short_type] = descendant
148
+ end
149
+
150
+ @types_to_descendants[type]
63
151
  end
64
152
 
153
+ # Returns ElementBase descendent type or Error if none found
154
+ #
155
+ # @param type [Symbol] the descendant type, e.g. `:page`
156
+ # @raise if the type is unknown
157
+ # @return [Class] the child class for the given type
158
+ #
159
+ def self.descendant!(type)
160
+ descendant(type) || raise("Unknown ElementBase descendant type '#{type}'")
161
+ end
162
+
163
+ # Returns true if this element is the given type
164
+ #
165
+ # @param type [Symbol] the descendant type, e.g. `:page`
166
+ # @raise if the type is unknown
167
+ # @return [Boolean]
168
+ #
169
+ def is?(type)
170
+ ElementBase.descendant!(type).is_the_element_class_for?(raw, config: config)
171
+ end
172
+
173
+ # Returns true if this class represents the element for the given node
174
+ #
175
+ # @param node [Nokogiri::XML::Node] the underlying node
176
+ # @param config [Kitchen::Config]
177
+ # @return [Boolean]
178
+ #
179
+ def self.is_the_element_class_for?(node, config:)
180
+ Selector.named(short_type).matches?(node, config: config)
181
+ end
182
+
183
+ # Returns true if this element has the given class
184
+ #
185
+ # @param klass [String] the class to test for
186
+ # @return [Boolean]
187
+ #
65
188
  def has_class?(klass)
66
- (self[:class] || "").include?(klass)
189
+ (self[:class] || '').include?(klass)
67
190
  end
68
191
 
192
+ # Returns the element's ID
193
+ #
194
+ # @return [String]
195
+ #
69
196
  def id
70
197
  self[:id]
71
198
  end
72
199
 
200
+ # Sets the element's ID
201
+ #
202
+ # @param value [String] the new value for the ID
203
+ #
73
204
  def id=(value)
74
205
  self[:id] = value
75
206
  end
76
207
 
77
208
  # A way to set values and chain them
209
+ #
210
+ # @param property [String, Symbol] the name of the property to set
211
+ # @param value [String] the value to set
212
+ #
213
+ # @example
214
+ # element.set(:name,"div").set("id","foo")
215
+ #
78
216
  def set(property, value)
79
217
  case property.to_sym
80
218
  when :name
@@ -85,18 +223,37 @@ module Kitchen
85
223
  self
86
224
  end
87
225
 
226
+ # Returns this element's ancestor of the given type
227
+ #
228
+ # @param type [String, Symbol] e.g. +:page+, +:term+
229
+ # @return [Ancestor]
230
+ # @raise [StandardError] if there is no ancestor of the given type
231
+ #
88
232
  def ancestor(type)
89
233
  @ancestors[type.to_sym]&.element || raise("No ancestor of type '#{type}'")
90
234
  end
91
235
 
236
+ # Returns true iff this element has an ancestor of the given type
237
+ #
238
+ # @param type [String, Symbol] e.g. +:page+, +:term+
239
+ # @return [Boolean]
240
+ #
92
241
  def has_ancestor?(type)
93
242
  @ancestors[type.to_sym].present?
94
243
  end
95
244
 
96
- def ancestors
97
- @ancestors
98
- end
245
+ # Returns the element's ancestors
246
+ #
247
+ # @return [Array<Ancestor>]
248
+ #
249
+ attr_reader :ancestors
99
250
 
251
+ # Adds ancestors to this element, for each incrementing descendant counts for this type
252
+ #
253
+ # @param args [Array<Hash, Ancestor, Element, Document>] the ancestors
254
+ # @raise [StandardError] if there is already an ancestor with the one of
255
+ # the given ancestors' types
256
+ #
100
257
  def add_ancestors(*args)
101
258
  args.each do |arg|
102
259
  case arg
@@ -112,50 +269,93 @@ module Kitchen
112
269
  end
113
270
  end
114
271
 
272
+ # Adds one ancestor, incrementing its descendant counts for this element type
273
+ #
274
+ # @param ancestor [Ancestor]
275
+ # @raise [StandardError] if there is already an ancestor with the given ancestor's type
276
+ #
115
277
  def add_ancestor(ancestor)
116
278
  if @ancestors[ancestor.type].present?
117
279
  raise "Trying to add an ancestor of type '#{ancestor.type}' but one of that " \
118
280
  "type is already present"
119
281
  end
120
282
 
283
+ ancestor.increment_descendant_count(short_type)
121
284
  @ancestors[ancestor.type] = ancestor
122
285
  end
123
286
 
287
+ # Return the elements in all of the ancestors
288
+ #
289
+ # @return [Array<ElementBase>]
290
+ #
124
291
  def ancestor_elements
125
292
  @ancestors.values.map(&:element)
126
293
  end
127
294
 
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
-
295
+ # Returns the count of this element's type in the given ancestor type
296
+ #
297
+ # @param ancestor_type [String, Symbol]
298
+ #
134
299
  def count_in(ancestor_type)
135
- @counts_in[ancestor_type] || raise("No ancestor of type '#{ancestor_type}'")
136
- end
137
-
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
300
+ @ancestors[ancestor_type]&.get_descendant_count(short_type) ||
301
+ raise("No ancestor of type '#{ancestor_type}'")
140
302
  end
141
303
 
142
- def have_sub_elements_already_been_counted?(css_or_xpath)
143
- number_of_sub_elements_already_counted(css_or_xpath) != 0
304
+ # Track that a sub element found by the given query has been counted
305
+ #
306
+ # @param search_query [SearchQuery] the search query matching the counted element
307
+ # @param type [String] the type of the sub element that was counted
308
+ #
309
+ def remember_that_a_sub_element_was_counted(search_query, type)
310
+ @search_query_matches_that_have_been_counted[search_query.to_s] ||= Hash.new(0)
311
+ @search_query_matches_that_have_been_counted[search_query.to_s][type] += 1
144
312
  end
145
313
 
146
- def number_of_sub_elements_already_counted(css_or_xpath)
147
- @css_or_xpath_that_has_been_counted[css_or_xpath] || 0
314
+ # Undo the counts from a prior search query (so that they can be counted again)
315
+ #
316
+ # @param search_query [SearchQuery] the prior search query whose counts need to be undone
317
+ #
318
+ def uncount(search_query)
319
+ @search_query_matches_that_have_been_counted.delete(search_query.to_s)&.each do |type, count|
320
+ ancestors.each_value do |ancestor|
321
+ ancestor.decrement_descendant_count(type, by: count)
322
+ end
323
+ end
148
324
  end
149
325
 
326
+ # Returns the search history that found this element
327
+ #
328
+ # @return [SearchHistory]
329
+ #
150
330
  def search_history
151
- history = ancestor_elements.map(&:css_or_xpath_that_found_me) + [css_or_xpath_that_found_me]
152
- history.compact.join(" ")
331
+ SearchHistory.new(
332
+ ancestor_elements.last&.search_history || SearchHistory.empty,
333
+ search_query_that_found_me
334
+ )
153
335
  end
154
336
 
155
- def search(*selector_or_xpath_args)
337
+ # Returns an ElementEnumerator that iterates over the provided selector or xpath queries
338
+ #
339
+ # @param selector_or_xpath_args [Array<String>] Selector or XPath queries
340
+ # @param only [Symbol, Callable] the name of a method to call on an element or a
341
+ # lambda or proc that accepts an element; elements will only be included in the
342
+ # search results if the method or callable returns true
343
+ # @param except [Symbol, Callable] the name of a method to call on an element or a
344
+ # lambda or proc that accepts an element; elements will not be included in the
345
+ # search results if the method or callable returns false
346
+ # @return [ElementEnumerator]
347
+ #
348
+ def search(*selector_or_xpath_args, only: nil, except: nil)
156
349
  block_error_if(block_given?)
157
350
 
158
- ElementEnumerator.factory.build_within(self, css_or_xpath: selector_or_xpath_args)
351
+ ElementEnumerator.factory.build_within(
352
+ self,
353
+ search_query: SearchQuery.new(
354
+ css_or_xpath: selector_or_xpath_args,
355
+ only: only,
356
+ except: except
357
+ )
358
+ )
159
359
  end
160
360
 
161
361
  # Yields and returns the first child element that matches the provided
@@ -185,16 +385,32 @@ module Kitchen
185
385
  end
186
386
  end
187
387
 
388
+ # @!method at
389
+ # @see first
188
390
  alias_method :at, :first
189
391
 
190
- def element_children()
392
+ # Returns an enumerator over the direct child elements of this element, with the
393
+ # specific type (e.g. +TermElement+) if such type is available.
394
+ #
395
+ # @return [TypeCastingElementEnumerator]
396
+ #
397
+ def element_children
191
398
  block_error_if(block_given?)
192
- TypeCastingElementEnumerator.factory.build_within(self, css_or_xpath: "./*")
399
+ TypeCastingElementEnumerator.factory.build_within(
400
+ self,
401
+ search_query: SearchQuery.new(css_or_xpath: './*')
402
+ )
193
403
  end
194
404
 
405
+ # Searches for elements handled by a list of enumerator classes. All element that
406
+ # matches one of those enumerator classes are iterated over.
407
+ #
408
+ # @param enumerator_classes [Array<ElementEnumeratorBase>]
409
+ # @return [TypeCastingElementEnumerator]
410
+ #
195
411
  def search_with(*enumerator_classes)
196
412
  block_error_if(block_given?)
197
- raise "must supply at least one enumerator class" if enumerator_classes.empty?
413
+ raise 'must supply at least one enumerator class' if enumerator_classes.empty?
198
414
 
199
415
  factory = enumerator_classes[0].factory
200
416
  enumerator_classes[1..-1].each do |enumerator_class|
@@ -214,7 +430,7 @@ module Kitchen
214
430
  block_error_if(block_given?)
215
431
 
216
432
  node.remove
217
- clipboard(to).add(self) if to.present?
433
+ get_clipboard(to).add(self) if to.present?
218
434
  self
219
435
  end
220
436
 
@@ -232,9 +448,10 @@ module Kitchen
232
448
  the_copy = clone
233
449
  the_copy.raw.traverse do |node|
234
450
  next if node.text? || node.document?
451
+
235
452
  document.record_id_copied(node[:id])
236
453
  end
237
- clipboard(to).add(the_copy) if to.present?
454
+ get_clipboard(to).add(the_copy) if to.present?
238
455
  the_copy
239
456
  end
240
457
 
@@ -247,11 +464,18 @@ module Kitchen
247
464
  temp_copy = clone
248
465
  temp_copy.raw.traverse do |node|
249
466
  next if node.text? || node.document?
467
+
250
468
  node[:id] = document.modified_id_to_paste(node[:id]) unless node[:id].blank?
251
469
  end
252
470
  temp_copy.to_s
253
471
  end
254
472
 
473
+ # Copy the element's id
474
+ def copied_id
475
+ document.record_id_copied(id)
476
+ document.modified_id_to_paste(id)
477
+ end
478
+
255
479
  # Delete the element
256
480
  #
257
481
  def trash
@@ -263,20 +487,19 @@ module Kitchen
263
487
  Element.new(node: raw.parent, document: document, short_type: "parent(#{short_type})")
264
488
  end
265
489
 
266
- # TODO make it clear if all of these methods take Element, Node, or String
490
+ # TODO: make it clear if all of these methods take Element, Node, or String
267
491
 
268
492
  # If child argument given, prepends it before the element's current children.
269
493
  # If sibling is given, prepends it as a sibling to this element.
270
494
  #
271
495
  # @param child [String] the child to prepend
272
496
  # @param sibling [String] the sibling to prepend
497
+ # @raise [RecipeError] if specify other than just a child or a sibling
273
498
  #
274
499
  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
500
+ require_one_of_child_or_sibling(child, sibling)
501
+
502
+ if child
280
503
  if node.children.empty?
281
504
  node.children = child.to_s
282
505
  else
@@ -285,6 +508,7 @@ module Kitchen
285
508
  else
286
509
  node.add_previous_sibling(sibling)
287
510
  end
511
+
288
512
  self
289
513
  end
290
514
 
@@ -293,13 +517,12 @@ module Kitchen
293
517
  #
294
518
  # @param child [String] the child to append
295
519
  # @param sibling [String] the sibling to append
520
+ # @raise [RecipeError] if specify other than just a child or a sibling
296
521
  #
297
522
  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
523
+ require_one_of_child_or_sibling(child, sibling)
524
+
525
+ if child
303
526
  if node.children.empty?
304
527
  node.children = child.to_s
305
528
  else
@@ -308,6 +531,7 @@ module Kitchen
308
531
  else
309
532
  node.next = sibling
310
533
  end
534
+
311
535
  self
312
536
  end
313
537
 
@@ -320,7 +544,36 @@ module Kitchen
320
544
  self
321
545
  end
322
546
 
323
- # TODO methods like replace_children that take string, either forbid or handle Element/Node args
547
+ # Wraps the element's children in a new element. Yields the new wrapper element
548
+ # to a block, if provided.
549
+ #
550
+ # @param name [String] the wrapper's tag name, defaults to 'div'.
551
+ # @param attributes [Hash] the wrapper's attributes. XML attributes often use hyphens
552
+ # (e.g. 'data-type') which are hard to put into symbols. Therefore underscores in
553
+ # keys passed to this method will be converted to hyphens. If you really want an
554
+ # underscore you can use a double underscore.
555
+ # @yieldparam [Element] the wrapper Element
556
+ # @return [Element] self
557
+ #
558
+ def wrap_children(name='div', attributes={})
559
+ if name.is_a?(Hash)
560
+ attributes = name
561
+ name = 'div'
562
+ end
563
+
564
+ node.children = node.document.create_element(name) do |new_node|
565
+ # For some reason passing attributes to create_element doesn't work, so doing here
566
+ attributes.each do |k, v|
567
+ new_node[k.to_s.gsub(/([^_])_([^_])/, '\1-\2').gsub('__', '_')] = v
568
+ end
569
+ new_node.children = children.to_s
570
+ yield Element.new(node: new_node, document: document, short_type: nil) if block_given?
571
+ end.to_s
572
+
573
+ self
574
+ end
575
+
576
+ # TODO: methods like replace_children that take string, either forbid or handle Element/Node args
324
577
 
325
578
  # Get the content of children matching the provided selector. Mostly
326
579
  # useful when there is one child with text you want to extract.
@@ -350,11 +603,19 @@ module Kitchen
350
603
  # @return [String] the sub header tag name
351
604
  #
352
605
  def sub_header_name
353
- first_header = node.search("h1, h2, h3, h4, h5, h6").first
606
+ first_header = node.search('h1, h2, h3, h4, h5, h6').first
607
+
608
+ if first_header.nil?
609
+ 'h1'
610
+ else
611
+ first_header.name.gsub(/\d/) { |num| (num.to_i + 1).to_s }
612
+ end
613
+ end
354
614
 
355
- first_header.nil? ?
356
- "h1" :
357
- first_header.name.gsub(/\d/) {|num| (num.to_i + 1).to_s}
615
+ # Mark the location so that if there's an error we can show the developer where.
616
+ #
617
+ def mark_as_current_location!
618
+ document.location = self
358
619
  end
359
620
 
360
621
  # Returns the underlying Nokogiri object
@@ -365,22 +626,40 @@ module Kitchen
365
626
  node
366
627
  end
367
628
 
629
+ # Returns a string version of this element
630
+ #
631
+ # @return [String]
632
+ #
368
633
  def inspect
369
634
  to_s
370
635
  end
371
636
 
637
+ # Returns a string version of this element
638
+ #
639
+ # @return [String]
640
+ #
372
641
  def to_s
373
642
  remove_default_namespaces_if_clone(node.to_s)
374
643
  end
375
644
 
645
+ # Returns a string version of this element as XML
646
+ #
647
+ # @return [String]
648
+ #
376
649
  def to_xml
377
650
  remove_default_namespaces_if_clone(node.to_xml)
378
651
  end
379
652
 
653
+ # Returns a string version of this element as XHTML
654
+ #
655
+ # @return [String]
656
+ #
380
657
  def to_xhtml
381
658
  remove_default_namespaces_if_clone(node.to_xhtml)
382
659
  end
383
660
 
661
+ # Returns a clone of this object
662
+ #
384
663
  def clone
385
664
  super.tap do |element|
386
665
  # When we call dup, the dup gets a bunch of default namespace stuff that
@@ -407,38 +686,69 @@ module Kitchen
407
686
  end
408
687
  end
409
688
 
689
+ def last_element
690
+ node.last_element_child
691
+ end
692
+
410
693
  # @!method pages
411
694
  # Returns a pages enumerator
412
- def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples
695
+ def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples,
696
+ :metadatas, :non_introduction_pages, :units, :titles, :exercises, :references,
697
+ :composite_pages
413
698
 
699
+ # Returns this element as an enumerator (over only one element, itself)
700
+ #
701
+ # @return [ElementEnumeratorBase] (actually returns the appropriate enumerator class for this element)
702
+ #
414
703
  def as_enumerator
415
- enumerator_class.new(css_or_xpath: css_or_xpath_that_found_me) {|block| block.yield(self)}
704
+ enumerator_class.new(search_query: search_query_that_found_me) { |block| block.yield(self) }
416
705
  end
417
706
 
418
707
  protected
419
708
 
709
+ # The wrapped Nokogiri node
710
+ # @return [Nokogiri::XML::Node] the node
420
711
  attr_accessor :node
712
+
713
+ # If this element is a clone
714
+ # @return [Boolean]
421
715
  attr_accessor :is_a_clone
422
716
 
423
- def clipboard(name_or_object)
717
+ # Return a clipboard
718
+ #
719
+ # @param name_or_object [String, Clipboard] the name of the clipboard or the clipboard itself
720
+ # @return [Clipboard]
721
+ #
722
+ def get_clipboard(name_or_object)
424
723
  case name_or_object
425
724
  when Symbol
426
- document.clipboard(name: name_or_object)
725
+ clipboard(name: name_or_object)
427
726
  when Clipboard
428
727
  name_or_object
429
728
  else
430
- raise ArgumentError, "The provided argument (#{name_or_object}) is not "
729
+ raise ArgumentError, "The provided argument (#{name_or_object}) is not " \
431
730
  "a clipboard name or a clipboard"
432
731
  end
433
732
  end
434
733
 
734
+ # Clean up some default namespace junk for cloned elements
735
+ #
736
+ # @param string [String] the string to clean
435
737
  def remove_default_namespaces_if_clone(string)
436
738
  if is_a_clone
437
- string.gsub("xmlns:default=\"http://www.w3.org/1999/xhtml\"","").gsub("default:","")
739
+ string.gsub('xmlns:default="http://www.w3.org/1999/xhtml"', '')
740
+ .gsub('xmlns="http://www.w3.org/1999/xhtml"', '')
741
+ .gsub('default:', '')
438
742
  else
439
743
  string
440
744
  end
441
745
  end
442
746
 
747
+ def require_one_of_child_or_sibling(child, sibling)
748
+ raise RecipeError, 'Only one of `child` or `sibling` can be specified' if child && sibling
749
+ raise RecipeError, 'One of `child` or `sibling` must be specified' if !child && !sibling
750
+ end
751
+
443
752
  end
444
753
  end
754
+ # rubocop:enable Metrics/ClassLength