openstax_kitchen 3.0.0 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/CHANGELOG.md +77 -3
  4. data/Gemfile.lock +13 -14
  5. data/README.md +23 -0
  6. data/codecov.yaml +1 -0
  7. data/docker/ci +0 -1
  8. data/lib/kitchen/book_document.rb +1 -1
  9. data/lib/kitchen/book_element.rb +16 -2
  10. data/lib/kitchen/chapter_element.rb +10 -13
  11. data/lib/kitchen/chapter_element_enumerator.rb +1 -1
  12. data/lib/kitchen/composite_chapter_element.rb +7 -11
  13. data/lib/kitchen/composite_chapter_element_enumerator.rb +21 -0
  14. data/lib/kitchen/composite_page_element.rb +15 -10
  15. data/lib/kitchen/composite_page_element_enumerator.rb +1 -1
  16. data/lib/kitchen/config.rb +14 -0
  17. data/lib/kitchen/directions/bake_chapter_glossary/main.rb +18 -0
  18. data/lib/kitchen/directions/bake_chapter_glossary/v1.rb +30 -0
  19. data/lib/kitchen/directions/bake_chapter_introductions.rb +1 -1
  20. data/lib/kitchen/directions/bake_chapter_key_concepts/main.rb +16 -0
  21. data/lib/kitchen/directions/bake_chapter_key_concepts/v1.rb +35 -0
  22. data/lib/kitchen/directions/bake_chapter_key_equations.rb +27 -20
  23. data/lib/kitchen/directions/bake_chapter_references/main.rb +16 -0
  24. data/lib/kitchen/directions/bake_chapter_references/v1.rb +35 -0
  25. data/lib/kitchen/directions/bake_chapter_section_exercises/main.rb +11 -0
  26. data/lib/kitchen/directions/bake_chapter_section_exercises/v1.rb +28 -0
  27. data/lib/kitchen/directions/bake_chapter_summary.rb +48 -42
  28. data/lib/kitchen/directions/bake_checkpoint.rb +44 -0
  29. data/lib/kitchen/directions/bake_composite_chapters.rb +14 -0
  30. data/lib/kitchen/directions/bake_composite_pages.rb +1 -1
  31. data/lib/kitchen/directions/bake_equations.rb +37 -0
  32. data/lib/kitchen/directions/bake_example.rb +34 -8
  33. data/lib/kitchen/directions/bake_figure.rb +1 -1
  34. data/lib/kitchen/directions/bake_first_elements.rb +16 -0
  35. data/lib/kitchen/directions/bake_footnotes/v1.rb +2 -1
  36. data/lib/kitchen/directions/bake_free_response/free_response.xhtml.erb +10 -0
  37. data/lib/kitchen/directions/bake_free_response/main.rb +11 -0
  38. data/lib/kitchen/directions/bake_free_response/v1.rb +29 -0
  39. data/lib/kitchen/directions/bake_further_research.rb +59 -0
  40. data/lib/kitchen/directions/bake_index/v1.rb +35 -14
  41. data/lib/kitchen/directions/bake_link_placeholders.rb +1 -1
  42. data/lib/kitchen/directions/bake_non_introduction_pages.rb +26 -0
  43. data/lib/kitchen/directions/bake_notes/bake_autotitled_notes.rb +29 -0
  44. data/lib/kitchen/directions/bake_notes/bake_note_subtitle.rb +22 -0
  45. data/lib/kitchen/directions/bake_notes/bake_numbered_notes.rb +51 -0
  46. data/lib/kitchen/directions/bake_notes/bake_unclassified_notes.rb +30 -0
  47. data/lib/kitchen/directions/bake_numbered_exercise/main.rb +15 -0
  48. data/lib/kitchen/directions/bake_numbered_exercise/v1.rb +47 -0
  49. data/lib/kitchen/directions/bake_numbered_table/main.rb +2 -2
  50. data/lib/kitchen/directions/bake_numbered_table/v1.rb +18 -4
  51. data/lib/kitchen/directions/bake_page_abstracts.rb +16 -0
  52. data/lib/kitchen/directions/bake_references/main.rb +16 -0
  53. data/lib/kitchen/directions/bake_references/v1.rb +48 -0
  54. data/lib/kitchen/directions/bake_stepwise.rb +1 -5
  55. data/lib/kitchen/directions/bake_suggested_reading.rb +5 -0
  56. data/lib/kitchen/directions/bake_theorem/main.rb +11 -0
  57. data/lib/kitchen/directions/bake_theorem/v1.rb +28 -0
  58. data/lib/kitchen/directions/bake_toc.rb +10 -2
  59. data/lib/kitchen/directions/book_answer_key_container/eob_solutions_container.xhtml.erb +9 -0
  60. data/lib/kitchen/directions/{bake_exercises → book_answer_key_container}/main.rb +1 -1
  61. data/lib/kitchen/directions/book_answer_key_container/v1.rb +13 -0
  62. data/lib/kitchen/directions/chapter_review_container/chapter_review.xhtml.erb +9 -0
  63. data/lib/kitchen/directions/chapter_review_container/main.rb +11 -0
  64. data/lib/kitchen/directions/chapter_review_container/v1.rb +13 -0
  65. data/lib/kitchen/directions/eoc_section_title_link_snippet.rb +20 -0
  66. data/lib/kitchen/directions/move_exercises_to_eoc/main.rb +27 -0
  67. data/lib/kitchen/directions/move_exercises_to_eoc/v1.rb +36 -0
  68. data/lib/kitchen/directions/move_exercises_to_eoc/v2.rb +49 -0
  69. data/lib/kitchen/directions/move_solutions_to_answer_key/main.rb +14 -0
  70. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/american_government.rb +19 -0
  71. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/calculus.rb +41 -0
  72. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/uphysics.rb +63 -0
  73. data/lib/kitchen/directions/move_solutions_to_answer_key/v1.rb +34 -0
  74. data/lib/kitchen/document.rb +3 -0
  75. data/lib/kitchen/element.rb +9 -3
  76. data/lib/kitchen/element_base.rb +118 -16
  77. data/lib/kitchen/element_enumerator_base.rb +118 -8
  78. data/lib/kitchen/element_enumerator_factory.rb +28 -12
  79. data/lib/kitchen/element_factory.rb +3 -3
  80. data/lib/kitchen/example_element.rb +8 -11
  81. data/lib/kitchen/example_element_enumerator.rb +1 -1
  82. data/lib/kitchen/exercise_element.rb +42 -0
  83. data/lib/kitchen/exercise_element_enumerator.rb +21 -0
  84. data/lib/kitchen/figure_element.rb +8 -11
  85. data/lib/kitchen/figure_element_enumerator.rb +1 -1
  86. data/lib/kitchen/metadata_element.rb +8 -2
  87. data/lib/kitchen/metadata_element_enumerator.rb +1 -1
  88. data/lib/kitchen/note_element.rb +25 -27
  89. data/lib/kitchen/note_element_enumerator.rb +1 -1
  90. data/lib/kitchen/oven.rb +2 -0
  91. data/lib/kitchen/page_element.rb +33 -9
  92. data/lib/kitchen/page_element_enumerator.rb +1 -1
  93. data/lib/kitchen/patches/nokogiri.rb +55 -0
  94. data/lib/kitchen/patches/nokogiri_profiling.rb +60 -0
  95. data/lib/kitchen/recipe.rb +35 -2
  96. data/lib/kitchen/reference_element.rb +27 -0
  97. data/lib/kitchen/references_element_enumerator.rb +20 -0
  98. data/lib/kitchen/search_query.rb +31 -3
  99. data/lib/kitchen/selector.rb +25 -0
  100. data/lib/kitchen/selectors/base.rb +39 -0
  101. data/lib/kitchen/selectors/standard_1.rb +13 -0
  102. data/lib/kitchen/table_element.rb +8 -11
  103. data/lib/kitchen/table_element_enumerator.rb +1 -1
  104. data/lib/kitchen/templates/eob_section_title_template.xhtml.erb +10 -0
  105. data/lib/kitchen/templates/eoc_section_title_template.xhtml.erb +10 -0
  106. data/lib/kitchen/term_element.rb +5 -8
  107. data/lib/kitchen/term_element_enumerator.rb +1 -1
  108. data/lib/kitchen/unit_element.rb +13 -7
  109. data/lib/kitchen/unit_element_enumerator.rb +1 -1
  110. data/lib/kitchen/version.rb +1 -1
  111. data/lib/locales/en.yml +12 -7
  112. data/lib/locales/pl.yml +24 -0
  113. data/lib/openstax_kitchen.rb +2 -1
  114. metadata +54 -6
  115. data/lib/kitchen/directions/bake_chapter_glossary.rb +0 -37
  116. data/lib/kitchen/directions/bake_exercises/v1.rb +0 -166
  117. data/lib/kitchen/directions/bake_notes.rb +0 -58
@@ -0,0 +1,9 @@
1
+ <div class="os-eoc os-chapter-review-container" data-type="composite-chapter" data-uuid-key=".chapter-review">
2
+ <h2 data-type="document-title">
3
+ <span class="os-text"><%= I18n.t(:eoc_chapter_review) %></span>
4
+ </h2>
5
+ <div data-type="metadata" style="display: none;">
6
+ <h1 data-type="document-title" itemprop="name"><%= I18n.t(:eoc_chapter_review) %></h1>
7
+ <%= @metadata.paste %>
8
+ </div>
9
+ </div>
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ module Directions
5
+ module ChapterReviewContainer
6
+ def self.v1(chapter:, metadata_source:)
7
+ V1.new.bake(chapter: chapter, metadata_source: metadata_source)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::ChapterReviewContainer
4
+ class V1
5
+ renderable
6
+
7
+ def bake(chapter:, metadata_source:)
8
+ @metadata = metadata_source.children_to_keep.copy
9
+ chapter.append(child: render(file: 'chapter_review.xhtml.erb'))
10
+ chapter.first('div.os-eoc.os-chapter-review-container')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ module Directions
5
+ module EocSectionTitleLinkSnippet
6
+ def self.v1(page:)
7
+ chapter = page.ancestor(:chapter)
8
+ <<~HTML
9
+ <a href="##{page.title.id}">
10
+ <h3 data-type="document-title" id="#{page.title.copied_id}">
11
+ <span class="os-number">#{chapter.count_in(:book)}.#{page.count_in(:chapter)}</span>
12
+ <span class="os-divider"> </span>
13
+ <span class="os-text" data-type="" itemprop="">#{page.title_text}</span>
14
+ </h3>
15
+ </a>
16
+ HTML
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ module Directions
5
+ module MoveExercisesToEOC
6
+ def self.v1(chapter:, metadata_source:, klass:, append_to: nil, uuid_prefix: '.')
7
+ V1.new.bake(
8
+ chapter: chapter,
9
+ metadata_source: metadata_source,
10
+ append_to: append_to,
11
+ klass: klass,
12
+ uuid_prefix: uuid_prefix
13
+ )
14
+ end
15
+
16
+ def self.v2(chapter:, metadata_source:, klass:, append_to: nil, uuid_prefix: '.')
17
+ V2.new.bake(
18
+ chapter: chapter,
19
+ metadata_source: metadata_source,
20
+ append_to: append_to,
21
+ klass: klass,
22
+ uuid_prefix: uuid_prefix
23
+ )
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::MoveExercisesToEOC
4
+ class V1
5
+ renderable
6
+
7
+ def bake(chapter:, metadata_source:, klass:, append_to: nil, uuid_prefix: '.')
8
+ @klass = klass
9
+ @metadata = metadata_source.children_to_keep.copy
10
+ @title = I18n.t(:"eoc.#{klass}")
11
+ @uuid_prefix = uuid_prefix
12
+
13
+ exercise_clipboard = Kitchen::Clipboard.new
14
+
15
+ chapter.non_introduction_pages.each do |page|
16
+ sections = page.search("section.#{@klass}")
17
+
18
+ sections.each do |exercise_section|
19
+ exercise_section.first("[data-type='title']")&.trash
20
+
21
+ exercise_section.cut(to: exercise_clipboard)
22
+ end
23
+ end
24
+
25
+ return if exercise_clipboard.none?
26
+
27
+ @content = exercise_clipboard.paste
28
+
29
+ append_to_element = append_to || chapter
30
+ @in_composite_chapter = append_to_element.is?(:composite_chapter)
31
+
32
+ append_to_element.append(child: render(file:
33
+ '../../templates/eoc_section_title_template.xhtml.erb'))
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::MoveExercisesToEOC
4
+ # Main difference from v1 is the presence of a section title
5
+ # and some additional wrappers
6
+ class V2
7
+ renderable
8
+
9
+ def bake(chapter:, metadata_source:, klass:, append_to: nil, uuid_prefix: '.')
10
+ @klass = klass
11
+ @metadata = metadata_source.children_to_keep.copy
12
+ @title = I18n.t(:"eoc.#{klass}")
13
+ @uuid_prefix = uuid_prefix
14
+
15
+ exercise_clipboard = Kitchen::Clipboard.new
16
+
17
+ chapter.non_introduction_pages.each do |page|
18
+ sections = page.search("section.#{@klass}")
19
+
20
+ sections.each do |exercise_section|
21
+ exercise_section.first("[data-type='title']")&.trash
22
+
23
+ # Get parent page title
24
+ section_title = Kitchen::Directions::EocSectionTitleLinkSnippet.v1(page: page)
25
+
26
+ # Configure section title & wrappers
27
+ exercise_section.prepend(child: section_title)
28
+ exercise_section.wrap('<div class="os-section-area">')
29
+ exercise_section = exercise_section.parent
30
+ exercise_section.cut(to: exercise_clipboard)
31
+ end
32
+ end
33
+
34
+ return if exercise_clipboard.none?
35
+
36
+ @content = <<~HTML
37
+ <div class="os-#{@klass}">
38
+ #{exercise_clipboard.paste}
39
+ </div>
40
+ HTML
41
+
42
+ append_to_element = append_to || chapter
43
+ @in_composite_chapter = append_to_element[:'data-type'] == 'composite-chapter'
44
+
45
+ append_to_element.append(child: render(file:
46
+ '../../templates/eoc_section_title_template.xhtml.erb'))
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ module Directions
5
+ module MoveSolutionsToAnswerKey
6
+ def self.v1(chapter:, metadata_source:, strategy:, append_to:)
7
+ V1.new.bake(
8
+ chapter: chapter,
9
+ metadata_source: metadata_source,
10
+ strategy: strategy, append_to: append_to)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::MoveSolutionsToAnswerKey
4
+ module Strategies
5
+ class AmericanGovernment
6
+ def bake(chapter:, append_to:)
7
+ bake_section(chapter: chapter, append_to: append_to, klass: 'review-questions')
8
+ end
9
+
10
+ protected
11
+
12
+ def bake_section(chapter:, append_to:, klass:)
13
+ chapter.search(".#{klass} [data-type='solution']").each do |solution|
14
+ append_to.add_child(solution.cut.to_s)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::MoveSolutionsToAnswerKey
4
+ module Strategies
5
+ class Calculus
6
+ def bake(chapter:, append_to:)
7
+ checkpoint_solutions = chapter.search('.checkpoint [data-type="solution"]').cut
8
+ append_solution_area(I18n.t(:checkpoint), checkpoint_solutions, append_to)
9
+
10
+ chapter.search('.section-exercises').each do |section|
11
+ section_solutions = section.search('[data-type="solution"]').cut
12
+ section_title = I18n.t(
13
+ :section_exercises,
14
+ number: "#{chapter.count_in(:book)}.#{section.count_in(:chapter)}"
15
+ )
16
+ append_solution_area(section_title, section_solutions, append_to)
17
+ end
18
+
19
+ chapter.search('.review-exercises').each do |section|
20
+ section_solutions = section.search('[data-type="solution"]').cut
21
+ append_solution_area(I18n.t(:review_exercises), section_solutions, append_to)
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def append_solution_area(title, clipboard, append_to)
28
+ append_to.add_child(
29
+ <<~HTML
30
+ <div class="os-solution-area">
31
+ <h3 data-type="title">
32
+ <span class="os-title-label">#{title}</span>
33
+ </h3>
34
+ #{clipboard.paste}
35
+ </div>
36
+ HTML
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::MoveSolutionsToAnswerKey
4
+ module Strategies
5
+ class UPhysics
6
+ def bake(chapter:, append_to:)
7
+ bake_from_notes(chapter: chapter, append_to: append_to, klass: 'check-understanding')
8
+
9
+ classes = %w[review-conceptual-questions review-problems review-additional-problems
10
+ review-challenge]
11
+ classes.each do |klass|
12
+ bake_section(chapter: chapter, append_to: append_to, klass: klass)
13
+ end
14
+ end
15
+
16
+ protected
17
+
18
+ def bake_section(chapter:, append_to:, klass:)
19
+ section_solutions_set = []
20
+ chapter.search(".#{klass}").each do |section|
21
+ section.search('div[data-type="solution"]').each do |solution|
22
+ section_solutions_set.push(solution.cut)
23
+ end
24
+ end
25
+
26
+ return if section_solutions_set.empty?
27
+
28
+ title = I18n.t(:"eoc.#{klass}")
29
+ append_solution_area(title, section_solutions_set, append_to)
30
+ end
31
+
32
+ def bake_from_notes(chapter:, append_to:, klass:)
33
+ solutions = []
34
+ chapter.notes("$.#{klass}").each do |note|
35
+ note.exercises.each do |exercise|
36
+ solution = exercise.solution
37
+ solutions.push(solution.cut) if solution
38
+ end
39
+ end
40
+ return if solutions.empty?
41
+
42
+ title = I18n.t(:"notes.#{klass}")
43
+ append_solution_area(title, solutions, append_to)
44
+ end
45
+
46
+ def append_solution_area(title, solutions, append_to)
47
+ append_to = append_to.add_child(
48
+ <<~HTML
49
+ <div class="os-solution-area">
50
+ <h3 data-type="title">
51
+ <span class="os-title-label">#{title}</span>
52
+ </h3>
53
+ </div>
54
+ HTML
55
+ ).first
56
+
57
+ solutions.each do |solution|
58
+ append_to.add_child(solution.raw)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::MoveSolutionsToAnswerKey
4
+ class V1
5
+ def bake(chapter:, metadata_source:, strategy:, append_to:)
6
+ strategy =
7
+ case strategy
8
+ when :calculus
9
+ Strategies::Calculus
10
+ when :uphysics
11
+ Strategies::UPhysics
12
+ when :american_government
13
+ Strategies::AmericanGovernment
14
+ else
15
+ raise 'No such strategy'
16
+ end
17
+
18
+ append_to.append(child:
19
+ <<~HTML
20
+ <div class="os-eob os-solutions-container" data-type="composite-page" data-uuid-key=".solutions#{chapter.count_in(:book)}">
21
+ <h2 data-type="document-title">
22
+ <span class="os-text">#{I18n.t(:chapter)} #{chapter.count_in(:book)}</span>
23
+ </h2>
24
+ <div data-type="metadata" style="display: none;">
25
+ <h1 data-type="document-title" itemprop="name">#{I18n.t(:chapter)} #{chapter.count_in(:book)}</h1>
26
+ #{metadata_source.children_to_keep.copy.paste}
27
+ </div>
28
+ </div>
29
+ HTML
30
+ )
31
+ strategy.new.bake(chapter: chapter, append_to: append_to.last_element)
32
+ end
33
+ end
34
+ end
@@ -46,6 +46,9 @@ module Kitchen
46
46
  @config = config || Config.new
47
47
  @next_paste_count_for_id = {}
48
48
  @id_copy_suffix = '_copy_'
49
+
50
+ # Nokogiri by default only recognizes the namespaces on the root node. Add all others.
51
+ raw&.add_all_namespaces! if @config.enable_all_namespaces
49
52
  end
50
53
 
51
54
  # Returns an enumerator that iterates over all children of this document
@@ -19,8 +19,14 @@ module Kitchen
19
19
  short_type: short_type)
20
20
  end
21
21
 
22
- # # @!method pages
23
- # # Returns a pages enumerator
24
- # 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
25
31
  end
26
32
  end
@@ -3,6 +3,7 @@
3
3
  require 'forwardable'
4
4
  require 'securerandom'
5
5
 
6
+ # rubocop:disable Metrics/ClassLength
6
7
  module Kitchen
7
8
  # Abstract base class for all elements. If you are looking for a simple concrete
8
9
  # element class, use `Element`.
@@ -92,6 +93,14 @@ module Kitchen
92
93
  # @return [Selectors::Base]
93
94
  def_delegators :config, :selectors
94
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
+
95
104
  # Creates a new instance
96
105
  #
97
106
  # @param node [Nokogiri::XML::Node] the wrapped element
@@ -108,7 +117,9 @@ module Kitchen
108
117
 
109
118
  @enumerator_class = enumerator_class
110
119
 
111
- @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)}"
112
123
 
113
124
  @document =
114
125
  case document
@@ -121,16 +132,53 @@ module Kitchen
121
132
  @ancestors = HashWithIndifferentAccess.new
122
133
  @search_query_matches_that_have_been_counted = {}
123
134
  @is_a_clone = false
135
+ @search_cache = {}
136
+ end
137
+
138
+ # Returns ElementBase descendent type or nil if none found
139
+ #
140
+ # @param type [Symbol] the descendant type, e.g. `:page`
141
+ # @return [Class] the child class for the given type
142
+ #
143
+ def self.descendant(type)
144
+ @types_to_descendants ||=
145
+ descendants.each_with_object({}) do |descendant, hash|
146
+ next unless descendant.try(:short_type)
147
+
148
+ hash[descendant.short_type] = descendant
149
+ end
150
+
151
+ @types_to_descendants[type]
152
+ end
153
+
154
+ # Returns ElementBase descendent type or Error if none found
155
+ #
156
+ # @param type [Symbol] the descendant type, e.g. `:page`
157
+ # @raise if the type is unknown
158
+ # @return [Class] the child class for the given type
159
+ #
160
+ def self.descendant!(type)
161
+ descendant(type) || raise("Unknown ElementBase descendant type '#{type}'")
162
+ end
163
+
164
+ # Returns true if this element is the given type
165
+ #
166
+ # @param type [Symbol] the descendant type, e.g. `:page`
167
+ # @raise if the type is unknown
168
+ # @return [Boolean]
169
+ #
170
+ def is?(type)
171
+ ElementBase.descendant!(type).is_the_element_class_for?(raw, config: config)
124
172
  end
125
173
 
126
174
  # Returns true if this class represents the element for the given node
127
175
  #
128
176
  # @param node [Nokogiri::XML::Node] the underlying node
177
+ # @param config [Kitchen::Config]
129
178
  # @return [Boolean]
130
179
  #
131
- def self.is_the_element_class_for?(_node)
132
- # override this in subclasses
133
- false
180
+ def self.is_the_element_class_for?(node, config:)
181
+ Selector.named(short_type).matches?(node, config: config)
134
182
  end
135
183
 
136
184
  # Returns true if this element has the given class
@@ -298,7 +346,7 @@ module Kitchen
298
346
  # search results if the method or callable returns false
299
347
  # @return [ElementEnumerator]
300
348
  #
301
- def search(*selector_or_xpath_args, only: nil, except: nil)
349
+ def search(*selector_or_xpath_args, only: nil, except: nil, reload: false)
302
350
  block_error_if(block_given?)
303
351
 
304
352
  ElementEnumerator.factory.build_within(
@@ -307,19 +355,29 @@ module Kitchen
307
355
  css_or_xpath: selector_or_xpath_args,
308
356
  only: only,
309
357
  except: except
310
- )
358
+ ),
359
+ reload: reload
311
360
  )
312
361
  end
313
362
 
363
+ def raw_search(*selector_or_xpath_args, reload: false)
364
+ key = selector_or_xpath_args
365
+ @search_cache[key] = nil if reload || !config.enable_search_cache
366
+ # cache nil search results with a fake -1 value
367
+ @search_cache[key] ||= raw.search(*selector_or_xpath_args) || -1
368
+ @search_cache[key] == -1 ? nil : @search_cache[key]
369
+ end
370
+
314
371
  # Yields and returns the first child element that matches the provided
315
372
  # selector or XPath arguments.
316
373
  #
317
374
  # @param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments
375
+ # @param reload [Boolean] ignores cache if true
318
376
  # @yieldparam [Element] the matched XML element
319
377
  # @return [Element, nil] the matched XML element or nil if no match found
320
378
  #
321
- def first(*selector_or_xpath_args)
322
- search(*selector_or_xpath_args).first.tap do |element|
379
+ def first(*selector_or_xpath_args, reload: false)
380
+ search(*selector_or_xpath_args, reload: reload).first.tap do |element|
323
381
  yield(element) if block_given?
324
382
  end
325
383
  end
@@ -328,12 +386,13 @@ module Kitchen
328
386
  # selector or XPath arguments.
329
387
  #
330
388
  # @param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments
389
+ # @param reload [Boolean] ignores cache if true
331
390
  # @yieldparam [Element] the matched XML element
332
391
  # @raise [ElementNotFoundError] if no matching element is found
333
392
  # @return [Element] the matched XML element
334
393
  #
335
- def first!(*selector_or_xpath_args)
336
- search(*selector_or_xpath_args).first!.tap do |element|
394
+ def first!(*selector_or_xpath_args, reload: false)
395
+ search(*selector_or_xpath_args, reload: reload).first!.tap do |element|
337
396
  yield(element) if block_given?
338
397
  end
339
398
  end
@@ -383,7 +442,7 @@ module Kitchen
383
442
  block_error_if(block_given?)
384
443
 
385
444
  node.remove
386
- clipboard(to).add(self) if to.present?
445
+ get_clipboard(to).add(self) if to.present?
387
446
  self
388
447
  end
389
448
 
@@ -404,7 +463,7 @@ module Kitchen
404
463
 
405
464
  document.record_id_copied(node[:id])
406
465
  end
407
- clipboard(to).add(the_copy) if to.present?
466
+ get_clipboard(to).add(the_copy) if to.present?
408
467
  the_copy
409
468
  end
410
469
 
@@ -423,6 +482,12 @@ module Kitchen
423
482
  temp_copy.to_s
424
483
  end
425
484
 
485
+ # Copy the element's id
486
+ def copied_id
487
+ document.record_id_copied(id)
488
+ document.modified_id_to_paste(id)
489
+ end
490
+
426
491
  # Delete the element
427
492
  #
428
493
  def trash
@@ -491,6 +556,35 @@ module Kitchen
491
556
  self
492
557
  end
493
558
 
559
+ # Wraps the element's children in a new element. Yields the new wrapper element
560
+ # to a block, if provided.
561
+ #
562
+ # @param name [String] the wrapper's tag name, defaults to 'div'.
563
+ # @param attributes [Hash] the wrapper's attributes. XML attributes often use hyphens
564
+ # (e.g. 'data-type') which are hard to put into symbols. Therefore underscores in
565
+ # keys passed to this method will be converted to hyphens. If you really want an
566
+ # underscore you can use a double underscore.
567
+ # @yieldparam [Element] the wrapper Element
568
+ # @return [Element] self
569
+ #
570
+ def wrap_children(name='div', attributes={})
571
+ if name.is_a?(Hash)
572
+ attributes = name
573
+ name = 'div'
574
+ end
575
+
576
+ node.children = node.document.create_element(name) do |new_node|
577
+ # For some reason passing attributes to create_element doesn't work, so doing here
578
+ attributes.each do |k, v|
579
+ new_node[k.to_s.gsub(/([^_])_([^_])/, '\1-\2').gsub('__', '_')] = v
580
+ end
581
+ new_node.children = children.to_s
582
+ yield Element.new(node: new_node, document: document, short_type: nil) if block_given?
583
+ end.to_s
584
+
585
+ self
586
+ end
587
+
494
588
  # TODO: methods like replace_children that take string, either forbid or handle Element/Node args
495
589
 
496
590
  # Get the content of children matching the provided selector. Mostly
@@ -604,10 +698,15 @@ module Kitchen
604
698
  end
605
699
  end
606
700
 
701
+ def last_element
702
+ node.last_element_child
703
+ end
704
+
607
705
  # @!method pages
608
706
  # Returns a pages enumerator
609
707
  def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples,
610
- :metadatas, :units
708
+ :metadatas, :non_introduction_pages, :units, :titles, :exercises, :references,
709
+ :composite_pages, :composite_chapters
611
710
 
612
711
  # Returns this element as an enumerator (over only one element, itself)
613
712
  #
@@ -632,10 +731,10 @@ module Kitchen
632
731
  # @param name_or_object [String, Clipboard] the name of the clipboard or the clipboard itself
633
732
  # @return [Clipboard]
634
733
  #
635
- def clipboard(name_or_object)
734
+ def get_clipboard(name_or_object)
636
735
  case name_or_object
637
736
  when Symbol
638
- document.clipboard(name: name_or_object)
737
+ clipboard(name: name_or_object)
639
738
  when Clipboard
640
739
  name_or_object
641
740
  else
@@ -649,7 +748,9 @@ module Kitchen
649
748
  # @param string [String] the string to clean
650
749
  def remove_default_namespaces_if_clone(string)
651
750
  if is_a_clone
652
- string.gsub('xmlns:default="http://www.w3.org/1999/xhtml"', '').gsub('default:', '')
751
+ string.gsub('xmlns:default="http://www.w3.org/1999/xhtml"', '')
752
+ .gsub('xmlns="http://www.w3.org/1999/xhtml"', '')
753
+ .gsub('default:', '')
653
754
  else
654
755
  string
655
756
  end
@@ -662,3 +763,4 @@ module Kitchen
662
763
 
663
764
  end
664
765
  end
766
+ # rubocop:enable Metrics/ClassLength