openstax_kitchen 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dacc1aae587e492dc42e693e46195c0ed10ba749f62929e322b0744f2f82e94c
4
- data.tar.gz: 67b4f9691c4431fc172d48381527c1fd75c62dc866ea056ff6666205e73b0e22
3
+ metadata.gz: b80bf07271b311949246bf6a63a9d10fe5f14396175f66a129debcc9e9ef15e3
4
+ data.tar.gz: 7a3a7050d134a0b106d3a80dc18add9209742fca1eea4873f351eb0dd4d05034
5
5
  SHA512:
6
- metadata.gz: 4a788770e4176617a260d8b2ce265303a59490dd1e95727bd38102732ce6459e3600a9eca807e6c80d364b3fb710c2dbce4ec71b66dbec6570b2d61ed8e00e40
7
- data.tar.gz: 6df9ea776c2718b1d7fdc8508210774c29f407391e0802ac6e39a94b98aadfbdb6d47447f0f2a164a66413e122ba7b46e7669fd4af5caeeda26ac0cc591919ff
6
+ metadata.gz: 7fee3b2b4dd92639af2e20520c3f65a31255d944df649e91c5c0e5c8f0a8e1e1f6d19b4477480c5cbc899f7124727c8a40b5a0759985ec94225885baf36b3287
7
+ data.tar.gz: ed62932aa44e597135158cc182daefd2773b89070e1d82750669d4d1f760fa8179f1197c67eddcdec73c1cdc2b1bd7537ca28c8f5603d3a2a7962457d777141f
data/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [4.1.1] - 2021-05-24
10
+
11
+ * Adds low level Nokogiri caching, disabled by default (patch)
12
+ * Cache Selector objects since they don't change (patch)
13
+ * Use more specific selectors when to reduce bake time (patch)
14
+
9
15
  ## [4.1.0] - 2021-05-18
10
16
 
11
17
  * Fixed performance problem with element class detection (patch)
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openstax_kitchen (4.1.0)
4
+ openstax_kitchen (4.1.1)
5
5
  activesupport
6
6
  i18n
7
7
  nokogiri
data/README.md CHANGED
@@ -641,6 +641,22 @@ expect(book_1).to match_normalized_html("some string of HTML here")
641
641
  expect(book_1).to match_html_nodes("some string of HTML here")
642
642
  ```
643
643
 
644
+ ### Profiling
645
+
646
+ If you set the `PROFILE` environment variable to something before you run specs or a recipe, search query profile data will be collected and printed, e.g.
647
+
648
+ ```bash
649
+ %> PROFILE=1 rspec
650
+ ```
651
+
652
+ ### Caching
653
+
654
+ There's a low-level CSS query caching tool that saves repeated queries. In some tests, it saves 15% of query time. It is disabled by default (because we aren't super sure that it is completely safe) but can be turned on with
655
+
656
+ ```ruby
657
+ doc.config.enable_search_cache = true
658
+ ```
659
+
644
660
  ### VSCode
645
661
 
646
662
  1. Visit `vscode:extension/ms-vscode-remote.remote-containers` in a browser
data/codecov.yaml CHANGED
@@ -25,3 +25,4 @@ ignore:
25
25
  - "bin"
26
26
  - "docker"
27
27
  - "books"
28
+ - "lib/kitchen/patches/nokogiri_profiling"
@@ -29,7 +29,7 @@ module Kitchen
29
29
  # @return [BookElement]
30
30
  #
31
31
  def book
32
- BookElement.new(node: nokogiri_document.search('html').first, document: self)
32
+ BookElement.new(node: nokogiri_document.root, document: self)
33
33
  end
34
34
 
35
35
  end
@@ -41,7 +41,7 @@ module Kitchen
41
41
  # @return [Element, nil]
42
42
  #
43
43
  def introduction_page
44
- pages('.introduction').first
44
+ pages('$.introduction').first
45
45
  end
46
46
 
47
47
  # Returns an enumerator for the glossaries
@@ -65,7 +65,7 @@ module Kitchen
65
65
  # @return [ElementEnumerator]
66
66
  #
67
67
  def abstracts
68
- search('[data-type="abstract"]')
68
+ search('div[data-type="abstract"]')
69
69
  end
70
70
 
71
71
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # An enumerator for composite page elements
5
+ #
6
+ class CompositeChapterElementEnumerator < ElementEnumeratorBase
7
+
8
+ # Returns a factory for this enumerator
9
+ #
10
+ # @return [ElementEnumeratorFactory]
11
+ #
12
+ def self.factory
13
+ ElementEnumeratorFactory.new(
14
+ default_css_or_xpath: Selector.named(:composite_chapter),
15
+ sub_element_class: CompositeChapterElement,
16
+ enumerator_class: self
17
+ )
18
+ end
19
+
20
+ end
21
+ end
@@ -17,6 +17,12 @@ module Kitchen
17
17
  #
18
18
  attr_accessor :enable_all_namespaces
19
19
 
20
+ # @!attribute [rw] enable_search_cache
21
+ #
22
+ # @return [Boolean]
23
+ #
24
+ attr_accessor :enable_search_cache
25
+
20
26
  # Creates a new config from a file (not implemented)
21
27
  #
22
28
  def self.new_from_file(_file)
@@ -28,6 +34,7 @@ module Kitchen
28
34
  def initialize(hash: {}, selectors: nil)
29
35
  @selectors = selectors || Kitchen::Selectors::Standard1.new
30
36
  @enable_all_namespaces = hash[:enable_all_namespaces] || true
37
+ @enable_search_cache = hash[:enable_search_cache] || false
31
38
  @hash = hash
32
39
  end
33
40
  end
@@ -19,7 +19,7 @@ module Kitchen::Directions::BakeChapterKeyConcepts
19
19
  key_concepts.each do |key_concept|
20
20
  key_concept.prepend(child: title)
21
21
  key_concept.wrap("<div class='os-section-area'>")
22
- page.search('.os-section-area').first.cut(to: key_concepts_clipboard)
22
+ page.search('div.os-section-area').first.cut(to: key_concepts_clipboard)
23
23
  end
24
24
  end
25
25
 
@@ -4,7 +4,7 @@ module Kitchen
4
4
  module Directions
5
5
  module BakeCompositeChapters
6
6
  def self.v1(book:)
7
- book.search("[data-type='composite-chapter']").each do |chapter|
7
+ book.composite_chapters.each do |chapter|
8
8
  chapter.first("[data-type='document-title']").id =
9
9
  "composite-chapter-#{chapter.count_in(:book)}"
10
10
  end
@@ -4,7 +4,7 @@ module Kitchen
4
4
  module Directions
5
5
  module BakeCompositePages
6
6
  def self.v1(book:)
7
- book.search("[data-type='composite-page']").each do |page|
7
+ book.composite_pages.each do |page|
8
8
  page.id = "composite-page-#{page.count_in(:book)}"
9
9
  end
10
10
  end
@@ -4,7 +4,7 @@ module Kitchen
4
4
  module Directions
5
5
  module BakeEquations
6
6
  def self.v1(book:, number_decorator: :none)
7
- book.chapters.search('[data-type="equation"]:not(.unnumbered)').each do |eq|
7
+ book.chapters.search('div[data-type="equation"]:not(.unnumbered)').each do |eq|
8
8
  chapter = eq.ancestor(:chapter)
9
9
  number = "#{chapter.count_in(:book)}.#{eq.count_in(:chapter)}"
10
10
 
@@ -7,7 +7,7 @@ module Kitchen::Directions::BookAnswerKeyContainer
7
7
  def bake(book:)
8
8
  @metadata = book.metadata.children_to_keep.copy
9
9
  book.body.append(child: render(file: 'eob_solutions_container.xhtml.erb'))
10
- book.body.first('.os-eob.os-solutions-container')
10
+ book.body.first('div.os-eob.os-solutions-container')
11
11
  end
12
12
  end
13
13
  end
@@ -7,7 +7,7 @@ module Kitchen::Directions::ChapterReviewContainer
7
7
  def bake(chapter:, metadata_source:)
8
8
  @metadata = metadata_source.children_to_keep.copy
9
9
  chapter.append(child: render(file: 'chapter_review.xhtml.erb'))
10
- chapter.first('.os-eoc.os-chapter-review-container')
10
+ chapter.first('div.os-eoc.os-chapter-review-container')
11
11
  end
12
12
  end
13
13
  end
@@ -18,7 +18,7 @@ module Kitchen::Directions::MoveSolutionsToAnswerKey
18
18
  def bake_section(chapter:, append_to:, klass:)
19
19
  section_solutions_set = []
20
20
  chapter.search(".#{klass}").each do |section|
21
- section.search('[data-type="solution"]').each do |solution|
21
+ section.search('div[data-type="solution"]').each do |solution|
22
22
  section_solutions_set.push(solution.cut)
23
23
  end
24
24
  end
@@ -31,7 +31,7 @@ module Kitchen::Directions::MoveSolutionsToAnswerKey
31
31
 
32
32
  def bake_from_notes(chapter:, append_to:, klass:)
33
33
  solutions = []
34
- chapter.notes(".#{klass}").each do |note|
34
+ chapter.notes("$.#{klass}").each do |note|
35
35
  note.exercises.each do |exercise|
36
36
  solution = exercise.solution
37
37
  solutions.push(solution.cut) if solution
@@ -47,16 +47,8 @@ module Kitchen
47
47
  @next_paste_count_for_id = {}
48
48
  @id_copy_suffix = '_copy_'
49
49
 
50
- # Nokogiri by default only recognizes the namespaces on the root node. Collect all
51
- # namespaces and add them manually.
52
- return unless @config.enable_all_namespaces && raw.present?
53
-
54
- raw.collect_namespaces.each do |namespace, url|
55
- prefix, name = namespace.split(':')
56
- next unless prefix == 'xmlns' && name.present?
57
-
58
- raw.root.add_namespace_definition(name, url)
59
- end
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
60
52
  end
61
53
 
62
54
  # Returns an enumerator that iterates over all children of this document
@@ -132,6 +132,7 @@ module Kitchen
132
132
  @ancestors = HashWithIndifferentAccess.new
133
133
  @search_query_matches_that_have_been_counted = {}
134
134
  @is_a_clone = false
135
+ @search_cache = {}
135
136
  end
136
137
 
137
138
  # Returns ElementBase descendent type or nil if none found
@@ -345,7 +346,7 @@ module Kitchen
345
346
  # search results if the method or callable returns false
346
347
  # @return [ElementEnumerator]
347
348
  #
348
- def search(*selector_or_xpath_args, only: nil, except: nil)
349
+ def search(*selector_or_xpath_args, only: nil, except: nil, reload: false)
349
350
  block_error_if(block_given?)
350
351
 
351
352
  ElementEnumerator.factory.build_within(
@@ -354,19 +355,29 @@ module Kitchen
354
355
  css_or_xpath: selector_or_xpath_args,
355
356
  only: only,
356
357
  except: except
357
- )
358
+ ),
359
+ reload: reload
358
360
  )
359
361
  end
360
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
+
361
371
  # Yields and returns the first child element that matches the provided
362
372
  # selector or XPath arguments.
363
373
  #
364
374
  # @param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments
375
+ # @param reload [Boolean] ignores cache if true
365
376
  # @yieldparam [Element] the matched XML element
366
377
  # @return [Element, nil] the matched XML element or nil if no match found
367
378
  #
368
- def first(*selector_or_xpath_args)
369
- 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|
370
381
  yield(element) if block_given?
371
382
  end
372
383
  end
@@ -375,12 +386,13 @@ module Kitchen
375
386
  # selector or XPath arguments.
376
387
  #
377
388
  # @param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments
389
+ # @param reload [Boolean] ignores cache if true
378
390
  # @yieldparam [Element] the matched XML element
379
391
  # @raise [ElementNotFoundError] if no matching element is found
380
392
  # @return [Element] the matched XML element
381
393
  #
382
- def first!(*selector_or_xpath_args)
383
- 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|
384
396
  yield(element) if block_given?
385
397
  end
386
398
  end
@@ -694,7 +706,7 @@ module Kitchen
694
706
  # Returns a pages enumerator
695
707
  def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples,
696
708
  :metadatas, :non_introduction_pages, :units, :titles, :exercises, :references,
697
- :composite_pages
709
+ :composite_pages, :composite_chapters
698
710
 
699
711
  # Returns this element as an enumerator (over only one element, itself)
700
712
  #
@@ -84,7 +84,30 @@ module Kitchen
84
84
  #
85
85
  def composite_pages(css_or_xpath=nil, only: nil, except: nil)
86
86
  block_error_if(block_given?)
87
- chain_to(CompositePageElementEnumerator, css_or_xpath: css_or_xpath, only: only, except: except)
87
+ chain_to(CompositePageElementEnumerator,
88
+ css_or_xpath: css_or_xpath,
89
+ only: only,
90
+ except: except)
91
+ end
92
+
93
+ # Returns an enumerator that iterates through composite chapters within the scope of this enumerator
94
+ #
95
+ # @param css_or_xpath [String] additional selectors to further narrow the element iterated over;
96
+ # a "$" in this argument will be replaced with the default selector for the element being
97
+ # iterated over.
98
+ # @param only [Symbol, Callable] the name of a method to call on an element or a
99
+ # lambda or proc that accepts an element; elements will only be included in the
100
+ # search results if the method or callable returns true
101
+ # @param except [Symbol, Callable] the name of a method to call on an element or a
102
+ # lambda or proc that accepts an element; elements will not be included in the
103
+ # search results if the method or callable returns false
104
+ #
105
+ def composite_chapters(css_or_xpath=nil, only: nil, except: nil)
106
+ block_error_if(block_given?)
107
+ chain_to(CompositeChapterElementEnumerator,
108
+ css_or_xpath: css_or_xpath,
109
+ only: only,
110
+ except: except)
88
111
  end
89
112
 
90
113
  # Returns an enumerator that iterates through pages that arent the introduction page within the scope of this enumerator
@@ -39,12 +39,22 @@ module Kitchen
39
39
  # @return [ElementEnumeratorBase] actually returns the concrete enumerator class
40
40
  # given to the factory in its constructor.
41
41
  #
42
- def build_within(enumerator_or_element, search_query: SearchQuery.new)
42
+ def build_within(enumerator_or_element, search_query: SearchQuery.new, reload: false)
43
43
  case enumerator_or_element
44
44
  when ElementBase
45
- build_within_element(enumerator_or_element, search_query: search_query)
45
+ build_within_element(enumerator_or_element,
46
+ search_query: search_query,
47
+ reload: reload)
46
48
  when ElementEnumeratorBase
47
- build_within_other_enumerator(enumerator_or_element, search_query: search_query)
49
+ if enumerator_class != ElementEnumerator && !search_query.expects_substitution?
50
+ raise "Query #{search_query} is missing the substitution character ('$') but " \
51
+ "is run with an enumerator #{enumerator_class.name} that has its own " \
52
+ "selectors for substitution."
53
+ end
54
+
55
+ build_within_other_enumerator(enumerator_or_element,
56
+ search_query: search_query,
57
+ reload: reload)
48
58
  end
49
59
  end
50
60
 
@@ -64,7 +74,7 @@ module Kitchen
64
74
 
65
75
  protected
66
76
 
67
- def build_within_element(element, search_query:)
77
+ def build_within_element(element, search_query:, reload:)
68
78
  search_query.apply_default_css_or_xpath_and_normalize(default_css_or_xpath,
69
79
  config: element.config)
70
80
 
@@ -77,7 +87,7 @@ module Kitchen
77
87
  # below, the counts are correct.
78
88
  element.uncount(search_query)
79
89
 
80
- element.raw.search(*search_query.css_or_xpath).each do |sub_node|
90
+ element.raw_search(*search_query.css_or_xpath, reload: reload).each do |sub_node|
81
91
  sub_element = ElementFactory.build_from_node(
82
92
  node: sub_node,
83
93
  document: element.document,
@@ -105,13 +115,15 @@ module Kitchen
105
115
  end
106
116
  end
107
117
 
108
- def build_within_other_enumerator(other_enumerator, search_query:)
118
+ def build_within_other_enumerator(other_enumerator, search_query:, reload:)
109
119
  # Return a new enumerator instance that internally iterates over `other_enumerator`
110
120
  # running a new enumerator for each element returned by that other enumerator.
111
121
  enumerator_class.new(search_query: search_query,
112
122
  upstream_enumerator: other_enumerator) do |block|
113
123
  other_enumerator.each do |element|
114
- build_within_element(element, search_query: search_query).each do |sub_element|
124
+ build_within_element(element,
125
+ search_query: search_query,
126
+ reload: reload).each do |sub_element|
115
127
  block.yield(sub_element)
116
128
  end
117
129
  end
@@ -28,7 +28,7 @@ module Kitchen
28
28
  # @return ElementEnumerator
29
29
  #
30
30
  def problem
31
- first("[data-type='problem']")
31
+ first("div[data-type='problem']")
32
32
  end
33
33
 
34
34
  # Returns the enumerator for solution.
@@ -36,7 +36,7 @@ module Kitchen
36
36
  # @return ElementEnumerator
37
37
  #
38
38
  def solution
39
- first("[data-type='solution']")
39
+ first("div[data-type='solution']")
40
40
  end
41
41
  end
42
42
  end
data/lib/kitchen/oven.rb CHANGED
@@ -39,6 +39,8 @@ module Kitchen
39
39
  end
40
40
  profile.written!
41
41
 
42
+ Nokogiri::XML.print_profile_data if ENV['PROFILE'] && !ENV['TESTING']
43
+
42
44
  profile
43
45
  end
44
46
 
@@ -29,9 +29,12 @@ module Kitchen
29
29
  # @raise [ElementNotFoundError] if no matching element is found
30
30
  # @return [Element]
31
31
  #
32
- def title
32
+ def title(reload: false)
33
33
  # The selector for intro titles changes during the baking process
34
- 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
35
38
  end
36
39
 
37
40
  # Returns the title's text regardless of whether the title has been baked
@@ -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
@@ -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
@@ -82,6 +82,12 @@ module Kitchen
82
82
  as_type
83
83
  end
84
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
+
85
91
  protected
86
92
 
87
93
  def condition_passes?(method_or_callable, element, success_outcome)
@@ -7,7 +7,8 @@ module Kitchen
7
7
  attr_reader :name
8
8
 
9
9
  def self.named(name)
10
- new(name) { |config| config.selectors.send(name) }
10
+ @instances ||= {}
11
+ @instances[name] ||= new(name) { |config| config.selectors.send(name) }
11
12
  end
12
13
 
13
14
  def initialize(name, &block)
@@ -3,5 +3,5 @@
3
3
  # A library for modifying the structure of OpenStax book XML.
4
4
  #
5
5
  module Kitchen
6
- VERSION = '4.1.0'
6
+ VERSION = '4.1.1'
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openstax_kitchen
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.1.0
4
+ version: 4.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Slavinsky
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-05-20 00:00:00.000000000 Z
11
+ date: 2021-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -220,6 +220,7 @@ files:
220
220
  - lib/kitchen/chapter_element_enumerator.rb
221
221
  - lib/kitchen/clipboard.rb
222
222
  - lib/kitchen/composite_chapter_element.rb
223
+ - lib/kitchen/composite_chapter_element_enumerator.rb
223
224
  - lib/kitchen/composite_page_element.rb
224
225
  - lib/kitchen/composite_page_element_enumerator.rb
225
226
  - lib/kitchen/config.rb
@@ -320,6 +321,7 @@ files:
320
321
  - lib/kitchen/page_element_enumerator.rb
321
322
  - lib/kitchen/pantry.rb
322
323
  - lib/kitchen/patches/nokogiri.rb
324
+ - lib/kitchen/patches/nokogiri_profiling.rb
323
325
  - lib/kitchen/patches/renderable.rb
324
326
  - lib/kitchen/patches/string.rb
325
327
  - lib/kitchen/recipe.rb