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,12 @@
1
+ module Kitchen
2
+ class ElementEnumerator < ElementEnumeratorBase
3
+
4
+ def self.factory
5
+ ElementEnumeratorFactory.new(
6
+ sub_element_class: Element,
7
+ enumerator_class: self
8
+ )
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,101 @@
1
+ module Kitchen
2
+ class ElementEnumeratorBase < Enumerator
3
+
4
+ def initialize(size=nil, css_or_xpath: nil, upstream_enumerator: nil)
5
+ @css_or_xpath = css_or_xpath
6
+ @upstream_enumerator = upstream_enumerator
7
+ super(size)
8
+ end
9
+
10
+ def search_history
11
+ (@upstream_enumerator&.search_history || SearchHistory.empty).add(@css_or_xpath)
12
+ end
13
+
14
+ def terms(css_or_xpath=nil, &block)
15
+ chain_to(TermElementEnumerator, css_or_xpath: css_or_xpath, &block)
16
+ end
17
+
18
+ def pages(css_or_xpath=nil, &block)
19
+ chain_to(PageElementEnumerator, css_or_xpath: css_or_xpath, &block)
20
+ end
21
+
22
+ def chapters(css_or_xpath=nil, &block)
23
+ chain_to(ChapterElementEnumerator, css_or_xpath: css_or_xpath, &block)
24
+ end
25
+
26
+ # use block_error_if
27
+ def figures(css_or_xpath=nil, &block)
28
+ chain_to(FigureElementEnumerator, css_or_xpath: css_or_xpath, &block)
29
+ end
30
+
31
+ def notes(css_or_xpath=nil, &block)
32
+ chain_to(NoteElementEnumerator, css_or_xpath: css_or_xpath, &block)
33
+ end
34
+
35
+ def tables(css_or_xpath=nil, &block)
36
+ chain_to(TableElementEnumerator, css_or_xpath: css_or_xpath, &block)
37
+ end
38
+
39
+ def examples(css_or_xpath=nil, &block)
40
+ chain_to(ExampleElementEnumerator, css_or_xpath: css_or_xpath, &block)
41
+ end
42
+
43
+ def search(css_or_xpath=nil, &block)
44
+ chain_to(ElementEnumerator, css_or_xpath: css_or_xpath, &block)
45
+ end
46
+
47
+ def chain_to(enumerator_class, css_or_xpath: nil, &block)
48
+ raise(RecipeError, "Did you forget a `.each` call on this enumerator?") if block_given?
49
+
50
+ enumerator_class.factory.build_within(self, css_or_xpath: css_or_xpath)
51
+ end
52
+
53
+ def first!(missing_message: "Could not return a first result")
54
+ first || raise(RecipeError, "#{missing_message} matching #{search_history.latest} " \
55
+ "inside [#{search_history.upstream}]")
56
+ end
57
+
58
+ # Removes enumerated elements from their parent and places them on the specified clipboard
59
+ #
60
+ # @param to [Symbol, String, Clipboard, nil] the name of the clipboard (or a Clipboard
61
+ # object) to cut to. String values are converted to symbols. If not provided, the
62
+ # elements are placed on a new clipboard.
63
+ # @return [Clipboard] the clipboard
64
+ #
65
+ def cut(to: nil)
66
+ to ||= Clipboard.new
67
+ self.each do |element|
68
+ element.cut(to: to)
69
+ end
70
+ to
71
+ end
72
+
73
+ # Makes a copy of the enumerated elements and places them on the specified clipboard.
74
+ #
75
+ # @param to [Symbol, String, Clipboard, nil] the name of the clipboard (or a Clipboard
76
+ # object) to copy to. String values are converted to symbols. If not provided, the
77
+ # copies are placed on a new clipboard.
78
+ # @return [Clipboard] the clipboard
79
+ #
80
+ def copy(to: nil)
81
+ to ||= Clipboard.new
82
+ self.each do |element|
83
+ element.copy(to: to)
84
+ end
85
+ to
86
+ end
87
+
88
+ def trash
89
+ self.each(&:trash)
90
+ end
91
+
92
+ def [](index)
93
+ to_a[index]
94
+ end
95
+
96
+ def to_s
97
+ self.map(&:to_s).join("")
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,111 @@
1
+ module Kitchen
2
+ class ElementEnumeratorFactory
3
+
4
+ attr_reader :default_css_or_xpath
5
+ attr_reader :enumerator_class
6
+ attr_reader :sub_element_class
7
+ attr_reader :detect_sub_element_class
8
+
9
+ def initialize(default_css_or_xpath: nil, sub_element_class: nil,
10
+ enumerator_class:, detect_sub_element_class: false)
11
+ @default_css_or_xpath = default_css_or_xpath
12
+ @sub_element_class = sub_element_class
13
+ @enumerator_class = enumerator_class
14
+ @detect_sub_element_class = detect_sub_element_class
15
+ end
16
+
17
+ # TODO spec this!
18
+ def apply_default_css_or_xpath_and_normalize(css_or_xpath=nil)
19
+ css_or_xpath ||= "$"
20
+ [css_or_xpath].flatten.each {|item| item.gsub!(/\$/, [default_css_or_xpath].flatten.join(", ")) }
21
+ [css_or_xpath].flatten
22
+ end
23
+
24
+ def build_within(enumerator_or_element, css_or_xpath: nil)
25
+ css_or_xpath = apply_default_css_or_xpath_and_normalize(css_or_xpath)
26
+
27
+ case enumerator_or_element
28
+ when ElementBase
29
+ build_within_element(enumerator_or_element, css_or_xpath: css_or_xpath)
30
+ when ElementEnumeratorBase
31
+ build_within_other_enumerator(enumerator_or_element, css_or_xpath: css_or_xpath)
32
+ end
33
+ end
34
+
35
+ def or_with(other_factory)
36
+ self.class.new(
37
+ default_css_or_xpath: default_css_or_xpath + ", " + other_factory.default_css_or_xpath,
38
+ enumerator_class: TypeCastingElementEnumerator,
39
+ detect_sub_element_class: true
40
+ )
41
+ end
42
+
43
+ protected
44
+
45
+ def build_within_element(element, css_or_xpath:)
46
+ enumerator_class.new(css_or_xpath: css_or_xpath) do |block|
47
+ grand_ancestors = element.ancestors
48
+ parent_ancestor = Ancestor.new(element)
49
+
50
+ num_sub_elements = 0
51
+
52
+ element.raw.search(*css_or_xpath).each_with_index do |sub_node, index|
53
+ sub_element = ElementFactory.build_from_node(
54
+ node: sub_node,
55
+ document: element.document,
56
+ element_class: sub_element_class,
57
+ default_short_type: Utils.search_path_to_type(css_or_xpath),
58
+ detect_element_class: detect_sub_element_class
59
+ )
60
+
61
+ # If the provided `css_or_xpath` has already been counted, we need to uncount
62
+ # them on the ancestors so that when they are counted again below, the counts
63
+ # are correct. Only do this on the first loop!
64
+ if index == 0
65
+ if element.have_sub_elements_already_been_counted?(css_or_xpath)
66
+ grand_ancestors.values.each do |ancestor|
67
+ ancestor.decrement_descendant_count(
68
+ sub_element.short_type,
69
+ by: element.number_of_sub_elements_already_counted(css_or_xpath)
70
+ )
71
+ end
72
+ end
73
+ end
74
+
75
+ # Record this sub element's ancestors and increment their descendant counts
76
+ sub_element.add_ancestors(grand_ancestors, parent_ancestor)
77
+ sub_element.count_as_descendant
78
+
79
+ # Remember how this sub element was found so can trace search history given
80
+ # any element.
81
+ sub_element.css_or_xpath_that_found_me = css_or_xpath
82
+
83
+ # Count runs through this loop for below
84
+ num_sub_elements += 1
85
+
86
+ # Mark the location so that if there's an error we can show the developer where.
87
+ sub_element.document.location = sub_element
88
+
89
+ block.yield(sub_element)
90
+ end
91
+
92
+ element.remember_that_sub_elements_are_already_counted(
93
+ css_or_xpath: css_or_xpath, count: num_sub_elements
94
+ )
95
+ end
96
+ end
97
+
98
+ def build_within_other_enumerator(other_enumerator, css_or_xpath:)
99
+ # Return a new enumerator instance that internally iterates over `other_enumerator`
100
+ # running a new enumerator for each element returned by that other enumerator.
101
+ enumerator_class.new(css_or_xpath: css_or_xpath, upstream_enumerator: other_enumerator) do |block|
102
+ other_enumerator.each do |element|
103
+ build_within_element(element, css_or_xpath: css_or_xpath).each do |sub_element|
104
+ block.yield(sub_element)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,32 @@
1
+ module Kitchen
2
+ class ElementFactory
3
+
4
+ ELEMENT_CLASSES = ElementBase.descendants
5
+
6
+ def self.specific_element_class_for_node(node)
7
+ ELEMENT_CLASSES.find do |klass|
8
+ klass.is_the_element_class_for?(node)
9
+ end || Element
10
+ end
11
+
12
+ def self.build_from_node(node:,
13
+ document:,
14
+ element_class: nil,
15
+ default_short_type: nil,
16
+ detect_element_class: false)
17
+ element_class ||= detect_element_class ?
18
+ specific_element_class_for_node(node) :
19
+ Element
20
+
21
+ if element_class == Element
22
+ element_class.new(node: node,
23
+ document: document,
24
+ short_type: default_short_type)
25
+ else
26
+ element_class.new(node: node,
27
+ document: document)
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ module Kitchen
2
+ class RecipeError < StandardError; end
3
+ class ElementNotFoundError < StandardError; end
4
+ end
@@ -0,0 +1,20 @@
1
+ module Kitchen
2
+ class ExampleElement < ElementBase
3
+
4
+ def initialize(node:, document: nil)
5
+ super(node: node,
6
+ document: document,
7
+ enumerator_class: ExampleElementEnumerator,
8
+ short_type: :example)
9
+ end
10
+
11
+ def titles
12
+ search("span[data-type='title']")
13
+ end
14
+
15
+ def self.is_the_element_class_for?(node)
16
+ node['data-type'] == "example"
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Kitchen
2
+ class ExampleElementEnumerator < ElementEnumeratorBase
3
+
4
+ def self.factory
5
+ ElementEnumeratorFactory.new(
6
+ default_css_or_xpath: "div[data-type='example']", # TODO element.document.selectors.example
7
+ sub_element_class: ExampleElement,
8
+ enumerator_class: self
9
+ )
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ module Kitchen
2
+ class FigureElement < ElementBase
3
+
4
+ def initialize(node:, document: nil)
5
+ super(node: node,
6
+ document: document,
7
+ enumerator_class: FigureElementEnumerator,
8
+ short_type: :figure)
9
+ end
10
+
11
+ def caption
12
+ first("figcaption")
13
+ end
14
+
15
+ def self.is_the_element_class_for?(node)
16
+ node.name == "figure"
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module Kitchen
2
+ class FigureElementEnumerator < ElementEnumeratorBase
3
+
4
+ def self.factory
5
+ ElementEnumeratorFactory.new(
6
+ default_css_or_xpath: "figure", # TODO get from config?
7
+ sub_element_class: FigureElement,
8
+ enumerator_class: self
9
+ )
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module Kitchen
2
+ module Mixins
3
+ module BlockErrorIf
4
+
5
+ def block_error_if(block_given)
6
+ calling_method = begin
7
+ this_method_location_index = caller_locations.find_index do |location|
8
+ location.label == "block_error_if"
9
+ end
10
+
11
+ caller_locations[(this_method_location_index || -1) + 1].label
12
+ end
13
+
14
+ raise(RecipeError, "The `#{calling_method}` method does not take a block argument") if block_given
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ module Kitchen
2
+ class NoteElement < ElementBase
3
+
4
+ TITLE_TRANSLATION_KEYS = %w(
5
+ link-to-learning
6
+ everyday-life
7
+ sciences-interconnect
8
+ chemist-portrait
9
+ )
10
+
11
+ def initialize(node:, document: nil)
12
+ super(node: node,
13
+ document: document,
14
+ enumerator_class: NoteElementEnumerator,
15
+ short_type: :note)
16
+ end
17
+
18
+ def title
19
+ block_error_if(block_given?)
20
+ first("[data-type='title']")
21
+ end
22
+
23
+ def indicates_autogenerated_title?
24
+ translation_key_in(TITLE_TRANSLATION_KEYS).present?
25
+ end
26
+
27
+ def autogenerated_title
28
+ translation_key = translation_key_in(TITLE_TRANSLATION_KEYS)
29
+ I18n.t(:"notes.#{document.short_name}.#{translation_key}", default: :"notes.#{translation_key}")
30
+ end
31
+
32
+ def translation_key_in(possible_translation_keys)
33
+ keys = possible_translation_keys & classes
34
+ raise("too many translation keys: #{keys.join(', ')}") if keys.many?
35
+ keys.first
36
+ end
37
+
38
+ def self.is_the_element_class_for?(node)
39
+ node['data-type'] == "note"
40
+ end
41
+
42
+ end
43
+ end
@@ -0,0 +1,13 @@
1
+ module Kitchen
2
+ class NoteElementEnumerator < ElementEnumeratorBase
3
+
4
+ def self.factory
5
+ ElementEnumeratorFactory.new(
6
+ default_css_or_xpath: "div[data-type='note']", # TODO get from config?
7
+ sub_element_class: NoteElement,
8
+ enumerator_class: self
9
+ )
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,61 @@
1
+ module Kitchen
2
+ class Oven
3
+
4
+ def self.bake(input_file:,
5
+ config_file: nil,
6
+ recipes:,
7
+ output_file:)
8
+
9
+ profile = BakeProfile.new
10
+ profile.started!
11
+
12
+ nokogiri_doc = File.open(input_file) do |f|
13
+ profile.opened!
14
+ Nokogiri::XML(f).tap { profile.parsed! }
15
+ end
16
+
17
+ config = config_file.nil? ? nil : Config.new_from_file(File.open(config_file))
18
+
19
+ doc = Kitchen::Document.new(
20
+ nokogiri_document: nokogiri_doc,
21
+ config: config
22
+ )
23
+
24
+ [recipes].flatten.each do |recipe|
25
+ recipe.document = doc
26
+ recipe.bake
27
+ end
28
+ profile.baked!
29
+
30
+ File.open(output_file, "w") do |f|
31
+ f.write doc.to_xhtml(indent:2)
32
+ end
33
+ profile.written!
34
+
35
+ profile
36
+ end
37
+
38
+ class BakeProfile
39
+ def started!; @started_at = Time.now; end
40
+ def opened!; @opened_at = Time.now; end
41
+ def parsed!; @parsed_at = Time.now; end
42
+ def baked!; @baked_at = Time.now; end
43
+ def written!; @written_at = Time.now; end
44
+
45
+ def open_seconds; @opened_at - @started_at; end
46
+ def parse_seconds; @parsed_at - @opened_at; end
47
+ def bake_seconds; @baked_at - @parsed_at; end
48
+ def write_seconds; @written_at - @baked_at; end
49
+
50
+ def to_s
51
+ <<~STRING
52
+ Open: #{open_seconds} s
53
+ Parse: #{parse_seconds} s
54
+ Bake: #{bake_seconds} s
55
+ Write: #{write_seconds} s
56
+ STRING
57
+ end
58
+ end
59
+
60
+ end
61
+ end