openstax_kitchen 1.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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/devcontainer.json +19 -0
  3. data/.github/workflows/tests.yml +36 -0
  4. data/.gitignore +20 -0
  5. data/.rspec +3 -0
  6. data/.solargraph.yml +15 -0
  7. data/CHANGELOG.md +11 -0
  8. data/CODE_OF_CONDUCT.md +74 -0
  9. data/Dockerfile +19 -0
  10. data/Gemfile +9 -0
  11. data/Gemfile.lock +74 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +674 -0
  14. data/Rakefile +6 -0
  15. data/bin/console +14 -0
  16. data/bin/normalize +79 -0
  17. data/bin/setup +8 -0
  18. data/books/chemistry2e/bake.rb +133 -0
  19. data/codecov.yaml +27 -0
  20. data/docker-compose.yml +12 -0
  21. data/docker/bash +1 -0
  22. data/docker/entrypoint +9 -0
  23. data/lib/kitchen.rb +57 -0
  24. data/lib/kitchen/ancestor.rb +30 -0
  25. data/lib/kitchen/book_document.rb +18 -0
  26. data/lib/kitchen/book_element.rb +24 -0
  27. data/lib/kitchen/book_element_enumerator.rb +5 -0
  28. data/lib/kitchen/book_recipe.rb +25 -0
  29. data/lib/kitchen/chapter_element.rb +35 -0
  30. data/lib/kitchen/chapter_element_enumerator.rb +13 -0
  31. data/lib/kitchen/clipboard.rb +37 -0
  32. data/lib/kitchen/composite_chapter_element.rb +23 -0
  33. data/lib/kitchen/composite_page_element.rb +27 -0
  34. data/lib/kitchen/composite_page_element_enumerator.rb +13 -0
  35. data/lib/kitchen/config.rb +20 -0
  36. data/lib/kitchen/counter.rb +34 -0
  37. data/lib/kitchen/debug/print_recipe_error.rb +80 -0
  38. data/lib/kitchen/directions/bake_appendix.rb +26 -0
  39. data/lib/kitchen/directions/bake_chapter_glossary.rb +34 -0
  40. data/lib/kitchen/directions/bake_chapter_introductions.rb +58 -0
  41. data/lib/kitchen/directions/bake_chapter_key_equations.rb +30 -0
  42. data/lib/kitchen/directions/bake_chapter_summary.rb +52 -0
  43. data/lib/kitchen/directions/bake_composite_pages.rb +13 -0
  44. data/lib/kitchen/directions/bake_example.rb +31 -0
  45. data/lib/kitchen/directions/bake_exercises.rb +164 -0
  46. data/lib/kitchen/directions/bake_figure.rb +25 -0
  47. data/lib/kitchen/directions/bake_footnotes/main.rb +11 -0
  48. data/lib/kitchen/directions/bake_footnotes/v1.rb +38 -0
  49. data/lib/kitchen/directions/bake_index/main.rb +11 -0
  50. data/lib/kitchen/directions/bake_index/v1.rb +138 -0
  51. data/lib/kitchen/directions/bake_index/v1.xhtml.erb +28 -0
  52. data/lib/kitchen/directions/bake_math_in_paragraph.rb +13 -0
  53. data/lib/kitchen/directions/bake_notes.rb +58 -0
  54. data/lib/kitchen/directions/bake_numbered_table/main.rb +11 -0
  55. data/lib/kitchen/directions/bake_numbered_table/v1.rb +47 -0
  56. data/lib/kitchen/directions/bake_stepwise.rb +27 -0
  57. data/lib/kitchen/directions/bake_toc.rb +103 -0
  58. data/lib/kitchen/directions/bake_unnumbered_tables.rb +14 -0
  59. data/lib/kitchen/directions/move_title_text_into_span.rb +15 -0
  60. data/lib/kitchen/document.rb +142 -0
  61. data/lib/kitchen/element.rb +15 -0
  62. data/lib/kitchen/element_base.rb +444 -0
  63. data/lib/kitchen/element_enumerator.rb +12 -0
  64. data/lib/kitchen/element_enumerator_base.rb +101 -0
  65. data/lib/kitchen/element_enumerator_factory.rb +111 -0
  66. data/lib/kitchen/element_factory.rb +32 -0
  67. data/lib/kitchen/errors.rb +4 -0
  68. data/lib/kitchen/example_element.rb +20 -0
  69. data/lib/kitchen/example_element_enumerator.rb +13 -0
  70. data/lib/kitchen/figure_element.rb +20 -0
  71. data/lib/kitchen/figure_element_enumerator.rb +13 -0
  72. data/lib/kitchen/mixins/block_error_if.rb +19 -0
  73. data/lib/kitchen/note_element.rb +43 -0
  74. data/lib/kitchen/note_element_enumerator.rb +13 -0
  75. data/lib/kitchen/oven.rb +61 -0
  76. data/lib/kitchen/page_element.rb +51 -0
  77. data/lib/kitchen/page_element_enumerator.rb +13 -0
  78. data/lib/kitchen/pantry.rb +35 -0
  79. data/lib/kitchen/patches/nokogiri.rb +31 -0
  80. data/lib/kitchen/patches/renderable.rb +31 -0
  81. data/lib/kitchen/patches/string.rb +5 -0
  82. data/lib/kitchen/recipe.rb +78 -0
  83. data/lib/kitchen/search_history.rb +33 -0
  84. data/lib/kitchen/selectors/base.rb +8 -0
  85. data/lib/kitchen/selectors/standard_1.rb +12 -0
  86. data/lib/kitchen/table_element.rb +36 -0
  87. data/lib/kitchen/table_element_enumerator.rb +13 -0
  88. data/lib/kitchen/term_element.rb +16 -0
  89. data/lib/kitchen/term_element_enumerator.rb +13 -0
  90. data/lib/kitchen/transliterations.rb +19 -0
  91. data/lib/kitchen/type_casting_element_enumerator.rb +23 -0
  92. data/lib/kitchen/utils.rb +19 -0
  93. data/lib/kitchen/version.rb +3 -0
  94. data/lib/locales/en.yml +21 -0
  95. data/lib/notes.md +9 -0
  96. data/openstax_kitchen.gemspec +39 -0
  97. data/tutorials/00/expected_baked.html +3 -0
  98. data/tutorials/00/raw.html +3 -0
  99. data/tutorials/00/solution_1.rb +7 -0
  100. data/tutorials/00/solution_2.rb +6 -0
  101. data/tutorials/01/expected_baked.html +66 -0
  102. data/tutorials/01/raw.html +62 -0
  103. data/tutorials/01/solution_1.rb +16 -0
  104. data/tutorials/01/solution_2.rb +24 -0
  105. data/tutorials/02/expected_baked.html +207 -0
  106. data/tutorials/02/raw.html +201 -0
  107. data/tutorials/02/solution_1.rb +29 -0
  108. data/tutorials/03/expected_baked.html +33 -0
  109. data/tutorials/03/raw.html +31 -0
  110. data/tutorials/03/solution_1.rb +16 -0
  111. data/tutorials/03/solution_2.rb +15 -0
  112. data/tutorials/04/expected_baked.html +36 -0
  113. data/tutorials/04/raw.html +36 -0
  114. data/tutorials/04/solution_1.rb +20 -0
  115. data/tutorials/04/solution_2.rb +25 -0
  116. data/tutorials/05/expected_baked.html +11 -0
  117. data/tutorials/05/raw.html +11 -0
  118. data/tutorials/05/solution_1.rb +9 -0
  119. data/tutorials/check_it +64 -0
  120. data/tutorials/setup_my_recipes +30 -0
  121. metadata +278 -0
@@ -0,0 +1,52 @@
1
+ module Kitchen
2
+ module Directions
3
+ module BakeChapterSummary
4
+
5
+ def self.v1(chapter:, metadata_source:)
6
+ metadata_elements = metadata_source.search(%w(.authors .publishers .print-style
7
+ .permissions [data-type='subject'])).copy
8
+
9
+ summaries = Clipboard.new
10
+
11
+ # TODO include specific page types somehow without writing it out
12
+ chapter.pages("$:not(.introduction)").each do |page|
13
+ summary = page.summary
14
+ summary.first("[data-type='title']").trash # get rid of old title
15
+ summary_title = page.title.copy
16
+ summary_title.name = "h3"
17
+ summary_title.replace_children(with: <<~HTML
18
+ <span class="os-number">#{chapter.count_in(:book)}.#{page.count_in(:chapter)}</span>
19
+ <span class="os-divider"> </span>
20
+ <span class="os-text" data-type="" itemprop="">#{summary_title.children}</span>
21
+ HTML
22
+ )
23
+
24
+ summary.prepend(child:
25
+ <<~HTML
26
+ <a href="##{page.title.id}">
27
+ #{summary_title.paste}
28
+ </a>
29
+ HTML
30
+ )
31
+ summary.cut(to: summaries)
32
+ end
33
+
34
+ chapter.append(child:
35
+ <<~HTML
36
+ <div class="os-eoc os-summary-container" data-type="composite-page" data-uuid-key=".summary">
37
+ <h2 data-type="document-title">
38
+ <span class="os-text">#{I18n.t(:eoc_summary_title)}</span>
39
+ </h2>
40
+ <div data-type="metadata" style="display: none;">
41
+ <h1 data-type="document-title" itemprop="name">#{I18n.t(:eoc_summary_title)}</h1>
42
+ #{metadata_elements.paste}
43
+ </div>
44
+ #{summaries.paste}
45
+ </div>
46
+ HTML
47
+ ) unless summaries.none?
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ module Kitchen
2
+ module Directions
3
+ module BakeCompositePages
4
+
5
+ def self.v1(book:)
6
+ book.search("[data-type='composite-page']").each do |page|
7
+ page.id = "composite-page-#{page.count_in(:book)}"
8
+ end
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ module Kitchen
2
+ module Directions
3
+ module BakeExample
4
+
5
+ def self.v1(example:, number:, title_tag:)
6
+ example.replace_children(with:
7
+ <<~HTML
8
+ <div class="body">
9
+ #{example.children}
10
+ </div>
11
+ HTML
12
+ )
13
+
14
+ example.prepend(child:
15
+ <<~HTML
16
+ <#{title_tag} class="os-title">
17
+ <span class="os-title-label">#{I18n.t(:example_label)} </span>
18
+ <span class="os-number">#{number}</span>
19
+ <span class="os-divider"> </span>
20
+ </#{title_tag}>
21
+ HTML
22
+ )
23
+
24
+ example.document.pantry(name: :link_text).store "#{I18n.t(:example_label)} #{number}", label: example.id
25
+
26
+ example.titles.each {|title| title.name = "h4"}
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,164 @@
1
+ module Kitchen
2
+ module Directions
3
+ module BakeExercises
4
+
5
+ def self.v1(book:)
6
+ metadata_elements = book.metadata.search(%w(.authors .publishers .print-style
7
+ .permissions [data-type='subject'])).copy
8
+
9
+ solutions_clipboards = []
10
+
11
+ book.chapters.each do |chapter|
12
+ exercise_clipboard = Clipboard.new
13
+ solution_clipboard = Clipboard.new
14
+ solutions_clipboards.push(solution_clipboard)
15
+
16
+ chapter.pages("$:not(.introduction)").each do |page|
17
+ exercise_section = page.exercises
18
+ exercise_section.first("[data-type='title']")&.trash
19
+ exercise_section_title = page.title.copy
20
+ exercise_section_title.name = "h3"
21
+ exercise_section_title.replace_children(with: <<~HTML
22
+ <span class="os-number">#{chapter.count_in(:book)}.#{page.count_in(:chapter)}</span>
23
+ <span class="os-divider"> </span>
24
+ <span class="os-text" data-type="" itemprop="">#{exercise_section_title.children}</span>
25
+ HTML
26
+ )
27
+
28
+ exercise_section.prepend(child:
29
+ <<~HTML
30
+ <a href="##{page.title.id}">
31
+ #{exercise_section_title.paste}
32
+ </a>
33
+ HTML
34
+ )
35
+
36
+ exercise_section.search("[data-type='exercise']").each do |exercise|
37
+ exercise.document.pantry(name: :link_text).store(
38
+ "#{I18n.t(:exercise_label)} #{chapter.count_in(:book)}.#{exercise.count_in(:chapter)}",
39
+ label: exercise.id
40
+ )
41
+
42
+ bake_exercise_in_place(exercise: exercise)
43
+ exercise.first("[data-type='solution']")&.cut(to: solution_clipboard)
44
+ end
45
+
46
+ exercise_section.cut(to: exercise_clipboard)
47
+ end
48
+
49
+ chapter.append(child:
50
+ <<~HTML
51
+ <div class="os-eoc os-exercises-container" data-type="composite-page" data-uuid-key=".exercises">
52
+ <h2 data-type="document-title">
53
+ <span class="os-text">#{I18n.t(:eoc_exercises_title)}</span>
54
+ </h2>
55
+ <div data-type="metadata" style="display: none;">
56
+ <h1 data-type="document-title" itemprop="name">#{I18n.t(:eoc_exercises_title)}</h1>
57
+ #{metadata_elements.paste}
58
+ </div>
59
+ #{exercise_clipboard.paste}
60
+ </div>
61
+ HTML
62
+ ) unless exercise_clipboard.none?
63
+ end
64
+
65
+ # Store a paste here to use at end so that uniquifyied IDs match legacy baking
66
+ eob_metadata = metadata_elements.paste
67
+
68
+ solutions = solutions_clipboards.map.with_index do |solution_clipboard, index|
69
+ <<~HTML
70
+ <div class="os-eob os-solution-container " data-type="composite-page" data-uuid-key=".solution#{index+1}">
71
+ <h2 data-type="document-title">
72
+ <span class="os-text">#{index+1}</span>
73
+ </h2>
74
+ <div data-type="metadata" style="display: none;">
75
+ <h1 data-type="document-title" itemprop="name">#{index+1}</h1>
76
+ #{metadata_elements.paste}
77
+ </div>
78
+ #{solution_clipboard.paste}
79
+ </div>
80
+ HTML
81
+ end
82
+
83
+ book.first("body").append(child:
84
+ <<~HTML
85
+ <div class="os-eob os-solution-container " data-type="composite-chapter" data-uuid-key=".solution">
86
+ <h1 data-type="document-title" id="composite-chapter-1">
87
+ <span class="os-text">#{I18n.t(:eoc_answer_key_title)}</span>
88
+ </h1>
89
+ <div data-type="metadata" style="display: none;">
90
+ <h1 data-type="document-title" itemprop="name">#{I18n.t(:eoc_answer_key_title)}</h1>
91
+ #{eob_metadata}
92
+ </div>
93
+ #{solutions.join("\n")}
94
+ </div>
95
+ HTML
96
+ ) unless solutions.none?
97
+ end
98
+
99
+ def self.bake_exercise_in_place(exercise:)
100
+ # Bake an exercise in place going from:
101
+ #
102
+ # <div data-type="exercise" id="exerciseId">
103
+ # <div data-type="problem" id="problemId">
104
+ # Problem Content
105
+ # </div>
106
+ # <div data-type="solution" id="solutionId">
107
+ # Solution Content
108
+ # </div>
109
+ # </div>
110
+ #
111
+ # to
112
+ #
113
+ # <div data-type="exercise" id="exerciseId" class="os-hasSolution">
114
+ # <div data-type="problem" id="problemId">
115
+ # <a class="os-number" href="#exerciseId-solution">1</a>
116
+ # <span class="os-divider">. </span>
117
+ # <div class="os-problem-container ">
118
+ # Problem Content
119
+ # </div>
120
+ # </div>
121
+ # <div data-type="solution" id="exerciseId-solution">
122
+ # <a class="os-number" href="#exerciseId">1</a>
123
+ # <span class="os-divider">. </span>
124
+ # <div class="os-solution-container ">
125
+ # Solution Content
126
+ # </div>
127
+ # </div>
128
+ # </div>
129
+ #
130
+ # If there is no solution, don't add the 'os-hasSolution' class and don't
131
+ # link the number.
132
+
133
+ problem = exercise.first("[data-type='problem']")
134
+ solution = exercise.first("[data-type='solution']")
135
+
136
+ problem_number = "<span class='os-number'>#{exercise.count_in(:chapter)}</span>"
137
+
138
+ if solution.present?
139
+ solution.id = "#{exercise.id}-solution"
140
+
141
+ exercise.add_class("os-hasSolution")
142
+ problem_number = "<a href='##{solution.id}' class='os-number' >#{exercise.count_in(:chapter)}</a>"
143
+
144
+ solution.replace_children(with:
145
+ <<~HTML
146
+ <a class="os-number" href="##{exercise.id}">#{exercise.count_in(:chapter)}</a>
147
+ <span class="os-divider">. </span>
148
+ <div class="os-solution-container ">#{solution.children}</div>
149
+ HTML
150
+ )
151
+ end
152
+
153
+ problem.replace_children(with:
154
+ <<~HTML
155
+ #{problem_number}
156
+ <span class="os-divider">. </span>
157
+ <div class="os-problem-container ">#{problem.children}</div>
158
+ HTML
159
+ )
160
+ end
161
+
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,25 @@
1
+ module Kitchen
2
+ module Directions
3
+ module BakeFigure
4
+
5
+ def self.v1(figure:, number:)
6
+ figure.wrap(%Q(<div class="os-figure#{' has-splash' if figure.has_class?('splash')}">))
7
+
8
+ figure.document.pantry(name: :link_text).store "Figure #{number}", label: figure.id
9
+
10
+ caption = figure.caption&.cut
11
+ figure.append(sibling:
12
+ <<~HTML
13
+ <div class="os-caption-container">
14
+ <span class="os-title-label">Figure </span>
15
+ <span class="os-number">#{number}</span>
16
+ <span class="os-divider"> </span>
17
+ <span class="os-divider"> </span>
18
+ #{'<span class="os-caption">' + caption.children.to_s + '</span>'if caption}
19
+ </div>
20
+ HTML
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module Kitchen
2
+ module Directions
3
+ module BakeFootnotes
4
+
5
+ def self.v1(book:)
6
+ V1.new.bake(book: book)
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,38 @@
1
+ module Kitchen::Directions::BakeFootnotes
2
+ class V1
3
+
4
+ def bake(book:)
5
+ # Footnotes are numbered either within their top-level pages (preface,
6
+ # appendices, etc) or within chapters. Tackle each case separately
7
+
8
+ book.body.element_children.only(Kitchen::PageElement,
9
+ Kitchen::CompositePageElement).each do |page|
10
+ bake_footnotes_within(page)
11
+ end
12
+
13
+ book.chapters.each do |chapter|
14
+ bake_footnotes_within(chapter)
15
+ end
16
+ end
17
+
18
+ def bake_footnotes_within(container)
19
+ footnote_number = 0
20
+ aside_id_to_footnote_number = {}
21
+
22
+ container.search("a[role='doc-noteref']").each do |anchor|
23
+ footnote_number += 1
24
+ anchor.replace_children(with: footnote_number.to_s)
25
+ aside_id = anchor[:href][1..-1]
26
+ aside_id_to_footnote_number[aside_id] = footnote_number
27
+ end
28
+
29
+
30
+ container.search("aside").each do |aside|
31
+ footnote_number = aside_id_to_footnote_number[aside.id]
32
+ aside.prepend(child: "<div class='footnote-number'>#{footnote_number}</div>")
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,11 @@
1
+ module Kitchen
2
+ module Directions
3
+ module BakeIndex
4
+
5
+ def self.v1(book:)
6
+ V1.new.bake(book: book)
7
+ end
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,138 @@
1
+ module Kitchen::Directions::BakeIndex
2
+ class V1
3
+ renderable
4
+
5
+ class Term
6
+ attr_reader :text, :id, :group_by, :page_title
7
+
8
+ def initialize(text:, id:, group_by:, page_title:)
9
+ @text = text.strip
10
+ @id = id
11
+ @group_by = group_by
12
+ @page_title = page_title
13
+ end
14
+ end
15
+
16
+ class IndexItem
17
+ attr_reader :term_text
18
+ attr_reader :terms
19
+
20
+ def initialize(term_text:)
21
+ @term_text = term_text
22
+ @terms = []
23
+
24
+ # Sort by transliterated version first to support accent marks,
25
+ # then by the raw text to support the same text with different capitalization
26
+ @sortable = [
27
+ ActiveSupport::Inflector.transliterate(term_text).downcase,
28
+ term_text
29
+ ]
30
+ end
31
+
32
+ def add_term(term)
33
+ @terms.push(term)
34
+ end
35
+
36
+ def uncapitalize_term_text!
37
+ @term_text = @term_text.uncapitalize
38
+ end
39
+
40
+ def <=>(other)
41
+ self.sortable <=> other.sortable
42
+ end
43
+
44
+ protected
45
+
46
+ attr_reader :sortable
47
+ end
48
+
49
+ class IndexSection
50
+ attr_reader :name
51
+ attr_reader :items
52
+
53
+ def initialize(name:)
54
+ @force_first = name == I18n.t(:eob_index_symbols_group)
55
+ @name = name
56
+ @items = SortedSet.new
57
+ @items_by_term_text = {}
58
+ end
59
+
60
+ def add_term(term)
61
+ item_for(term).add_term(term)
62
+ end
63
+
64
+ def <=>(other)
65
+ return -1 if force_first
66
+ return 1 if other.force_first
67
+ self.name <=> other.name
68
+ end
69
+
70
+ protected
71
+
72
+ attr_reader :force_first
73
+
74
+ def item_for(term)
75
+ @items_by_term_text[term.text] ||= begin
76
+ different_caps_item = @items_by_term_text[term.text.uncapitalize]
77
+ different_caps_item&.uncapitalize_term_text!
78
+
79
+ (different_caps_item || IndexItem.new(term_text: term.text)).tap do |item|
80
+ @items.add(item)
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ class Index
87
+ attr_reader :sections
88
+
89
+ def initialize
90
+ @sections = SortedSet.new
91
+ @sections_by_name = {}
92
+ end
93
+
94
+ def add_term(term)
95
+ section_named(term.group_by.capitalize).add_term(term)
96
+ end
97
+
98
+ protected
99
+
100
+ def section_named(name)
101
+ @sections_by_name[name] ||= begin
102
+ IndexSection.new(name: name).tap do |section|
103
+ @sections.add(section)
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ def bake(book:)
110
+ @metadata_elements = book.metadata.search(%w(.authors .publishers .print-style
111
+ .permissions [data-type='subject'])).copy
112
+ @index = Index.new
113
+
114
+ book.pages.terms.each do |term_element|
115
+ # Markup the term
116
+ page = term_element.ancestor(:page)
117
+ term_element.id = "auto_#{page.id}_term#{term_element.count_in(:book)}"
118
+
119
+ group_by = term_element.text.strip[0]
120
+ if !group_by.match? (/\w/)
121
+ group_by = I18n.t(:eob_index_symbols_group)
122
+ end
123
+ term_element['group-by'] = group_by
124
+
125
+ # Add it to our index object
126
+ @index.add_term(Term.new(
127
+ text: term_element.text,
128
+ id: term_element.id,
129
+ group_by: group_by,
130
+ page_title: page.title.text.gsub(/\n/,'')
131
+ ))
132
+ end
133
+
134
+ book.first("body").append(child: render(file: 'v1.xhtml.erb'))
135
+ end
136
+
137
+ end
138
+ end