openstax_kitchen 11.0.0 → 11.1.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/Gemfile.lock +1 -1
  4. data/lib/kitchen/composite_page_element.rb +8 -0
  5. data/lib/kitchen/directions/bake_chapter_introductions/bake_chapter_objectives.rb +46 -0
  6. data/lib/kitchen/directions/bake_chapter_introductions/bake_chapter_outline.rb +14 -0
  7. data/lib/kitchen/directions/bake_chapter_introductions/main.rb +43 -0
  8. data/lib/kitchen/directions/bake_chapter_introductions/v1.rb +56 -0
  9. data/lib/kitchen/directions/bake_chapter_introductions/v2.rb +91 -0
  10. data/lib/kitchen/directions/bake_custom_sections/main.rb +14 -0
  11. data/lib/kitchen/directions/bake_custom_sections/v1.rb +42 -0
  12. data/lib/kitchen/directions/bake_equations.rb +2 -3
  13. data/lib/kitchen/directions/bake_example.rb +4 -5
  14. data/lib/kitchen/directions/bake_figure.rb +5 -3
  15. data/lib/kitchen/directions/bake_iframes/main.rb +11 -0
  16. data/lib/kitchen/directions/bake_iframes/v1.rb +25 -0
  17. data/lib/kitchen/directions/bake_index/main.rb +2 -2
  18. data/lib/kitchen/directions/bake_index/v1.rb +39 -7
  19. data/lib/kitchen/directions/bake_index/v1.xhtml.erb +3 -3
  20. data/lib/kitchen/directions/bake_injected_exercise.rb +18 -0
  21. data/lib/kitchen/directions/bake_injected_exercise_question.rb +71 -0
  22. data/lib/kitchen/directions/bake_link_placeholders.rb +15 -2
  23. data/lib/kitchen/directions/bake_lists_with_para.rb +3 -3
  24. data/lib/kitchen/directions/bake_notes/bake_autotitled_notes.rb +5 -5
  25. data/lib/kitchen/directions/bake_notes/bake_note_subtitle.rb +6 -2
  26. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/main.rb +13 -3
  27. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v1.rb +29 -25
  28. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v2.rb +22 -17
  29. data/lib/kitchen/directions/bake_notes/bake_numbered_notes/v3.rb +27 -22
  30. data/lib/kitchen/directions/bake_numbered_exercise/main.rb +3 -2
  31. data/lib/kitchen/directions/bake_numbered_exercise/v1.rb +6 -24
  32. data/lib/kitchen/directions/bake_numbered_table/bake_table_body.rb +3 -3
  33. data/lib/kitchen/directions/bake_numbered_table/main.rb +4 -4
  34. data/lib/kitchen/directions/bake_numbered_table/v1.rb +3 -3
  35. data/lib/kitchen/directions/bake_numbered_table/v2.rb +3 -3
  36. data/lib/kitchen/directions/bake_toc.rb +1 -1
  37. data/lib/kitchen/directions/move_solutions_to_answer_key/move_solutions_from_exercise_section.rb +1 -7
  38. data/lib/kitchen/directions/move_solutions_to_answer_key/move_solutions_from_numbered_note.rb +1 -7
  39. data/lib/kitchen/directions/move_solutions_to_answer_key/strategies/contemporary_math.rb +17 -0
  40. data/lib/kitchen/element_base.rb +38 -2
  41. data/lib/kitchen/element_enumerator_base.rb +35 -0
  42. data/lib/kitchen/injected_question_element.rb +77 -0
  43. data/lib/kitchen/injected_question_element_enumerator.rb +21 -0
  44. data/lib/kitchen/selectors/base.rb +6 -0
  45. data/lib/kitchen/selectors/standard_1.rb +3 -0
  46. data/lib/kitchen/solution_element_enumerator.rb +21 -0
  47. data/lib/kitchen/version.rb +1 -1
  48. data/lib/locales/en.yml +6 -4
  49. data/lib/locales/es.yml +5 -4
  50. data/lib/locales/pl.yml +52 -7
  51. metadata +17 -6
  52. data/lib/kitchen/directions/bake_chapter_introductions/chapter_introduction.xhtml.erb +0 -0
  53. data/lib/kitchen/directions/bake_chapter_introductions.rb +0 -65
  54. data/lib/kitchen/directions/bake_notes/bake_note_iframes.rb +0 -27
@@ -1,29 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Kitchen::Directions::BakeNumberedNotes
4
- class V3
5
- # for the try it notes, must be called AFTER bake_exercises
6
- def bake(book:, classes:, suppress_solution: true)
7
- classes.each do |klass|
8
- book.chapters.notes("$.#{klass}").each do |note|
9
- note.wrap_children(class: 'os-note-body')
10
- previous_example = note.previous
11
- os_number = previous_example&.first('.os-number')&.children&.to_s
3
+ module Kitchen::Directions
4
+ module BakeNumberedNotes
5
+ class V3
6
+ # for the try it notes, must be called AFTER bake_exercises
7
+ def bake(book:, classes:, suppress_solution: true)
8
+ classes.each do |klass|
9
+ book.chapters.pages.notes("$.#{klass}").each do |note|
10
+ note.wrap_children(class: 'os-note-body')
11
+ previous_example = note.previous
12
+ os_number = previous_example&.first('.os-number')&.children&.to_s
12
13
 
13
- note.prepend(child:
14
- <<~HTML
15
- <h3 class="os-title">
16
- <span class="os-title-label">#{note.autogenerated_title}</span>
17
- <span class="os-number">#{os_number}</span>
18
- </h3>
19
- HTML
20
- )
21
-
22
- note.title&.trash
23
- note.exercises.each do |exercise|
24
- Kitchen::Directions::BakeNumberedNotes.bake_note_exercise(
25
- note: note, exercise: exercise, divider: '. ', suppress_solution: suppress_solution
14
+ note.prepend(child:
15
+ <<~HTML
16
+ <h3 class="os-title">
17
+ <span class="os-title-label">#{note.autogenerated_title}</span>
18
+ <span class="os-number">#{os_number}</span>
19
+ </h3>
20
+ HTML
26
21
  )
22
+
23
+ note.title&.trash
24
+ note.exercises.each do |exercise|
25
+ BakeNumberedNotes.bake_note_exercise(
26
+ note: note, exercise: exercise, divider: '. ', suppress_solution: suppress_solution
27
+ )
28
+ end
29
+ note.injected_questions.each do |question|
30
+ BakeNumberedNotes.bake_note_injected_question(note: note, question: question)
31
+ end
27
32
  end
28
33
  end
29
34
  end
@@ -3,9 +3,10 @@
3
3
  module Kitchen
4
4
  module Directions
5
5
  module BakeNumberedExercise
6
- def self.v1(exercise:, number:, suppress_solution_if: false, note_suppressed_solutions: false)
6
+ def self.v1(exercise:, number:, suppress_solution_if: false,
7
+ note_suppressed_solutions: false, cases: false)
7
8
  V1.new.bake(exercise: exercise, number: number, suppress_solution_if: suppress_solution_if,
8
- note_suppressed_solutions: note_suppressed_solutions)
9
+ note_suppressed_solutions: note_suppressed_solutions, cases: cases)
9
10
  end
10
11
 
11
12
  def self.bake_solution_v1(exercise:, number:, divider: '. ')
@@ -2,14 +2,15 @@
2
2
 
3
3
  module Kitchen::Directions::BakeNumberedExercise
4
4
  class V1
5
- def bake(exercise:, number:, suppress_solution_if: false, note_suppressed_solutions: false)
5
+ def bake(exercise:, number:, suppress_solution_if: false,
6
+ note_suppressed_solutions: false, cases: false)
6
7
  problem = exercise.problem
7
8
  solution = exercise.solution
8
9
 
9
- exercise.pantry(name: :link_text).store(
10
- "#{I18n.t(:exercise_label)} #{exercise.ancestor(:chapter).count_in(:book)}.#{number}",
11
- label: exercise.id
12
- )
10
+ # Store label information
11
+ label_number = "#{exercise.ancestor(:chapter).count_in(:book)}.#{number}"
12
+ exercise.target_label(label_text: 'exercise', custom_content: label_number, cases: cases)
13
+
13
14
  problem_number = "<span class='os-number'>#{number}</span>"
14
15
 
15
16
  suppress_solution =
@@ -37,25 +38,6 @@ module Kitchen::Directions::BakeNumberedExercise
37
38
  <div class="os-problem-container">#{problem.children}</div>
38
39
  HTML
39
40
  )
40
-
41
- # Bake multipart questions
42
- multipart_questions = problem.search('div.question-stem')
43
- return unless multipart_questions.count > 1
44
-
45
- multipart_clipboard = Kitchen::Clipboard.new
46
- multipart_questions.each do |question|
47
- question.wrap('<li>')
48
- question = question.parent
49
- question.cut(to: multipart_clipboard)
50
- end
51
-
52
- problem.first('div.question-stimulus, div.exercise-stimulus').append(sibling:
53
- <<~HTML
54
- <ol type="a">
55
- #{multipart_clipboard.paste}
56
- </ol>
57
- HTML
58
- )
59
41
  end
60
42
 
61
43
  def bake_solution(exercise:, number:, divider: '. ')
@@ -36,12 +36,12 @@ module Kitchen
36
36
  end
37
37
  end
38
38
 
39
- def bake(table:, number:)
39
+ def bake(table:, number:, cases: false)
40
40
  table.remove_attribute('summary')
41
41
  table.wrap(%(<div class="os-table">))
42
42
 
43
- table_label = "#{I18n.t(:table_label)} #{number}"
44
- table.pantry(name: :link_text).store table_label, label: table.id if table.id
43
+ # Store label information
44
+ table.target_label(label_text: 'table', custom_content: number, cases: cases)
45
45
 
46
46
  if table.top_titled?
47
47
  custom_table = CustomBody.new(table: table,
@@ -3,12 +3,12 @@
3
3
  module Kitchen
4
4
  module Directions
5
5
  module BakeNumberedTable
6
- def self.v1(table:, number:, always_caption: false)
7
- V1.new.bake(table: table, number: number, always_caption: always_caption)
6
+ def self.v1(table:, number:, always_caption: false, cases: false)
7
+ V1.new.bake(table: table, number: number, always_caption: always_caption, cases: cases)
8
8
  end
9
9
 
10
- def self.v2(table:, number:)
11
- V2.new.bake(table: table, number: number)
10
+ def self.v2(table:, number:, cases: false)
11
+ V2.new.bake(table: table, number: number, cases: cases)
12
12
  end
13
13
  end
14
14
  end
@@ -3,8 +3,8 @@
3
3
  module Kitchen::Directions::BakeNumberedTable
4
4
  class V1
5
5
 
6
- def bake(table:, number:, always_caption: false)
7
- Kitchen::Directions::BakeTableBody::V1.new.bake(table: table, number: number)
6
+ def bake(table:, number:, always_caption: false, cases: false)
7
+ Kitchen::Directions::BakeTableBody::V1.new.bake(table: table, number: number, cases: cases)
8
8
 
9
9
  # TODO: extra spaces added here to match legacy implementation, but probably not meaningful?
10
10
  new_caption = ''
@@ -31,7 +31,7 @@ module Kitchen::Directions::BakeNumberedTable
31
31
  table.append(sibling:
32
32
  <<~HTML
33
33
  <div class="os-caption-container">
34
- <span class="os-title-label">#{I18n.t(:table_label)} </span>
34
+ <span class="os-title-label">#{I18n.t("table#{'.nominative' if cases}")} </span>
35
35
  <span class="os-number">#{number}</span>
36
36
  <span class="os-divider"> </span>#{caption_title}
37
37
  <span class="os-divider"> </span>#{new_caption}
@@ -5,8 +5,8 @@ module Kitchen::Directions::BakeNumberedTable
5
5
  # V2 caption titles are nested within an .os-caption span
6
6
  class V2
7
7
 
8
- def bake(table:, number:)
9
- Kitchen::Directions::BakeTableBody::V1.new.bake(table: table, number: number)
8
+ def bake(table:, number:, cases: false)
9
+ Kitchen::Directions::BakeTableBody::V1.new.bake(table: table, number: number, cases: cases)
10
10
 
11
11
  caption = ''
12
12
  if table&.caption&.first("span[data-type='title']") && !table.top_captioned?
@@ -19,7 +19,7 @@ module Kitchen::Directions::BakeNumberedTable
19
19
  table.append(sibling:
20
20
  <<~HTML
21
21
  <div class="os-caption-container">
22
- <span class="os-title-label">#{I18n.t(:table_label)} </span>
22
+ <span class="os-title-label">#{I18n.t("table#{'.nominative' if cases}")} </span>
23
23
  <span class="os-number">#{number}</span>
24
24
  <span class="os-divider"> </span>
25
25
  #{caption}
@@ -103,7 +103,7 @@ module Kitchen
103
103
  raise "do not know what TOC class to use for page with classes #{page.classes}"
104
104
  end
105
105
  when CompositePageElement
106
- if page.is_index?
106
+ if page.is_index? || page.is_index_of_type?
107
107
  'os-toc-index'
108
108
  elsif page.is_citation_reference?
109
109
  'os-toc-reference'
@@ -8,13 +8,7 @@ module Kitchen::Directions::MoveSolutionsFromExerciseSection
8
8
 
9
9
  class V1
10
10
  def bake(chapter:, append_to:, section_class:, title_number:)
11
- solutions_clipboard = Kitchen::Clipboard.new
12
- chapter.search("section.#{section_class}").exercises.each do |exercise|
13
- solution = exercise.solution
14
- next unless solution
15
-
16
- solution.cut(to: solutions_clipboard)
17
- end
11
+ solutions_clipboard = chapter.search("section.#{section_class}").solutions.cut
18
12
 
19
13
  return if solutions_clipboard.items.empty?
20
14
 
@@ -7,13 +7,7 @@ module Kitchen::Directions::MoveSolutionsFromNumberedNote
7
7
 
8
8
  class V1
9
9
  def bake(chapter:, append_to:, note_class:)
10
- solutions_clipboard = Kitchen::Clipboard.new
11
- chapter.notes("$.#{note_class}").exercises.each do |exercise|
12
- solution = exercise.solution
13
- next unless solution
14
-
15
- solution.cut(to: solutions_clipboard)
16
- end
10
+ solutions_clipboard = chapter.notes("$.#{note_class}").solutions.cut
17
11
 
18
12
  return if solutions_clipboard.items.empty?
19
13
 
@@ -17,6 +17,23 @@ module Kitchen::Directions::MoveSolutionsToAnswerKey
17
17
  Kitchen::Directions::MoveSolutionsFromNumberedNote.v1(
18
18
  chapter: chapter, append_to: append_to, note_class: 'your-turn'
19
19
  )
20
+
21
+ # Bake section exercises
22
+ chapter.non_introduction_pages.each do |page|
23
+ number = "#{chapter.count_in(:book)}.#{page.count_in(:chapter)}"
24
+ Kitchen::Directions::MoveSolutionsFromExerciseSection.v1(
25
+ chapter: page, append_to: append_to, section_class: 'section-exercises',
26
+ title_number: number
27
+ )
28
+ end
29
+
30
+ # Bake other exercise sections
31
+ classes = %w[chapter-review chapter-test]
32
+ classes.each do |klass|
33
+ Kitchen::Directions::MoveSolutionsFromExerciseSection.v1(
34
+ chapter: chapter, append_to: append_to, section_class: klass
35
+ )
36
+ end
20
37
  end
21
38
  end
22
39
  end
@@ -67,6 +67,9 @@ module Kitchen
67
67
  # @!method remove_attribute
68
68
  # Removes an attribute from the element
69
69
  # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#remove_attribute-instance_method Nokogiri::XML::Node#remove_attribute
70
+ # @!method key?(attribute)
71
+ # Returns true if attribute is set
72
+ # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#key%3F-instance_method Nokogiri::XML::Node#key?(attribute)
70
73
  # @!method classes
71
74
  # Gets the element's classes
72
75
  # @see https://www.rubydoc.info/github/sparklemotion/nokogiri/Nokogiri/XML/Node#classes-instance_method Nokogiri::XML::Node#classes
@@ -81,7 +84,7 @@ module Kitchen
81
84
  # @return Object
82
85
  def_delegators :@node, :name=, :name, :[], :[]=, :add_class, :remove_class,
83
86
  :text, :wrap, :children, :to_html, :remove_attribute,
84
- :classes, :path, :inner_html=
87
+ :key?, :classes, :path, :inner_html=
85
88
 
86
89
  # @!method config
87
90
  # Get the config for this element's document
@@ -722,11 +725,44 @@ module Kitchen
722
725
  end
723
726
  end
724
727
 
728
+ # Creates labels for links to inside elements
729
+ # like Figures, Tables, Equations, Exercises, Notes.
730
+ #
731
+ # @param label_text [String] label of the element defined in yml file.
732
+ # (e.g. "Figure", "Table", "Equation")
733
+ # @param custom_content [String] might be numbering of the element or text
734
+ # copied from content (e.g. note title)
735
+ # @param cases [Boolean] true if labels should use grammatical cases
736
+ # (used in Polish books)
737
+ # @return [Pantry]
738
+ #
739
+ def target_label(label_text: nil, custom_content: nil, cases: false)
740
+ if cases
741
+ cases = %w[nominative genitive dative accusative instrumental locative vocative]
742
+ element_labels = {}
743
+
744
+ cases.each do |label_case|
745
+ element_labels[label_case] = "#{I18n.t("#{label_text}.#{label_case}")} #{custom_content}"
746
+
747
+ element_label_case = element_labels[label_case]
748
+
749
+ pantry(name: "#{label_case}_link_text").store element_label_case, label: id if id
750
+ end
751
+ else
752
+ element_label = if label_text
753
+ "#{I18n.t(label_text.to_s)} #{custom_content}"
754
+ else
755
+ custom_content
756
+ end
757
+ pantry(name: :link_text).store element_label, label: id if id
758
+ end
759
+ end
760
+
725
761
  # @!method pages
726
762
  # Returns a pages enumerator
727
763
  def_delegators :as_enumerator, :pages, :chapters, :terms, :figures, :notes, :tables, :examples,
728
764
  :metadatas, :non_introduction_pages, :units, :titles, :exercises, :references,
729
- :composite_pages, :composite_chapters
765
+ :composite_pages, :composite_chapters, :solutions, :injected_questions
730
766
 
731
767
  # Returns this element as an enumerator (over only one element, itself)
732
768
  #
@@ -283,6 +283,41 @@ module Kitchen
283
283
  chain_to(MetadataElementEnumerator, css_or_xpath: css_or_xpath, only: only, except: except)
284
284
  end
285
285
 
286
+ # Returns an enumerator that iterates through solutions within the scope of this enumerator
287
+ #
288
+ # @param css_or_xpath [String] additional selectors to further narrow the element iterated over;
289
+ # a "$" in this argument will be replaced with the default selector for the element being
290
+ # iterated over.
291
+ # @param only [Symbol, Callable] the name of a method to call on an element or a
292
+ # lambda or proc that accepts an element; elements will only be included in the
293
+ # search results if the method or callable returns true
294
+ # @param except [Symbol, Callable] the name of a method to call on an element or a
295
+ # lambda or proc that accepts an element; elements will not be included in the
296
+ # search results if the method or callable returns false
297
+ #
298
+ def solutions(css_or_xpath=nil, only: nil, except: nil)
299
+ block_error_if(block_given?)
300
+ chain_to(SolutionElementEnumerator, css_or_xpath: css_or_xpath, only: only, except: except)
301
+ end
302
+
303
+ # Returns an enumerator that iterates through injected questions within the scope of this enumerator
304
+ #
305
+ # @param css_or_xpath [String] additional selectors to further narrow the element iterated over;
306
+ # a "$" in this argument will be replaced with the default selector for the element being
307
+ # iterated over.
308
+ # @param only [Symbol, Callable] the name of a method to call on an element or a
309
+ # lambda or proc that accepts an element; elements will only be included in the
310
+ # search results if the method or callable returns true
311
+ # @param except [Symbol, Callable] the name of a method to call on an element or a
312
+ # lambda or proc that accepts an element; elements will not be included in the
313
+ # search results if the method or callable returns false
314
+ #
315
+ def injected_questions(css_or_xpath=nil, only: nil, except: nil)
316
+ block_error_if(block_given?)
317
+ chain_to(InjectedQuestionElementEnumerator,
318
+ css_or_xpath: css_or_xpath, only: only, except: except)
319
+ end
320
+
286
321
  # Returns an enumerator that iterates within the scope of this enumerator
287
322
  #
288
323
  # @param css_or_xpath [String] additional selectors to further narrow the element iterated over
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ # An element for an example
5
+ #
6
+ class InjectedQuestionElement < ElementBase
7
+
8
+ # Creates a new +InjectedQuestionElement+
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: InjectedQuestionElementEnumerator)
17
+ end
18
+
19
+ # Returns the short type
20
+ # @return [Symbol]
21
+ #
22
+ def self.short_type
23
+ :injected_question
24
+ end
25
+
26
+ # Returns the question stimulus as an element.
27
+ #
28
+ # @return [Element]
29
+ #
30
+ def stimulus
31
+ first('div[data-type="question-stimulus"]')
32
+ end
33
+
34
+ # Returns the question stem as an element.
35
+ #
36
+ # @return [Element]
37
+ #
38
+ def stem
39
+ first('div[data-type="question-stem"]')
40
+ end
41
+
42
+ # Returns the list of answers as an element.
43
+ #
44
+ # @return [Element]
45
+ #
46
+ def answers
47
+ first("ol[data-type='question-answers']")
48
+ end
49
+
50
+ # Returns the solution element.
51
+ #
52
+ # @return [Element]
53
+ #
54
+ def solution
55
+ first("div[data-type='question-solution']")
56
+ end
57
+
58
+ # Returns the answer correctness given an alphabet
59
+ #
60
+ # @return [Array]
61
+ #
62
+ def correct_answer_letters(alphabet)
63
+ answers.search('li[data-type="question-answer"]').each_with_index.map \
64
+ do |answer, index|
65
+ answer[:'data-correctness'] == '1.0' ? alphabet[index] : nil
66
+ end.compact
67
+ end
68
+
69
+ # Returns or creates the question's id
70
+ #
71
+ # @return [String]
72
+ #
73
+ def id
74
+ self[:id] ||= "auto_#{ancestor(:page).id.gsub(/page_/, '')}_#{self[:'data-id']}"
75
+ end
76
+ end
77
+ end