openstax_kitchen 3.2.0 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/changelog.yml +27 -0
  3. data/.github/workflows/rubocop.yml +28 -0
  4. data/.gitignore +1 -1
  5. data/CHANGELOG.md +94 -1
  6. data/Gemfile.lock +26 -18
  7. data/README.md +16 -0
  8. data/codecov.yaml +1 -0
  9. data/docker/ci +0 -1
  10. data/docker/rubocop +22 -0
  11. data/lib/kitchen/book_document.rb +1 -1
  12. data/lib/kitchen/book_element.rb +16 -2
  13. data/lib/kitchen/chapter_element.rb +10 -13
  14. data/lib/kitchen/chapter_element_enumerator.rb +1 -1
  15. data/lib/kitchen/composite_chapter_element.rb +7 -11
  16. data/lib/kitchen/composite_chapter_element_enumerator.rb +21 -0
  17. data/lib/kitchen/composite_page_element.rb +15 -10
  18. data/lib/kitchen/composite_page_element_enumerator.rb +1 -1
  19. data/lib/kitchen/config.rb +14 -0
  20. data/lib/kitchen/directions/bake_appendix.rb +3 -1
  21. data/lib/kitchen/directions/bake_chapter_glossary/main.rb +18 -0
  22. data/lib/kitchen/directions/bake_chapter_glossary/v1.rb +30 -0
  23. data/lib/kitchen/directions/bake_chapter_introductions.rb +23 -16
  24. data/lib/kitchen/directions/bake_chapter_introductions/chapter_introduction.xhtml.erb +0 -0
  25. data/lib/kitchen/directions/bake_chapter_key_concepts/main.rb +7 -2
  26. data/lib/kitchen/directions/bake_chapter_key_concepts/v1.rb +12 -7
  27. data/lib/kitchen/directions/bake_chapter_key_equations.rb +26 -21
  28. data/lib/kitchen/directions/bake_chapter_references/main.rb +16 -0
  29. data/lib/kitchen/directions/bake_chapter_references/v1.rb +35 -0
  30. data/lib/kitchen/directions/bake_chapter_section_exercises/main.rb +2 -2
  31. data/lib/kitchen/directions/bake_chapter_section_exercises/v1.rb +2 -1
  32. data/lib/kitchen/directions/bake_chapter_solutions/main.rb +11 -0
  33. data/lib/kitchen/directions/bake_chapter_solutions/v1.rb +37 -0
  34. data/lib/kitchen/directions/bake_chapter_summary.rb +56 -43
  35. data/lib/kitchen/directions/bake_composite_chapters.rb +1 -1
  36. data/lib/kitchen/directions/bake_composite_pages.rb +1 -1
  37. data/lib/kitchen/directions/bake_equations.rb +2 -2
  38. data/lib/kitchen/directions/bake_example.rb +8 -1
  39. data/lib/kitchen/directions/bake_figure.rb +14 -1
  40. data/lib/kitchen/directions/bake_first_elements.rb +22 -0
  41. data/lib/kitchen/directions/bake_footnotes/main.rb +2 -2
  42. data/lib/kitchen/directions/bake_footnotes/v1.rb +13 -9
  43. data/lib/kitchen/directions/bake_free_response/free_response.xhtml.erb +10 -0
  44. data/lib/kitchen/directions/{bake_chapter_review → bake_free_response}/main.rb +3 -3
  45. data/lib/kitchen/directions/bake_free_response/v1.rb +29 -0
  46. data/lib/kitchen/directions/bake_further_research.rb +61 -0
  47. data/lib/kitchen/directions/bake_index/v1.rb +36 -26
  48. data/lib/kitchen/directions/bake_link_placeholders.rb +1 -1
  49. data/lib/kitchen/directions/bake_notes/bake_note_subtitle.rb +4 -0
  50. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/main.rb +43 -0
  51. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v1.rb +37 -0
  52. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v2.rb +25 -0
  53. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v3.rb +32 -0
  54. data/lib/kitchen/directions/bake_numbered_exercise/main.rb +7 -2
  55. data/lib/kitchen/directions/bake_numbered_exercise/v1.rb +34 -12
  56. data/lib/kitchen/directions/bake_numbered_table/bake_table_body.rb +29 -0
  57. data/lib/kitchen/directions/bake_numbered_table/main.rb +4 -0
  58. data/lib/kitchen/directions/bake_numbered_table/v1.rb +1 -24
  59. data/lib/kitchen/directions/bake_numbered_table/v2.rb +31 -0
  60. data/lib/kitchen/directions/bake_page_abstracts.rb +1 -1
  61. data/lib/kitchen/directions/bake_preface/main.rb +2 -2
  62. data/lib/kitchen/directions/bake_preface/v1.rb +3 -2
  63. data/lib/kitchen/directions/bake_references/main.rb +16 -0
  64. data/lib/kitchen/directions/bake_references/v1.rb +48 -0
  65. data/lib/kitchen/directions/bake_suggested_reading.rb +5 -0
  66. data/lib/kitchen/directions/bake_toc.rb +4 -2
  67. data/lib/kitchen/directions/book_answer_key_container/eob_answer_key_outer_container.xhtml.erb +9 -0
  68. data/lib/kitchen/directions/book_answer_key_container/main.rb +11 -0
  69. data/lib/kitchen/directions/book_answer_key_container/v1.rb +14 -0
  70. data/lib/kitchen/directions/chapter_review_container/chapter_review.xhtml.erb +9 -0
  71. data/lib/kitchen/directions/chapter_review_container/main.rb +11 -0
  72. data/lib/kitchen/directions/chapter_review_container/v1.rb +15 -0
  73. data/lib/kitchen/directions/eoc_section_title_link_snippet.rb +14 -1
  74. data/lib/kitchen/directions/move_exercises_to_eoc/main.rb +37 -0
  75. data/lib/kitchen/directions/{bake_chapter_review_exercises → move_exercises_to_eoc}/v1.rb +8 -10
  76. data/lib/kitchen/directions/{bake_chapter_review_exercises → move_exercises_to_eoc}/v2.rb +8 -9
  77. data/lib/kitchen/directions/move_exercises_to_eoc/v3.rb +49 -0
  78. data/lib/kitchen/directions/{bake_chapter_answer_key → move_solutions_to_answer_key}/main.rb +7 -3
  79. data/lib/kitchen/directions/{bake_chapter_answer_key → move_solutions_to_answer_key}/strategies/calculus.rb +1 -1
  80. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/default.rb +27 -0
  81. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/precalculus.rb +84 -0
  82. data/lib/kitchen/directions/{bake_chapter_answer_key → move_solutions_to_answer_key}/strategies/uphysics.rb +7 -5
  83. data/lib/kitchen/directions/{bake_chapter_answer_key → move_solutions_to_answer_key}/v1.rb +12 -6
  84. data/lib/kitchen/document.rb +20 -42
  85. data/lib/kitchen/element.rb +9 -3
  86. data/lib/kitchen/element_base.rb +108 -21
  87. data/lib/kitchen/element_enumerator_base.rb +33 -2
  88. data/lib/kitchen/element_enumerator_factory.rb +28 -12
  89. data/lib/kitchen/element_factory.rb +3 -3
  90. data/lib/kitchen/example_element.rb +8 -11
  91. data/lib/kitchen/example_element_enumerator.rb +1 -1
  92. data/lib/kitchen/exercise_element.rb +7 -10
  93. data/lib/kitchen/exercise_element_enumerator.rb +1 -1
  94. data/lib/kitchen/figure_element.rb +8 -11
  95. data/lib/kitchen/figure_element_enumerator.rb +1 -1
  96. data/lib/kitchen/id_tracker.rb +68 -0
  97. data/lib/kitchen/metadata_element.rb +8 -2
  98. data/lib/kitchen/metadata_element_enumerator.rb +1 -1
  99. data/lib/kitchen/note_element.rb +8 -11
  100. data/lib/kitchen/note_element_enumerator.rb +1 -1
  101. data/lib/kitchen/oven.rb +5 -1
  102. data/lib/kitchen/page_element.rb +27 -12
  103. data/lib/kitchen/page_element_enumerator.rb +1 -1
  104. data/lib/kitchen/patches/i18n.rb +34 -0
  105. data/lib/kitchen/patches/integer.rb +24 -0
  106. data/lib/kitchen/patches/nokogiri.rb +62 -0
  107. data/lib/kitchen/patches/nokogiri_profiling.rb +60 -0
  108. data/lib/kitchen/reference_element.rb +27 -0
  109. data/lib/kitchen/references_element_enumerator.rb +20 -0
  110. data/lib/kitchen/search_query.rb +31 -3
  111. data/lib/kitchen/selector.rb +25 -0
  112. data/lib/kitchen/selectors/base.rb +39 -0
  113. data/lib/kitchen/selectors/standard_1.rb +13 -0
  114. data/lib/kitchen/table_element.rb +8 -11
  115. data/lib/kitchen/table_element_enumerator.rb +1 -1
  116. data/lib/kitchen/templates/eob_section_title_template.xhtml.erb +10 -0
  117. data/lib/kitchen/templates/eoc_section_title_template.xhtml.erb +10 -0
  118. data/lib/kitchen/term_element.rb +5 -8
  119. data/lib/kitchen/term_element_enumerator.rb +1 -1
  120. data/lib/kitchen/unit_element.rb +13 -7
  121. data/lib/kitchen/unit_element_enumerator.rb +1 -1
  122. data/lib/kitchen/version.rb +1 -1
  123. data/lib/locales/en.yml +5 -1
  124. data/lib/locales/es.yml +33 -0
  125. data/lib/locales/pl.yml +3 -2
  126. data/lib/openstax_kitchen.rb +2 -5
  127. data/openstax_kitchen.gemspec +1 -0
  128. metadata +66 -25
  129. data/.github/config.yml +0 -14
  130. data/lib/kitchen/directions/bake_book_answer_key/eob_solutions_container.xhtml.erb +0 -9
  131. data/lib/kitchen/directions/bake_book_answer_key/main.rb +0 -11
  132. data/lib/kitchen/directions/bake_book_answer_key/v1.rb +0 -13
  133. data/lib/kitchen/directions/bake_chapter_glossary.rb +0 -39
  134. data/lib/kitchen/directions/bake_chapter_key_concepts/key_concepts.xhtml.erb +0 -16
  135. data/lib/kitchen/directions/bake_chapter_review/chapter_review.xhtml.erb +0 -9
  136. data/lib/kitchen/directions/bake_chapter_review/v1.rb +0 -13
  137. data/lib/kitchen/directions/bake_chapter_review_exercises/main.rb +0 -15
  138. data/lib/kitchen/directions/bake_chapter_review_exercises/review_exercises.xhtml.erb +0 -10
  139. data/lib/kitchen/directions/bake_exercises/main.rb +0 -12
  140. data/lib/kitchen/directions/bake_exercises/v1.rb +0 -169
  141. data/lib/kitchen/directions/bake_notes/bake_notes.rb +0 -48
  142. data/lib/kitchen/directions/bake_notes/bake_numbered_notes.rb +0 -63
  143. data/lib/kitchen/directions/bake_problem_first_elements.rb +0 -16
  144. data/lib/kitchen/transliterations.rb +0 -21
@@ -13,8 +13,14 @@ module Kitchen
13
13
  def initialize(node:, document: nil)
14
14
  super(node: node,
15
15
  document: document,
16
- enumerator_class: PageElementEnumerator,
17
- short_type: :page)
16
+ enumerator_class: PageElementEnumerator)
17
+ end
18
+
19
+ # Returns the short type
20
+ # @return [Symbol]
21
+ #
22
+ def self.short_type
23
+ :page
18
24
  end
19
25
 
20
26
  # Returns the title element. This method is aware that the title of the
@@ -23,9 +29,20 @@ module Kitchen
23
29
  # @raise [ElementNotFoundError] if no matching element is found
24
30
  # @return [Element]
25
31
  #
26
- def title
32
+ def title(reload: false)
27
33
  # The selector for intro titles changes during the baking process
28
- first!(is_introduction? ? selectors.title_in_introduction_page : selectors.title_in_page)
34
+ @title ||= begin
35
+ selector = is_introduction? ? selectors.title_in_introduction_page : selectors.title_in_page
36
+ first!(selector, reload: reload)
37
+ end
38
+ end
39
+
40
+ # Returns the title's text regardless of whether the title has been baked
41
+ #
42
+ # @return [String]
43
+ #
44
+ def title_text
45
+ title.children.one? ? title.text : title.first('.os-text').text
29
46
  end
30
47
 
31
48
  # Returns an enumerator for titles.
@@ -71,11 +88,10 @@ module Kitchen
71
88
 
72
89
  # Returns the summary element.
73
90
  #
74
- # @raise [ElementNotFoundError] if no matching element is found
75
- # @return [Element]
91
+ # @return [Element, nil] the summary or nil if no summary found
76
92
  #
77
93
  def summary
78
- first!(selectors.page_summary)
94
+ first(selectors.page_summary)
79
95
  end
80
96
 
81
97
  # Returns the exercises element.
@@ -95,13 +111,12 @@ module Kitchen
95
111
  search('section.key-concepts')
96
112
  end
97
113
 
98
- # Returns true if this class represents the element for the given node
114
+ # Returns the free response questions
99
115
  #
100
- # @param node [Nokogiri::XML::Node] the underlying node
101
- # @return [Boolean]
116
+ # @return [Element]
102
117
  #
103
- def self.is_the_element_class_for?(node)
104
- node['data-type'] == 'page'
118
+ def free_response
119
+ search('section.free-response')
105
120
  end
106
121
 
107
122
  end
@@ -11,7 +11,7 @@ module Kitchen
11
11
  #
12
12
  def self.factory
13
13
  ElementEnumeratorFactory.new(
14
- default_css_or_xpath: "div[data-type='page']", # TODO: get from config?
14
+ default_css_or_xpath: Selector.named(:page),
15
15
  sub_element_class: PageElement,
16
16
  enumerator_class: self
17
17
  )
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'twitter_cldr'
4
+
5
+ # rubocop:disable Style/Documentation
6
+ module I18n
7
+ def self.sort_strings(first, second)
8
+ string_sorter.compare(first, second)
9
+ end
10
+
11
+ def self.string_sorter
12
+ @string_sorter ||= begin
13
+ # TwitterCldr does not know about our :test locale, so substitute the English one
14
+ locale = I18n.locale == :test ? :en : I18n.locale
15
+ TwitterCldr::Collation::Collator.new(locale)
16
+ end
17
+ end
18
+
19
+ def self.clear_string_sorter
20
+ @string_sorter = nil
21
+ end
22
+
23
+ class <<self
24
+ alias_method :original_locale=, :locale=
25
+ end
26
+
27
+ def self.locale=(locale)
28
+ # We wrap the setting of locale so that we can clear the string sorter (so that it
29
+ # gets reset to the new locale the next time it is used)
30
+ clear_string_sorter
31
+ self.original_locale = locale
32
+ end
33
+ end
34
+ # rubocop:enable Style/Documentation
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Monkey patches for +Integer+
4
+ #
5
+ class Integer
6
+ ROMAN_NUMERALS = %w[0 i ii iii iv v vi vii viii ix x xi xii xiii xiv xv xvi xvii xviii xix xx].freeze
7
+
8
+ # Formats as different types of integers, including roman numerals.
9
+ #
10
+ # @return [String]
11
+ #
12
+ def to_format(format)
13
+ case format
14
+ when :arabic
15
+ to_s
16
+ when :roman
17
+ raise 'Unknown conversion to Roman numerals' if self >= ROMAN_NUMERALS.size
18
+
19
+ ROMAN_NUMERALS[self]
20
+ else
21
+ raise 'Unknown integer format'
22
+ end
23
+ end
24
+ end
@@ -31,6 +31,21 @@ module Nokogiri
31
31
  end
32
32
  end
33
33
  end
34
+
35
+ def add_all_namespaces!
36
+ # Nokogiri by default only recognizes the namespaces on the root node. Collect all
37
+ # namespaces and add them manually.
38
+ return if @all_namespaces_added
39
+
40
+ collect_namespaces.each do |namespace, url|
41
+ prefix, name = namespace.split(':')
42
+ next unless prefix == 'xmlns' && name.present?
43
+
44
+ root.add_namespace_definition(name, url)
45
+ end
46
+
47
+ @all_namespaces_added = true
48
+ end
34
49
  end
35
50
 
36
51
  # Monkey patches for Nokogiri::XML::Node
@@ -43,6 +58,53 @@ module Nokogiri
43
58
  def inspect
44
59
  to_s
45
60
  end
61
+
62
+ def quick_matches?(selector)
63
+ self.class.selector_to_css_nodes(selector).any? { |css_node| matches_css_node?(css_node) }
64
+ end
65
+
66
+ def classes
67
+ self[:class]&.split || []
68
+ end
69
+
70
+ def previous
71
+ prev = previous_element
72
+ return nil if prev.nil?
73
+
74
+ prev.text? ? prev.previous : prev
75
+ end
76
+
77
+ def self.selector_to_css_nodes(selector)
78
+ # No need to parse the same selector more than once.
79
+ @parsed_selectors ||= {}
80
+ @parsed_selectors[selector] ||= Nokogiri::CSS::Parser.new.parse(selector)
81
+ end
82
+
83
+ protected
84
+
85
+ # rubocop:disable Metrics/CyclomaticComplexity
86
+ def matches_css_node?(css_node)
87
+ case css_node.type
88
+ when :CONDITIONAL_SELECTOR
89
+ css_node.value.all? { |inner_css_node| matches_css_node?(inner_css_node) }
90
+ when :ELEMENT_NAME
91
+ css_node.value == ['*'] || css_node.value.include?(name)
92
+ when :CLASS_CONDITION
93
+ (css_node.value & classes).any?
94
+ when :ATTRIBUTE_CONDITION
95
+ attribute, operator, value = css_node.value
96
+
97
+ raise "Unknown attribute condition operator in #{css_node}" if operator != :equal
98
+
99
+ attribute_name = attribute.value
100
+ raise "More attribute names than expected, #{attribute_name}" if attribute_name.many?
101
+
102
+ self[attribute_name.first] == value.gsub('"', '').gsub("'", '')
103
+ else
104
+ raise "Unknown Nokogiri::CSS:Node type in #{css_node}"
105
+ end
106
+ end
107
+ # rubocop:enable Metrics/CyclomaticComplexity
46
108
  end
47
109
  end
48
110
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/Documentation
4
+
5
+ # Make debug output more useful (dumping entire document out is not useful)
6
+ module Nokogiri
7
+ module XML
8
+ # rubocop:disable Style/MutableConstant
9
+ PROFILE_DATA = {}
10
+ # rubocop:enable Style/MutableConstant
11
+
12
+ if ENV['PROFILE']
13
+ ENV['VERBOSE_PROFILE'] = 1 if ENV['PROFILE'].to_s.downcase == 'verbose'
14
+
15
+ # Patches inside Nokogiri to count, time, and print searches. At end of baking
16
+ # you can `puts Nokogiri::XML::PROFILE_DATA` to see the totals. The counts
17
+ # hash is defined outside of the if block so that code that prints it doesn't
18
+ # explode if run without the env var. The `print_profile_data` method is also
19
+ # provided for a nice printout
20
+
21
+ def self.print_profile_data
22
+ total_duration = 0
23
+
24
+ sorted_profile_data = PROFILE_DATA.sort_by { |_, data| data[:time] / data[:count] }.reverse
25
+
26
+ puts "\nSearch Profile Data"
27
+ puts '-----------------------------------------------------------------'
28
+ puts "#{'Total Time (ms)'.ljust(17)}#{'Avg Time (ms)'.ljust(15)}#{'Count'.ljust(7)}Query"
29
+ puts '-----------------------------------------------------------------'
30
+
31
+ sorted_profile_data.each do |search_path, data|
32
+ total_time = format('%0.4f', (data[:time] * 1000))
33
+ avg_time = format('%0.4f', ((data[:time] / data[:count]) * 1000))
34
+ puts total_time.ljust(17) + avg_time.to_s.ljust(15) + data[:count].to_s.ljust(7) + \
35
+ search_path
36
+ total_duration += data[:time] * 1000
37
+ end
38
+
39
+ puts "\nTotal time across all searches (ms): #{total_duration}"
40
+ end
41
+
42
+ class XPathContext
43
+ alias_method :original_evaluate, :evaluate
44
+ def evaluate(search_path, handler=nil)
45
+ puts search_path if ENV['VERBOSE_PROFILE']
46
+
47
+ PROFILE_DATA[search_path] ||= Hash.new(0)
48
+ PROFILE_DATA[search_path][:count] += 1
49
+
50
+ start_time = Time.now
51
+ original_evaluate(search_path, handler).tap do
52
+ PROFILE_DATA[search_path][:time] += Time.now - start_time
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ # rubocop:enable Style/Documentation
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # An element for an example
5
+ #
6
+ class ReferenceElement < ElementBase
7
+
8
+ # Creates a new +ReferenceElement+
9
+ #
10
+ # @param node [Nokogiri::XML::Node] the node this element wraps
11
+ # @param document [Document] this element's document
12
+ #
13
+ def initialize(node:, document: nil)
14
+ super(node: node,
15
+ document: document,
16
+ enumerator_class: ReferenceElementEnumerator)
17
+ end
18
+
19
+ # Returns the short type
20
+ # @return [Symbol]
21
+ #
22
+ def self.short_type
23
+ :reference
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # An enumerator for table elements
5
+ #
6
+ class ReferenceElementEnumerator < ElementEnumeratorBase
7
+ # Returns a factory for this enumerator
8
+ #
9
+ # @return [ElementEnumeratorFactory]
10
+ #
11
+ def self.factory
12
+ ElementEnumeratorFactory.new(
13
+ default_css_or_xpath: Selector.named(:reference),
14
+ sub_element_class: ReferenceElement,
15
+ enumerator_class: self
16
+ )
17
+ end
18
+
19
+ end
20
+ end
@@ -24,6 +24,7 @@ module Kitchen
24
24
  @css_or_xpath = css_or_xpath
25
25
  @only = only.is_a?(String) ? only.to_sym : only
26
26
  @except = except.is_a?(String) ? except.to_sym : except
27
+ @default_already_applied = false
27
28
  end
28
29
 
29
30
  # Returns true iff the element passes the `only` and `except` conditions
@@ -35,13 +36,34 @@ module Kitchen
35
36
  end
36
37
 
37
38
  # Replaces '$' in the `css_or_xpath` with the provided value; also normalizes
38
- # `css_or_xpath` to an array
39
+ # `css_or_xpath` to an array.
39
40
  #
40
- def apply_default_css_or_xpath_and_normalize(default_css_or_xpath=nil)
41
+ # @param default_css_or_xpath [String, Proc, Symbol] The selectors to substitute for the "$" character
42
+ # when this factory is used to build an enumerator. A string argument is used literally. A proc
43
+ # is eventually called given the document's Config object (for accessing selectors). A symbol
44
+ # is interpreted as the name of a selector and is called on the document's Config object's
45
+ # selectors object.
46
+ #
47
+ def apply_default_css_or_xpath_and_normalize(default_css_or_xpath=nil, config: nil)
48
+ return if @default_already_applied
49
+
50
+ default_css_or_xpath = [default_css_or_xpath].flatten.map do |item|
51
+ case item
52
+ when Proc
53
+ item.call(config)
54
+ when Symbol
55
+ config.selectors.send(item)
56
+ else
57
+ item
58
+ end
59
+ end
60
+
41
61
  @as_type = nil
42
62
  @css_or_xpath = [css_or_xpath || '$'].flatten.map do |item|
43
- item.gsub(/\$/, [default_css_or_xpath].flatten.join(', '))
63
+ item.gsub(/\$/, default_css_or_xpath.join(', '))
44
64
  end
65
+
66
+ @default_already_applied = true
45
67
  end
46
68
 
47
69
  # Returns the search query as a spaceless string suitable for use as an element type
@@ -60,6 +82,12 @@ module Kitchen
60
82
  as_type
61
83
  end
62
84
 
85
+ # Returns true if the query has the substitution character ('$')
86
+ #
87
+ def expects_substitution?
88
+ css_or_xpath.nil? || [css_or_xpath].flatten.all? { |item| item.include?('$') }
89
+ end
90
+
63
91
  protected
64
92
 
65
93
  def condition_passes?(method_or_callable, element, success_outcome)
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # A wrapper for a selector. Can be used as the default_css_or_xpath.
5
+ #
6
+ class Selector < Proc
7
+ attr_reader :name
8
+
9
+ def self.named(name)
10
+ @instances ||= {}
11
+ @instances[name] ||= new(name) { |config| config.selectors.send(name) }
12
+ end
13
+
14
+ def initialize(name, &block)
15
+ @name = name
16
+ super(&block)
17
+ end
18
+
19
+ def matches?(node, config:)
20
+ # This may not be incredibly efficient as it does a search of this node's
21
+ # ancestors to see if the node is in the results. Watch the performance.
22
+ node.quick_matches?(call(config))
23
+ end
24
+ end
25
+ end
@@ -17,6 +17,45 @@ module Kitchen
17
17
  # Selector for the summary in a page
18
18
  # @return [String]
19
19
  attr_accessor :page_summary
20
+ # Selector for a reference
21
+ # @return [String]
22
+ attr_accessor :reference
23
+ # Selector for a chapter
24
+ # @return [String]
25
+ attr_accessor :chapter
26
+ # Selector for a page
27
+ # @return [String]
28
+ attr_accessor :page
29
+ # Selector for a note
30
+ # @return [String]
31
+ attr_accessor :note
32
+ # Selector for a term
33
+ # @return [String]
34
+ attr_accessor :term
35
+ # Selector for a table
36
+ # @return [String]
37
+ attr_accessor :table
38
+ # Selector for a figure
39
+ # @return [String]
40
+ attr_accessor :figure
41
+ # Selector for a metadata
42
+ # @return [String]
43
+ attr_accessor :metadata
44
+ # Selector for a composite page
45
+ # @return [String]
46
+ attr_accessor :composite_page
47
+ # Selector for a composite chapter
48
+ # @return [String]
49
+ attr_accessor :composite_chapter
50
+ # Selector for an example
51
+ # @return [String]
52
+ attr_accessor :example
53
+ # Selector for an exercise
54
+ # @return [String]
55
+ attr_accessor :exercise
56
+ # Selector for an unit
57
+ # @return [String]
58
+ attr_accessor :unit
20
59
 
21
60
  # Override specific selectors
22
61
  #