openstax_kitchen 4.1.1 → 8.0.0

Sign up to get free protection for your applications and to get access to all the features.
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.