openstax_kitchen 4.0.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/changelog.yml +24 -0
  3. data/.github/workflows/rubocop.yml +28 -0
  4. data/CHANGELOG.md +58 -0
  5. data/Gemfile.lock +15 -6
  6. data/README.md +16 -0
  7. data/codecov.yaml +1 -0
  8. data/docker/rubocop +22 -0
  9. data/lib/kitchen/book_document.rb +1 -1
  10. data/lib/kitchen/chapter_element.rb +2 -2
  11. data/lib/kitchen/composite_chapter_element_enumerator.rb +21 -0
  12. data/lib/kitchen/composite_page_element.rb +19 -2
  13. data/lib/kitchen/config.rb +7 -0
  14. data/lib/kitchen/directions/bake_appendix.rb +3 -1
  15. data/lib/kitchen/directions/bake_chapter_introductions.rb +22 -15
  16. data/lib/kitchen/directions/bake_chapter_introductions/chapter_introduction.xhtml.erb +0 -0
  17. data/lib/kitchen/directions/bake_chapter_key_concepts/v1.rb +1 -1
  18. data/lib/kitchen/directions/bake_chapter_references/main.rb +15 -0
  19. data/lib/kitchen/directions/bake_chapter_references/v1.rb +49 -0
  20. data/lib/kitchen/directions/bake_chapter_section_exercises/main.rb +2 -2
  21. data/lib/kitchen/directions/bake_chapter_section_exercises/v1.rb +2 -1
  22. data/lib/kitchen/directions/bake_chapter_solutions/main.rb +11 -0
  23. data/lib/kitchen/directions/bake_chapter_solutions/v1.rb +37 -0
  24. data/lib/kitchen/directions/bake_chapter_summary.rb +13 -6
  25. data/lib/kitchen/directions/bake_composite_chapters.rb +1 -1
  26. data/lib/kitchen/directions/bake_composite_pages.rb +1 -1
  27. data/lib/kitchen/directions/bake_equations.rb +1 -1
  28. data/lib/kitchen/directions/bake_example.rb +4 -1
  29. data/lib/kitchen/directions/bake_figure.rb +13 -0
  30. data/lib/kitchen/directions/bake_first_elements.rb +7 -1
  31. data/lib/kitchen/directions/bake_footnotes/main.rb +2 -2
  32. data/lib/kitchen/directions/bake_footnotes/v1.rb +11 -8
  33. data/lib/kitchen/directions/bake_further_research.rb +2 -0
  34. data/lib/kitchen/directions/bake_index/v1.rb +3 -14
  35. data/lib/kitchen/directions/bake_inline_lists.rb +22 -0
  36. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/main.rb +43 -0
  37. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v1.rb +37 -0
  38. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v2.rb +25 -0
  39. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v3.rb +32 -0
  40. data/lib/kitchen/directions/bake_numbered_exercise/main.rb +3 -2
  41. data/lib/kitchen/directions/bake_numbered_exercise/v1.rb +10 -1
  42. data/lib/kitchen/directions/bake_numbered_table/bake_table_body.rb +29 -0
  43. data/lib/kitchen/directions/bake_numbered_table/main.rb +4 -0
  44. data/lib/kitchen/directions/bake_numbered_table/v1.rb +1 -24
  45. data/lib/kitchen/directions/bake_numbered_table/v2.rb +31 -0
  46. data/lib/kitchen/directions/bake_preface/main.rb +2 -2
  47. data/lib/kitchen/directions/bake_preface/v1.rb +3 -2
  48. data/lib/kitchen/directions/bake_references/main.rb +7 -0
  49. data/lib/kitchen/directions/bake_references/v2.rb +35 -0
  50. data/lib/kitchen/directions/bake_toc.rb +3 -1
  51. data/lib/kitchen/directions/book_answer_key_container/eob_answer_key_outer_container.xhtml.erb +9 -0
  52. data/lib/kitchen/directions/book_answer_key_container/main.rb +2 -2
  53. data/lib/kitchen/directions/book_answer_key_container/v1.rb +4 -3
  54. data/lib/kitchen/directions/chapter_review_container/chapter_review.xhtml.erb +3 -3
  55. data/lib/kitchen/directions/chapter_review_container/main.rb +2 -2
  56. data/lib/kitchen/directions/chapter_review_container/v1.rb +4 -2
  57. data/lib/kitchen/directions/eoc_section_title_link_snippet.rb +13 -0
  58. data/lib/kitchen/directions/move_exercises_to_eoc/main.rb +10 -0
  59. data/lib/kitchen/directions/move_exercises_to_eoc/v3.rb +49 -0
  60. data/lib/kitchen/directions/move_solutions_to_answer_key/main.rb +6 -2
  61. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/default.rb +27 -0
  62. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/precalculus.rb +84 -0
  63. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/uphysics.rb +2 -2
  64. data/lib/kitchen/directions/move_solutions_to_answer_key/v1.rb +11 -7
  65. data/lib/kitchen/document.rb +19 -52
  66. data/lib/kitchen/element_base.rb +48 -12
  67. data/lib/kitchen/element_enumerator_base.rb +24 -1
  68. data/lib/kitchen/element_enumerator_factory.rb +19 -7
  69. data/lib/kitchen/exercise_element.rb +2 -2
  70. data/lib/kitchen/id_tracker.rb +68 -0
  71. data/lib/kitchen/oven.rb +5 -1
  72. data/lib/kitchen/page_element.rb +7 -5
  73. data/lib/kitchen/patches/i18n.rb +34 -0
  74. data/lib/kitchen/patches/integer.rb +24 -0
  75. data/lib/kitchen/patches/nokogiri.rb +62 -0
  76. data/lib/kitchen/patches/nokogiri_profiling.rb +60 -0
  77. data/lib/kitchen/search_query.rb +6 -0
  78. data/lib/kitchen/selector.rb +3 -2
  79. data/lib/kitchen/version.rb +1 -1
  80. data/lib/locales/en.yml +2 -1
  81. data/lib/locales/es.yml +33 -0
  82. data/lib/locales/pl.yml +4 -2
  83. data/lib/openstax_kitchen.rb +1 -5
  84. data/openstax_kitchen.gemspec +1 -0
  85. metadata +42 -7
  86. data/.github/config.yml +0 -14
  87. data/lib/kitchen/directions/bake_notes/bake_numbered_notes.rb +0 -51
  88. data/lib/kitchen/directions/book_answer_key_container/eob_solutions_container.xhtml.erb +0 -9
  89. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/american_government.rb +0 -19
  90. data/lib/kitchen/transliterations.rb +0 -21
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::MoveExercisesToEOC
4
+ # The difference from v1 is the presence of a section title
5
+ # and from v2 the lack of additional "os-section-area" and os-#{@klass} wrappers
6
+ class V3
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
+ section_title = Kitchen::Directions::EocSectionTitleLinkSnippet.v1(page: page)
24
+
25
+ exercise_section.exercises.each do |exercise|
26
+ exercise.document.pantry(name: :link_text).store(
27
+ "#{I18n.t(:exercise_label)} #{chapter.count_in(:book)}.#{exercise.count_in(:chapter)}",
28
+ label: exercise.id
29
+ )
30
+ end
31
+
32
+ # Configure section title
33
+ exercise_section.prepend(child: section_title)
34
+ exercise_section.cut(to: exercise_clipboard)
35
+ end
36
+ end
37
+
38
+ return if exercise_clipboard.none?
39
+
40
+ @content = exercise_clipboard.paste
41
+
42
+ append_to_element = append_to || chapter
43
+ @in_composite_chapter = append_to_element.is?(: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
@@ -3,11 +3,15 @@
3
3
  module Kitchen
4
4
  module Directions
5
5
  module MoveSolutionsToAnswerKey
6
- def self.v1(chapter:, metadata_source:, strategy:, append_to:)
6
+ def self.v1(chapter:, metadata_source:, strategy:, append_to:, strategy_options: {}, solutions_plural: true)
7
7
  V1.new.bake(
8
8
  chapter: chapter,
9
9
  metadata_source: metadata_source,
10
- strategy: strategy, append_to: append_to)
10
+ strategy: strategy,
11
+ append_to: append_to,
12
+ strategy_options: strategy_options,
13
+ solutions_plural: solutions_plural
14
+ )
11
15
  end
12
16
  end
13
17
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::MoveSolutionsToAnswerKey
4
+ module Strategies
5
+ class Default
6
+ def bake(chapter:, append_to:)
7
+ bake_section(chapter: chapter, append_to: append_to)
8
+ end
9
+
10
+ protected
11
+
12
+ def bake_section(chapter:, append_to:)
13
+ @classes.each do |klass|
14
+ chapter.search(".#{klass} [data-type='solution']").each do |solution|
15
+ append_to.add_child(solution.cut.to_s)
16
+ end
17
+ end
18
+ end
19
+
20
+ # This method helps to obtain more strategy-specific params through
21
+ # "strategy_options: {blah1: 1, blah2: 2}"
22
+ def initialize(strategy_options)
23
+ @classes = strategy_options[:classes] || (raise 'missing classes for strategy')
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::MoveSolutionsToAnswerKey
4
+ module Strategies
5
+ class Precalculus
6
+ def bake(chapter:, append_to:)
7
+ try_note_solutions(chapter: chapter, append_to: append_to)
8
+
9
+ # Bake section exercises
10
+ chapter.non_introduction_pages.each do |page|
11
+ number = "#{chapter.count_in(:book)}.#{page.count_in(:chapter)}"
12
+ bake_section(chapter: page, append_to: append_to, klass: 'section-exercises',
13
+ number: number)
14
+ end
15
+
16
+ # Bake other types of exercises
17
+ classes = %w[review-exercises practice-test]
18
+ classes.each do |klass|
19
+ bake_section(chapter: chapter, append_to: append_to, klass: klass)
20
+ end
21
+ end
22
+
23
+ protected
24
+
25
+ def bake_section(chapter:, append_to:, klass:, number: nil)
26
+ section_solutions_set = Kitchen::Clipboard.new
27
+ chapter.search(".#{klass}").each do |section|
28
+ section.search('[data-type="solution"]').each do |solution|
29
+ solution.cut(to: section_solutions_set)
30
+ end
31
+ end
32
+
33
+ return if section_solutions_set.items.empty?
34
+
35
+ title = <<~HTML
36
+ <h3 data-type="title">
37
+ <span class="os-title-label">#{I18n.t(:"eoc.#{klass}", number: number)}</span>
38
+ </h3>
39
+ HTML
40
+
41
+ append_solution_area(title: title, solutions: section_solutions_set, append_to: append_to)
42
+ end
43
+
44
+ def try_note_solutions(chapter:, append_to:)
45
+ append_to.add_child(
46
+ <<~HTML
47
+ <div class="os-module-reset-solution-area os-try-solution-area">
48
+ <h3 data-type="title">
49
+ <span class="os-title-label">#{I18n.t(:"notes.try")}</span>
50
+ </h3>
51
+ </div>
52
+ HTML
53
+ )
54
+ chapter.non_introduction_pages.each do |page|
55
+ solutions = Kitchen::Clipboard.new
56
+ page.notes('$.try').each do |note|
57
+ note.exercises.each do |exercise|
58
+ solution = exercise.solution
59
+ solution&.cut(to: solutions) #if solution
60
+ end
61
+ end
62
+ next if solutions.items.empty?
63
+
64
+ title_snippet = Kitchen::Directions::EocSectionTitleLinkSnippet.v2(page: page)
65
+
66
+ append_solution_area(title: title_snippet, solutions: solutions,
67
+ append_to: append_to.search('.os-try-solution-area').first)
68
+ end
69
+ end
70
+
71
+ def append_solution_area(title:, solutions:, append_to:)
72
+ append_to = append_to.add_child(
73
+ <<~HTML
74
+ <div class="os-solution-area">
75
+ #{title}
76
+ </div>
77
+ HTML
78
+ ).first
79
+
80
+ append_to.add_child(solutions.paste)
81
+ end
82
+ end
83
+ end
84
+ 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
@@ -2,22 +2,26 @@
2
2
 
3
3
  module Kitchen::Directions::MoveSolutionsToAnswerKey
4
4
  class V1
5
- def bake(chapter:, metadata_source:, strategy:, append_to:)
5
+ def bake(chapter:, metadata_source:, strategy:, append_to:, strategy_options: {}, solutions_plural: true)
6
6
  strategy =
7
7
  case strategy
8
8
  when :calculus
9
- Strategies::Calculus
9
+ Strategies::Calculus.new
10
10
  when :uphysics
11
- Strategies::UPhysics
12
- when :american_government
13
- Strategies::AmericanGovernment
11
+ Strategies::UPhysics.new
12
+ when :precalculus
13
+ Strategies::Precalculus.new
14
+ when :default
15
+ Strategies::Default.new(strategy_options)
14
16
  else
15
17
  raise 'No such strategy'
16
18
  end
17
19
 
20
+ solutions_or_solution = solutions_plural ? 'solutions' : 'solution'
18
21
  append_to.append(child:
19
22
  <<~HTML
20
- <div class="os-eob os-solutions-container" data-type="composite-page" data-uuid-key=".solutions#{chapter.count_in(:book)}">
23
+ <div class="os-eob os-#{solutions_or_solution}-container" data-type="composite-page" \
24
+ data-uuid-key=".#{solutions_or_solution}#{chapter.count_in(:book)}">
21
25
  <h2 data-type="document-title">
22
26
  <span class="os-text">#{I18n.t(:chapter)} #{chapter.count_in(:book)}</span>
23
27
  </h2>
@@ -28,7 +32,7 @@ module Kitchen::Directions::MoveSolutionsToAnswerKey
28
32
  </div>
29
33
  HTML
30
34
  )
31
- strategy.new.bake(chapter: chapter, append_to: append_to.last_element)
35
+ strategy.bake(chapter: chapter, append_to: append_to.last_element)
32
36
  end
33
37
  end
34
38
  end
@@ -13,6 +13,8 @@ module Kitchen
13
13
  attr_accessor :location
14
14
  # @return [Config] the configuration used in this document
15
15
  attr_reader :config
16
+ # @return [IdTracker] the counter for duplicate IDs
17
+ attr_reader :id_tracker
16
18
 
17
19
  # @!method selectors
18
20
  # The document's selectors
@@ -34,7 +36,10 @@ module Kitchen
34
36
  # @!method to_html
35
37
  # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#to_html-instance_method Nokogiri::XML::Node#to_html
36
38
  # @return [String] the document as an HTML string
37
- def_delegators :@nokogiri_document, :to_xhtml, :to_s, :to_xml, :to_html
39
+ # @!method encoding
40
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Document#encoding-instance_method Nokogiri::XML::Document#encoding
41
+ # @return [String] the document as an HTML string
42
+ def_delegators :@nokogiri_document, :to_xhtml, :to_s, :to_xml, :to_html, :encoding
38
43
 
39
44
  # Return a new instance of Document
40
45
  #
@@ -44,19 +49,10 @@ module Kitchen
44
49
  @nokogiri_document = nokogiri_document
45
50
  @location = nil
46
51
  @config = config || Config.new
47
- @next_paste_count_for_id = {}
48
- @id_copy_suffix = '_copy_'
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?
52
+ @id_tracker = IdTracker.new
57
53
 
58
- raw.root.add_namespace_definition(name, url)
59
- end
54
+ # Nokogiri by default only recognizes the namespaces on the root node. Add all others.
55
+ raw&.add_all_namespaces! if @config.enable_all_namespaces
60
56
  end
61
57
 
62
58
  # Returns an enumerator that iterates over all children of this document
@@ -156,38 +152,6 @@ module Kitchen
156
152
  end
157
153
  end
158
154
 
159
- # Keeps track that an element with the given ID has been copied. When such
160
- # elements are pasted, this information is used to give those elements unique
161
- # IDs that don't duplicate the original element.
162
- #
163
- # @param id [String] the ID
164
- #
165
- def record_id_copied(id)
166
- return if id.blank?
167
-
168
- @next_paste_count_for_id[id] ||= 1
169
- end
170
-
171
- # Returns a unique ID given the ID of an element that was copied and is about
172
- # to be pasted
173
- #
174
- # @param original_id [String]
175
- #
176
- def modified_id_to_paste(original_id)
177
- return nil if original_id.nil?
178
- return '' if original_id.blank?
179
-
180
- count = next_count_for_pasted_id(original_id)
181
-
182
- # A count of 0 means the element was cut and this is the first paste, do not
183
- # modify the ID; otherwise, use the uniquified ID.
184
- if count.zero?
185
- original_id
186
- else
187
- "#{original_id}#{@id_copy_suffix}#{count}"
188
- end
189
- end
190
-
191
155
  # Returns the underlying Nokogiri Document object
192
156
  #
193
157
  # @return [Nokogiri::XML::Document]
@@ -196,16 +160,19 @@ module Kitchen
196
160
  @nokogiri_document
197
161
  end
198
162
 
199
- protected
200
-
201
- def next_count_for_pasted_id(id)
202
- return if id.blank?
203
-
204
- (@next_paste_count_for_id[id] ||= 0).tap do
205
- @next_paste_count_for_id[id] += 1
163
+ # Returns the locale for this document, default to `:en` if no locale detected
164
+ #
165
+ # @return [Symbol]
166
+ #
167
+ def locale
168
+ raw.root['lang']&.to_sym || begin
169
+ warn 'No `lang` attribute on this document so cannot detect its locale; defaulting to `:en`'
170
+ :en
206
171
  end
207
172
  end
208
173
 
174
+ protected
175
+
209
176
  attr_reader :nokogiri_document
210
177
 
211
178
  end
@@ -101,6 +101,8 @@ module Kitchen
101
101
  # @return [Clipboard]
102
102
  def_delegators :document, :pantry, :clipboard
103
103
 
104
+ def_delegators :document, :id_tracker
105
+
104
106
  # Creates a new instance
105
107
  #
106
108
  # @param node [Nokogiri::XML::Node] the wrapped element
@@ -132,6 +134,7 @@ module Kitchen
132
134
  @ancestors = HashWithIndifferentAccess.new
133
135
  @search_query_matches_that_have_been_counted = {}
134
136
  @is_a_clone = false
137
+ @search_cache = {}
135
138
  end
136
139
 
137
140
  # Returns ElementBase descendent type or nil if none found
@@ -345,7 +348,7 @@ module Kitchen
345
348
  # search results if the method or callable returns false
346
349
  # @return [ElementEnumerator]
347
350
  #
348
- def search(*selector_or_xpath_args, only: nil, except: nil)
351
+ def search(*selector_or_xpath_args, only: nil, except: nil, reload: false)
349
352
  block_error_if(block_given?)
350
353
 
351
354
  ElementEnumerator.factory.build_within(
@@ -354,19 +357,29 @@ module Kitchen
354
357
  css_or_xpath: selector_or_xpath_args,
355
358
  only: only,
356
359
  except: except
357
- )
360
+ ),
361
+ reload: reload
358
362
  )
359
363
  end
360
364
 
365
+ def raw_search(*selector_or_xpath_args, reload: false)
366
+ key = selector_or_xpath_args
367
+ @search_cache[key] = nil if reload || !config.enable_search_cache
368
+ # cache nil search results with a fake -1 value
369
+ @search_cache[key] ||= raw.search(*selector_or_xpath_args) || -1
370
+ @search_cache[key] == -1 ? nil : @search_cache[key]
371
+ end
372
+
361
373
  # Yields and returns the first child element that matches the provided
362
374
  # selector or XPath arguments.
363
375
  #
364
376
  # @param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments
377
+ # @param reload [Boolean] ignores cache if true
365
378
  # @yieldparam [Element] the matched XML element
366
379
  # @return [Element, nil] the matched XML element or nil if no match found
367
380
  #
368
- def first(*selector_or_xpath_args)
369
- search(*selector_or_xpath_args).first.tap do |element|
381
+ def first(*selector_or_xpath_args, reload: false)
382
+ search(*selector_or_xpath_args, reload: reload).first.tap do |element|
370
383
  yield(element) if block_given?
371
384
  end
372
385
  end
@@ -375,12 +388,13 @@ module Kitchen
375
388
  # selector or XPath arguments.
376
389
  #
377
390
  # @param selector_or_xpath_args [Array<String>] CSS selectors or XPath arguments
391
+ # @param reload [Boolean] ignores cache if true
378
392
  # @yieldparam [Element] the matched XML element
379
393
  # @raise [ElementNotFoundError] if no matching element is found
380
394
  # @return [Element] the matched XML element
381
395
  #
382
- def first!(*selector_or_xpath_args)
383
- search(*selector_or_xpath_args).first!.tap do |element|
396
+ def first!(*selector_or_xpath_args, reload: false)
397
+ search(*selector_or_xpath_args, reload: reload).first!.tap do |element|
384
398
  yield(element) if block_given?
385
399
  end
386
400
  end
@@ -429,6 +443,11 @@ module Kitchen
429
443
  def cut(to: nil)
430
444
  block_error_if(block_given?)
431
445
 
446
+ raw.traverse do |node|
447
+ next if node.text? || node.document?
448
+
449
+ id_tracker.record_id_cut(node[:id])
450
+ end
432
451
  node.remove
433
452
  get_clipboard(to).add(self) if to.present?
434
453
  self
@@ -449,7 +468,7 @@ module Kitchen
449
468
  the_copy.raw.traverse do |node|
450
469
  next if node.text? || node.document?
451
470
 
452
- document.record_id_copied(node[:id])
471
+ id_tracker.record_id_copied(node[:id])
453
472
  end
454
473
  get_clipboard(to).add(the_copy) if to.present?
455
474
  the_copy
@@ -460,20 +479,22 @@ module Kitchen
460
479
  def paste
461
480
  # See `clone` method for a note about namespaces
462
481
  block_error_if(block_given?)
463
-
464
482
  temp_copy = clone
465
483
  temp_copy.raw.traverse do |node|
466
484
  next if node.text? || node.document?
467
485
 
468
- node[:id] = document.modified_id_to_paste(node[:id]) unless node[:id].blank?
486
+ if node[:id].present?
487
+ id_tracker.record_id_pasted(node[:id])
488
+ node[:id] = id_tracker.modified_id_to_paste(node[:id])
489
+ end
469
490
  end
470
491
  temp_copy.to_s
471
492
  end
472
493
 
473
494
  # Copy the element's id
474
495
  def copied_id
475
- document.record_id_copied(id)
476
- document.modified_id_to_paste(id)
496
+ id_tracker.record_id_copied(id)
497
+ id_tracker.modified_id_to_paste(id)
477
498
  end
478
499
 
479
500
  # Delete the element
@@ -487,6 +508,21 @@ module Kitchen
487
508
  Element.new(node: raw.parent, document: document, short_type: "parent(#{short_type})")
488
509
  end
489
510
 
511
+ # returns previous element
512
+ # skips double indentations that the nokigiri sometimes picks up
513
+ # nil if there's no previous sibling
514
+ #
515
+ def previous
516
+ prev = raw.previous
517
+ return prev if prev.nil?
518
+
519
+ Element.new(
520
+ node: prev,
521
+ document: document,
522
+ short_type: "previous(#{short_type})"
523
+ )
524
+ end
525
+
490
526
  # TODO: make it clear if all of these methods take Element, Node, or String
491
527
 
492
528
  # If child argument given, prepends it before the element's current children.
@@ -694,7 +730,7 @@ module Kitchen
694
730
  # Returns a pages enumerator
695
731
  def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples,
696
732
  :metadatas, :non_introduction_pages, :units, :titles, :exercises, :references,
697
- :composite_pages
733
+ :composite_pages, :composite_chapters
698
734
 
699
735
  # Returns this element as an enumerator (over only one element, itself)
700
736
  #