openstax_kitchen 4.1.1 → 8.0.0

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.
Files changed (76) 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 +62 -0
  5. data/Gemfile.lock +16 -7
  6. data/docker/rubocop +24 -0
  7. data/lib/kitchen/composite_page_element.rb +20 -3
  8. data/lib/kitchen/directions/bake_appendix.rb +3 -1
  9. data/lib/kitchen/directions/bake_chapter_glossary/v1.rb +23 -5
  10. data/lib/kitchen/directions/bake_chapter_introductions.rb +22 -15
  11. data/lib/kitchen/directions/bake_chapter_introductions/chapter_introduction.xhtml.erb +0 -0
  12. data/lib/kitchen/directions/bake_chapter_references/main.rb +1 -2
  13. data/lib/kitchen/directions/bake_chapter_references/v1.rb +26 -12
  14. data/lib/kitchen/directions/bake_chapter_section_exercises/main.rb +2 -2
  15. data/lib/kitchen/directions/bake_chapter_section_exercises/v1.rb +2 -1
  16. data/lib/kitchen/directions/bake_chapter_solutions/main.rb +11 -0
  17. data/lib/kitchen/directions/bake_chapter_solutions/v1.rb +37 -0
  18. data/lib/kitchen/directions/bake_chapter_summary.rb +13 -6
  19. data/lib/kitchen/directions/bake_example.rb +11 -2
  20. data/lib/kitchen/directions/bake_figure.rb +13 -0
  21. data/lib/kitchen/directions/bake_first_elements.rb +7 -1
  22. data/lib/kitchen/directions/bake_footnotes/main.rb +2 -2
  23. data/lib/kitchen/directions/bake_footnotes/v1.rb +11 -8
  24. data/lib/kitchen/directions/bake_further_research.rb +2 -0
  25. data/lib/kitchen/directions/bake_index/v1.rb +3 -14
  26. data/lib/kitchen/directions/bake_inline_lists.rb +22 -0
  27. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/main.rb +43 -0
  28. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v1.rb +37 -0
  29. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v2.rb +25 -0
  30. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v3.rb +32 -0
  31. data/lib/kitchen/directions/bake_numbered_exercise/main.rb +3 -2
  32. data/lib/kitchen/directions/bake_numbered_exercise/v1.rb +10 -1
  33. data/lib/kitchen/directions/bake_numbered_table/bake_table_body.rb +29 -0
  34. data/lib/kitchen/directions/bake_numbered_table/main.rb +4 -0
  35. data/lib/kitchen/directions/bake_numbered_table/v1.rb +1 -24
  36. data/lib/kitchen/directions/bake_numbered_table/v2.rb +31 -0
  37. data/lib/kitchen/directions/bake_preface/main.rb +2 -2
  38. data/lib/kitchen/directions/bake_preface/v1.rb +3 -2
  39. data/lib/kitchen/directions/bake_references/main.rb +7 -0
  40. data/lib/kitchen/directions/bake_references/v2.rb +35 -0
  41. data/lib/kitchen/directions/bake_toc.rb +3 -1
  42. data/lib/kitchen/directions/book_answer_key_container/eob_answer_key_outer_container.xhtml.erb +9 -0
  43. data/lib/kitchen/directions/book_answer_key_container/main.rb +2 -2
  44. data/lib/kitchen/directions/book_answer_key_container/v1.rb +4 -3
  45. data/lib/kitchen/directions/chapter_review_container/chapter_review.xhtml.erb +3 -3
  46. data/lib/kitchen/directions/chapter_review_container/main.rb +2 -2
  47. data/lib/kitchen/directions/chapter_review_container/v1.rb +4 -2
  48. data/lib/kitchen/directions/eoc_section_title_link_snippet.rb +13 -0
  49. data/lib/kitchen/directions/move_exercises_to_eoc/main.rb +10 -0
  50. data/lib/kitchen/directions/move_exercises_to_eoc/v3.rb +49 -0
  51. data/lib/kitchen/directions/move_solutions_to_answer_key/main.rb +6 -2
  52. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/default.rb +27 -0
  53. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/precalculus.rb +84 -0
  54. data/lib/kitchen/directions/move_solutions_to_answer_key/v1.rb +11 -7
  55. data/lib/kitchen/document.rb +17 -42
  56. data/lib/kitchen/element_base.rb +31 -7
  57. data/lib/kitchen/i18n_string.rb +16 -0
  58. data/lib/kitchen/id_tracker.rb +68 -0
  59. data/lib/kitchen/oven.rb +3 -1
  60. data/lib/kitchen/page_element.rb +2 -3
  61. data/lib/kitchen/patches/array.rb +15 -0
  62. data/lib/kitchen/patches/i18n.rb +34 -0
  63. data/lib/kitchen/patches/integer.rb +24 -0
  64. data/lib/kitchen/patches/nokogiri.rb +7 -0
  65. data/lib/kitchen/version.rb +1 -1
  66. data/lib/locales/en.yml +2 -1
  67. data/lib/locales/es.yml +33 -0
  68. data/lib/locales/pl.yml +3 -1
  69. data/lib/openstax_kitchen.rb +2 -5
  70. data/openstax_kitchen.gemspec +1 -0
  71. metadata +40 -7
  72. data/.github/config.yml +0 -14
  73. data/lib/kitchen/directions/bake_notes/bake_numbered_notes.rb +0 -51
  74. data/lib/kitchen/directions/book_answer_key_container/eob_solutions_container.xhtml.erb +0 -9
  75. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/american_government.rb +0 -19
  76. data/lib/kitchen/transliterations.rb +0 -21
@@ -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
+ @selectors.each do |selector|
14
+ chapter.search("#{selector} div[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
+ @selectors = strategy_options[:selectors] || (raise 'missing selectors 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
@@ -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,8 +49,7 @@ 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_'
52
+ @id_tracker = IdTracker.new
49
53
 
50
54
  # Nokogiri by default only recognizes the namespaces on the root node. Add all others.
51
55
  raw&.add_all_namespaces! if @config.enable_all_namespaces
@@ -148,38 +152,6 @@ module Kitchen
148
152
  end
149
153
  end
150
154
 
151
- # Keeps track that an element with the given ID has been copied. When such
152
- # elements are pasted, this information is used to give those elements unique
153
- # IDs that don't duplicate the original element.
154
- #
155
- # @param id [String] the ID
156
- #
157
- def record_id_copied(id)
158
- return if id.blank?
159
-
160
- @next_paste_count_for_id[id] ||= 1
161
- end
162
-
163
- # Returns a unique ID given the ID of an element that was copied and is about
164
- # to be pasted
165
- #
166
- # @param original_id [String]
167
- #
168
- def modified_id_to_paste(original_id)
169
- return nil if original_id.nil?
170
- return '' if original_id.blank?
171
-
172
- count = next_count_for_pasted_id(original_id)
173
-
174
- # A count of 0 means the element was cut and this is the first paste, do not
175
- # modify the ID; otherwise, use the uniquified ID.
176
- if count.zero?
177
- original_id
178
- else
179
- "#{original_id}#{@id_copy_suffix}#{count}"
180
- end
181
- end
182
-
183
155
  # Returns the underlying Nokogiri Document object
184
156
  #
185
157
  # @return [Nokogiri::XML::Document]
@@ -188,16 +160,19 @@ module Kitchen
188
160
  @nokogiri_document
189
161
  end
190
162
 
191
- protected
192
-
193
- def next_count_for_pasted_id(id)
194
- return if id.blank?
195
-
196
- (@next_paste_count_for_id[id] ||= 0).tap do
197
- @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
198
171
  end
199
172
  end
200
173
 
174
+ protected
175
+
201
176
  attr_reader :nokogiri_document
202
177
 
203
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
@@ -441,6 +443,11 @@ module Kitchen
441
443
  def cut(to: nil)
442
444
  block_error_if(block_given?)
443
445
 
446
+ raw.traverse do |node|
447
+ next if node.text? || node.document?
448
+
449
+ id_tracker.record_id_cut(node[:id])
450
+ end
444
451
  node.remove
445
452
  get_clipboard(to).add(self) if to.present?
446
453
  self
@@ -461,7 +468,7 @@ module Kitchen
461
468
  the_copy.raw.traverse do |node|
462
469
  next if node.text? || node.document?
463
470
 
464
- document.record_id_copied(node[:id])
471
+ id_tracker.record_id_copied(node[:id])
465
472
  end
466
473
  get_clipboard(to).add(the_copy) if to.present?
467
474
  the_copy
@@ -472,20 +479,22 @@ module Kitchen
472
479
  def paste
473
480
  # See `clone` method for a note about namespaces
474
481
  block_error_if(block_given?)
475
-
476
482
  temp_copy = clone
477
483
  temp_copy.raw.traverse do |node|
478
484
  next if node.text? || node.document?
479
485
 
480
- 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
481
490
  end
482
491
  temp_copy.to_s
483
492
  end
484
493
 
485
494
  # Copy the element's id
486
495
  def copied_id
487
- document.record_id_copied(id)
488
- document.modified_id_to_paste(id)
496
+ id_tracker.record_id_copied(id)
497
+ id_tracker.modified_id_to_paste(id)
489
498
  end
490
499
 
491
500
  # Delete the element
@@ -499,6 +508,21 @@ module Kitchen
499
508
  Element.new(node: raw.parent, document: document, short_type: "parent(#{short_type})")
500
509
  end
501
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
+
502
526
  # TODO: make it clear if all of these methods take Element, Node, or String
503
527
 
504
528
  # If child argument given, prepends it before the element's current children.
@@ -578,9 +602,9 @@ module Kitchen
578
602
  attributes.each do |k, v|
579
603
  new_node[k.to_s.gsub(/([^_])_([^_])/, '\1-\2').gsub('__', '_')] = v
580
604
  end
581
- new_node.children = children.to_s
605
+ new_node.children = children
582
606
  yield Element.new(node: new_node, document: document, short_type: nil) if block_given?
583
- end.to_s
607
+ end
584
608
 
585
609
  self
586
610
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # Compare one string with another
5
+ #
6
+ # Returns 0 if first string equals second,
7
+ # 1 if first string is greater than the second
8
+ # and -1 if first string is less than the second.
9
+ #
10
+ class I18nString < String
11
+
12
+ def <=>(other)
13
+ I18n.sort_strings(self, other)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # A class to track and modify duplicate IDs in a document
5
+ #
6
+ class IdTracker
7
+
8
+ def initialize
9
+ @id_data = Hash.new { |hash, key| hash[key] = { count: 0, last_pasted: false } }
10
+ @id_copy_suffix = '_copy_'
11
+ end
12
+
13
+ # Keeps track that an element with the given ID has been copied. When such
14
+ # elements are pasted, this information is used to give those elements unique
15
+ # IDs that don't duplicate the original element.
16
+ #
17
+ # @param id [String] the ID
18
+ #
19
+ def record_id_copied(id)
20
+ return if id.blank?
21
+
22
+ @id_data[id][:count] += 1
23
+ @id_data[id][:last_pasted] = false
24
+ end
25
+
26
+ # Keeps track that an element with the given ID has been cut.
27
+ #
28
+ # @param id [String]
29
+ #
30
+ def record_id_cut(id)
31
+ return if id.blank?
32
+
33
+ @id_data[id][:count] -= 1 if @id_data[id][:count].positive?
34
+ @id_data[id][:last_pasted] = false
35
+ end
36
+
37
+ # Keeps track that an element with the given ID has been pasted.
38
+ #
39
+ # @param id [String]
40
+ #
41
+ def record_id_pasted(id)
42
+ return if id.blank?
43
+
44
+ @id_data[id][:count] += 1 if @id_data[id][:last_pasted]
45
+ @id_data[id][:last_pasted] = true
46
+ end
47
+
48
+ # Returns a unique ID given the ID of an element that was copied and is about
49
+ # to be pasted
50
+ #
51
+ # @param original_id [String]
52
+ # @return [String]
53
+ #
54
+ def modified_id_to_paste(original_id)
55
+ return nil if original_id.nil?
56
+ return '' if original_id.blank?
57
+
58
+ count = @id_data[original_id][:count]
59
+ # A count of 0 means the element was cut and this is the first paste, do not
60
+ # modify the ID; otherwise, use the uniquified ID.
61
+ if count.zero?
62
+ original_id
63
+ else
64
+ "#{original_id}#{@id_copy_suffix}#{count}"
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/kitchen/oven.rb CHANGED
@@ -28,6 +28,8 @@ module Kitchen
28
28
  config: config
29
29
  )
30
30
 
31
+ I18n.locale = doc.locale
32
+
31
33
  [recipes].flatten.each do |recipe|
32
34
  recipe.document = doc
33
35
  recipe.bake
@@ -35,7 +37,7 @@ module Kitchen
35
37
  profile.baked!
36
38
 
37
39
  File.open(output_file, 'w') do |f|
38
- f.write doc.to_xhtml(indent: 2)
40
+ f.write doc.to_xhtml(indent: 2, encoding: doc.encoding || 'utf-8')
39
41
  end
40
42
  profile.written!
41
43
 
@@ -88,11 +88,10 @@ module Kitchen
88
88
 
89
89
  # Returns the summary element.
90
90
  #
91
- # @raise [ElementNotFoundError] if no matching element is found
92
- # @return [Element]
91
+ # @return [Element, nil] the summary or nil if no summary found
93
92
  #
94
93
  def summary
95
- first!(selectors.page_summary)
94
+ first(selectors.page_summary)
96
95
  end
97
96
 
98
97
  # Returns the exercises element.