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
@@ -2,9 +2,10 @@
2
2
 
3
3
  module Kitchen::Directions::BakeChapterSectionExercises
4
4
  class V1
5
- def bake(chapter:)
5
+ def bake(chapter:, trash_title:)
6
6
  chapter.pages.each do |page|
7
7
  page.search('section.section-exercises').each do |section|
8
+ section.first('h3[data-type="title"]')&.trash if trash_title
8
9
  section.wrap(
9
10
  %(<div class="os-eos os-section-exercises-container"
10
11
  data-uuid-key=".section-exercises">)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ module Directions
5
+ module BakeChapterSolutions
6
+ def self.v1(chapter:, metadata_source:, uuid_prefix: '')
7
+ V1.new.bake(chapter: chapter, metadata_source: metadata_source, uuid_prefix: uuid_prefix)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::BakeChapterSolutions
4
+ class V1
5
+ renderable
6
+
7
+ def bake(chapter:, metadata_source:, uuid_prefix: '')
8
+ @metadata = metadata_source.children_to_keep.copy
9
+ @klass = 'solutions'
10
+ @title = I18n.t(:eoc_solutions_title)
11
+ @uuid_prefix = uuid_prefix
12
+
13
+ solutions_clipboard = Kitchen::Clipboard.new
14
+
15
+ chapter.search('section.free-response').each do |free_response_question|
16
+ exercises = free_response_question.exercises
17
+ # must run AFTER .free-response notes are baked
18
+
19
+ next if exercises.none?
20
+
21
+ exercises.each do |exercise|
22
+ solution = exercise.solution
23
+ next unless solution.present?
24
+
25
+ solution.cut(to: solutions_clipboard)
26
+ end
27
+ end
28
+
29
+ @content = solutions_clipboard.paste
30
+
31
+ @in_composite_chapter = false
32
+
33
+ chapter.append(child: render(file:
34
+ '../../templates/eoc_section_title_template.xhtml.erb'))
35
+ end
36
+ end
37
+ end
@@ -27,15 +27,22 @@ module Kitchen
27
27
  # TODO: include specific page types somehow without writing it out
28
28
  chapter.non_introduction_pages.each do |page|
29
29
  summary = page.summary
30
+
31
+ next if summary.nil?
32
+
30
33
  summary.first("[data-type='title']")&.trash # get rid of old title if exists
31
34
  summary_title = page.title.copy
32
35
  summary_title.name = 'h3'
33
- summary_title.replace_children(with: <<~HTML
34
- <span class="os-number">#{chapter.count_in(:book)}.#{page.count_in(:chapter)}</span>
35
- <span class="os-divider"> </span>
36
- <span class="os-text" data-type="" itemprop="">#{summary_title.children}</span>
37
- HTML
38
- )
36
+
37
+ unless summary_title.children.search('span.os-number').present?
38
+ summary_title.replace_children(with:
39
+ <<~HTML
40
+ <span class="os-number">#{chapter.count_in(:book)}.#{page.count_in(:chapter)}</span>
41
+ <span class="os-divider"> </span>
42
+ <span class="os-text" data-type="" itemprop="">#{summary_title.children}</span>
43
+ HTML
44
+ )
45
+ end
39
46
 
40
47
  summary.prepend(child:
41
48
  <<~HTML
@@ -3,7 +3,7 @@
3
3
  module Kitchen
4
4
  module Directions
5
5
  module BakeExample
6
- def self.v1(example:, number:, title_tag:)
6
+ def self.v1(example:, number:, title_tag:, numbered_solutions: false)
7
7
  example.wrap_children(class: 'body')
8
8
 
9
9
  example.prepend(child:
@@ -21,7 +21,10 @@ module Kitchen
21
21
  .store("#{I18n.t(:example_label)} #{number}", label: example.id)
22
22
 
23
23
  example.titles.each do |title|
24
- next if title.parent.has_class?('os-caption-container')
24
+ if title.parent.has_class?('os-caption-container') || \
25
+ title.parent.has_class?('os-caption')
26
+ next
27
+ end
25
28
 
26
29
  title.name = 'h4'
27
30
  end
@@ -33,10 +36,16 @@ module Kitchen
33
36
  end
34
37
 
35
38
  if (solution = exercise.solution)
39
+ solution_number = if numbered_solutions
40
+ "<span class=\"os-number\">#{exercise.count_in(:example)}</span>"
41
+ else
42
+ ''
43
+ end
36
44
  solution.replace_children(with:
37
45
  <<~HTML
38
46
  <h4 data-type="solution-title">
39
47
  <span class="os-title-label">#{I18n.t(:solution)} </span>
48
+ #{solution_number}
40
49
  </h4>
41
50
  <div class="os-solution-container">#{solution.children}</div>
42
51
  HTML
@@ -4,7 +4,20 @@ module Kitchen
4
4
  module Directions
5
5
  module BakeFigure
6
6
  def self.v1(figure:, number:)
7
+ return if figure.has_class?('unnumbered') && !figure.has_class?('splash')
8
+
7
9
  figure.wrap(%(<div class="os-figure#{' has-splash' if figure.has_class?('splash')}">))
10
+ if figure.has_class?('unnumbered') && figure.has_class?('splash')
11
+ caption = figure.caption&.cut
12
+ figure.append(sibling:
13
+ <<~HTML
14
+ <div class="os-caption-container">
15
+ #{"<span class=\"os-caption\">#{caption.children}</span>" if caption}
16
+ </div>
17
+ HTML
18
+ )
19
+ return
20
+ end
8
21
 
9
22
  figure.pantry(name: :link_text).store "#{I18n.t(:figure)} #{number}", label: figure.id
10
23
  title = figure.title&.cut
@@ -3,7 +3,13 @@
3
3
  module Kitchen
4
4
  module Directions
5
5
  module BakeFirstElements
6
- def self.v1(within:, selectors:)
6
+ def self.v1(within:)
7
+ selectors = [
8
+ 'div.os-problem-container > div.os-table',
9
+ 'div.os-problem-container > span[data-type="media"]',
10
+ 'div.os-solution-container > div.os-table',
11
+ 'div.os-solution-container > span[data-type="media"]'
12
+ ]
7
13
  selectors.each do |selector|
8
14
  within.search("#{selector}:first-child").each do |problem|
9
15
  problem.add_class('first-element')
@@ -3,8 +3,8 @@
3
3
  module Kitchen
4
4
  module Directions
5
5
  module BakeFootnotes
6
- def self.v1(book:)
7
- V1.new.bake(book: book)
6
+ def self.v1(book:, number_format: :arabic)
7
+ V1.new.bake(book: book, number_format: number_format)
8
8
  end
9
9
  end
10
10
  end
@@ -3,37 +3,40 @@
3
3
  module Kitchen::Directions::BakeFootnotes
4
4
  class V1
5
5
 
6
- def bake(book:)
6
+ def bake(book:, number_format: :arabic)
7
7
  # Footnotes are numbered either within their top-level pages (preface,
8
8
  # appendices, etc) or within chapters. Tackle each case separately
9
9
 
10
10
  book.body.element_children.only(Kitchen::PageElement,
11
11
  Kitchen::CompositePageElement,
12
12
  Kitchen::CompositeChapterElement).each do |page|
13
- bake_footnotes_within(page)
13
+ bake_footnotes_within(page, number_format: number_format)
14
14
  end
15
15
 
16
16
  book.chapters.each do |chapter|
17
- bake_footnotes_within(chapter)
17
+ bake_footnotes_within(chapter, number_format: number_format)
18
18
  end
19
19
  end
20
20
 
21
- def bake_footnotes_within(container)
22
- footnote_number = 0
21
+ def bake_footnotes_within(container, number_format:)
22
+ footnote_count = 0
23
23
  aside_id_to_footnote_number = {}
24
24
 
25
25
  container.search("a[role='doc-noteref']").each do |anchor|
26
- footnote_number += 1
27
- anchor.replace_children(with: footnote_number.to_s)
26
+ footnote_count += 1
27
+ footnote_number = footnote_count.to_format(number_format)
28
+ anchor.replace_children(with: footnote_number)
28
29
  aside_id = anchor[:href][1..-1]
29
30
  aside_id_to_footnote_number[aside_id] = footnote_number
31
+ anchor.parent.add_class('has-noteref') if anchor.parent.name == 'p'
30
32
  end
31
33
 
32
34
  container.search('aside').each do |aside|
33
35
  footnote_number = aside_id_to_footnote_number[aside.id]
34
36
  aside.prepend(child: "<div data-type='footnote-number'>#{footnote_number}</div>")
35
37
  end
36
- end
37
38
 
39
+ footnote_count
40
+ end
38
41
  end
39
42
  end
@@ -24,6 +24,8 @@ module Kitchen
24
24
 
25
25
  chapter.non_introduction_pages.each do |page|
26
26
  further_research = page.first('.further-research')
27
+ next unless further_research.present?
28
+
27
29
  further_research.first("[data-type='title']")&.trash # get rid of old title if exists
28
30
  further_research_title = page.title.copy
29
31
  further_research_title.name = 'h3'
@@ -27,13 +27,6 @@ module Kitchen::Directions::BakeIndex
27
27
  def initialize(term_text:)
28
28
  @term_text = term_text
29
29
  @terms = []
30
-
31
- # Sort by transliterated version first to support accent marks,
32
- # then by the raw text to support the same text with different capitalization
33
- @sortable = [
34
- ActiveSupport::Inflector.transliterate(term_text).downcase,
35
- term_text
36
- ]
37
30
  end
38
31
 
39
32
  def add_term(term)
@@ -49,12 +42,8 @@ module Kitchen::Directions::BakeIndex
49
42
  end
50
43
 
51
44
  def <=>(other)
52
- sortable <=> other.sortable
45
+ I18n.sort_strings(term_text, other.term_text)
53
46
  end
54
-
55
- protected
56
-
57
- attr_reader :sortable
58
47
  end
59
48
 
60
49
  class IndexSection
@@ -147,8 +136,8 @@ module Kitchen::Directions::BakeIndex
147
136
  end
148
137
 
149
138
  def add_term_to_index(term_element, page_title)
150
- group_by = term_element.text.strip[0]
151
- group_by = I18n.t(:eob_index_symbols_group) unless group_by.match?(/\w/)
139
+ group_by = I18n.transliterate(term_element.text.strip[0])
140
+ group_by = I18n.t(:eob_index_symbols_group) unless group_by.match?(/[[:alpha:]]/)
152
141
  term_element['group-by'] = group_by
153
142
 
154
143
  # Add it to our index object
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ module Directions
5
+ # Bakes inline lists with the desired list separator
6
+ # Does not separate the last list item
7
+ #
8
+ module BakeInlineLists
9
+ LIST_SEPARATOR = '; '
10
+ SEPARATOR_CLASS = '-os-inline-list-separator'
11
+
12
+ def self.v1(book:)
13
+ inline_lists = book.search('span[data-display="inline"][data-type="list"]')
14
+ inline_lists.each do |list|
15
+ list.search('span[data-type="item"]')[0..-2].each do |item|
16
+ item.append(child: "<span class=\"#{SEPARATOR_CLASS}\">#{LIST_SEPARATOR}</span>")
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen
4
+ module Directions
5
+ module BakeNumberedNotes
6
+ def self.v1(book:, classes:)
7
+ V1.new.bake(book: book, classes: classes)
8
+ end
9
+
10
+ def self.v2(book:, classes:)
11
+ V2.new.bake(book: book, classes: classes)
12
+ end
13
+
14
+ # V3 bakes notes tied to an example immediately previous ("Try It" notes)
15
+ # Must be called AFTER BakeExercises
16
+ #
17
+ def self.v3(book:, classes:, suppress_solution: true)
18
+ V3.new.bake(book: book, classes: classes, suppress_solution: suppress_solution)
19
+ end
20
+
21
+ # Used by V1, V2, V3
22
+ def self.bake_note_exercise(note:, exercise:, divider: ' ', suppress_solution: false)
23
+ exercise.add_class('unnumbered')
24
+ # bake problem
25
+ exercise.problem.wrap_children('div', class: 'os-problem-container')
26
+ exercise.search('[data-type="commentary"]').each(&:trash)
27
+ return unless exercise.solution
28
+
29
+ # bake solution in place
30
+ if suppress_solution
31
+ exercise.add_class('os-hasSolution')
32
+ exercise.solution.trash
33
+ else
34
+ BakeNumberedExercise.bake_solution_v1(
35
+ exercise: exercise,
36
+ number: note.first('.os-number').text.gsub(/#/, ''),
37
+ divider: divider
38
+ )
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::BakeNumberedNotes
4
+
5
+ class V1
6
+ def bake(book:, classes:)
7
+ classes.each do |klass|
8
+ book.chapters.notes("$.#{klass}").each do |note|
9
+ bake_note(note: note)
10
+ note.exercises.each do |exercise|
11
+ Kitchen::Directions::BakeNumberedNotes.bake_note_exercise(note: note, exercise: exercise)
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ def bake_note(note:)
18
+ note.wrap_children(class: 'os-note-body')
19
+
20
+ chapter_count = note.ancestor(:chapter).count_in(:book)
21
+ note_count = note.count_in(:chapter)
22
+ note.prepend(child:
23
+ <<~HTML
24
+ <h3 class="os-title">
25
+ <span class="os-title-label">#{note.autogenerated_title}</span>
26
+ <span class="os-number">#{chapter_count}.#{note_count}</span>
27
+ <span class="os-divider"> </span>
28
+ </h3>
29
+ HTML
30
+ )
31
+
32
+ return unless note['use-subtitle']
33
+
34
+ Kitchen::Directions::BakeNoteSubtitle.v1(note: note)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kitchen::Directions::BakeNumberedNotes
4
+ class V2
5
+ def bake(book:, classes:)
6
+ classes.each do |klass|
7
+ book.chapters.pages.notes("$.#{klass}").each do |note|
8
+ note.wrap_children(class: 'os-note-body')
9
+ note_count = note.count_in(:page)
10
+ note.prepend(child:
11
+ <<~HTML
12
+ <h3 class="os-title">
13
+ <span class="os-title-label">#{note.autogenerated_title}</span>
14
+ <span class="os-number">##{note_count}</span>
15
+ </h3>
16
+ HTML
17
+ )
18
+ note.exercises.each do |exercise|
19
+ Kitchen::Directions::BakeNumberedNotes.bake_note_exercise(note: note, exercise: exercise, divider: '. ')
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
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
12
+
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
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end