openstax_kitchen 1.0.0

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