openstax_kitchen 11.0.0 → 11.1.0

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